(() => { const { Button, Icon } = window.TaskFlowDesignSystem_dd53c2; /* Verifikasi jigsaw: geser potongan sampai pas dengan lubangnya. Gambar latar dibuat sendiri di canvas (tidak ada aset eksternal) dan posisi lubang diacak setiap percobaan. */ const W = 320, H = 180, SIZE = 52, TOL = 8; function drawBackdrop(ctx) { const g = ctx.createLinearGradient(0, 0, W, H); g.addColorStop(0, "#0F6F5C"); g.addColorStop(0.55, "#1FA588"); g.addColorStop(1, "#9BD9A8"); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); const dots = [[46, 42, 34], [255, 58, 46], [128, 138, 60], [210, 128, 40], [80, 150, 22], [292, 120, 30], [170, 40, 20]]; dots.forEach(([x, y, r], i) => { ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = i % 2 ? "rgba(255,255,255,.16)" : "rgba(9,64,54,.20)"; ctx.fill(); }); ctx.strokeStyle = "rgba(255,255,255,.22)"; ctx.lineWidth = 2; for (let i = -1; i < 6; i++) { ctx.beginPath(); ctx.moveTo(i * 64, H); ctx.lineTo(i * 64 + 70, 0); ctx.stroke(); } } function piecePath(ctx, x, y) { const s = SIZE, k = s * 0.22; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + s * 0.35, y); ctx.arc(x + s * 0.5, y, k, Math.PI, 0, true); ctx.lineTo(x + s, y); ctx.lineTo(x + s, y + s * 0.35); ctx.arc(x + s, y + s * 0.5, k, -Math.PI / 2, Math.PI / 2, false); ctx.lineTo(x + s, y + s); ctx.lineTo(x, y + s); ctx.closePath(); } function PuzzleGate({ onPass, onCancel, role }) { const holeX = React.useMemo(() => 120 + Math.round(Math.random() * 140), []); const holeY = React.useMemo(() => 30 + Math.round(Math.random() * 80), []); const [x, setX] = React.useState(6); const [state, setState] = React.useState("idle"); // idle | ok | fail const [tries, setTries] = React.useState(0); const [seed, setSeed] = React.useState(0); const boardRef = React.useRef(null); const pieceRef = React.useRef(null); const dragRef = React.useRef(null); React.useEffect(() => { const b = boardRef.current, p = pieceRef.current; if (!b || !p) return; const dpr = Math.min(window.devicePixelRatio || 1, 2); [[b, W, H], [p, SIZE + 14, SIZE + 14]].forEach(([c, w, h]) => { c.width = w * dpr; c.height = h * dpr; c.style.width = w + "px"; c.style.height = h + "px"; c.getContext("2d").setTransform(dpr, 0, 0, dpr, 0, 0); }); const bc = b.getContext("2d"); bc.clearRect(0, 0, W, H); drawBackdrop(bc); // lubang bc.save(); piecePath(bc, holeX, holeY); bc.fillStyle = "rgba(6,40,34,.55)"; bc.fill(); bc.strokeStyle = "rgba(255,255,255,.65)"; bc.lineWidth = 2; bc.stroke(); bc.restore(); // potongan: gambar ulang latar lalu potong sesuai path const pc = p.getContext("2d"); pc.clearRect(0, 0, SIZE + 14, SIZE + 14); pc.save(); pc.translate(7 - holeX, 7 - holeY); piecePath(pc, holeX, holeY); pc.clip(); drawBackdrop(pc); pc.restore(); pc.save(); pc.translate(7 - holeX, 7 - holeY); piecePath(pc, holeX, holeY); pc.strokeStyle = "rgba(255,255,255,.9)"; pc.lineWidth = 2; pc.stroke(); pc.restore(); }, [holeX, holeY, seed]); const startDrag = (e) => { if (state === "ok") return; dragRef.current = { startX: e.clientX, from: x }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId); }; const onDrag = (e) => { if (!dragRef.current) return; const next = Math.max(6, Math.min(W - SIZE - 6, dragRef.current.from + (e.clientX - dragRef.current.startX))); setX(next); if (state === "fail") setState("idle"); }; // dipakai saat potongan dilepas maupun saat rel geser dilepas const check = () => { if (state === "ok") return; if (Math.abs(x - holeX) <= TOL) { setState("ok"); setTimeout(onPass, 620); } else { setState("fail"); setTries((n) => n + 1); setTimeout(() => { setX(6); setState("idle"); }, 700); } }; const endDrag = () => { if (!dragRef.current) return; dragRef.current = null; check(); }; const refresh = () => { setX(6); setState("idle"); setSeed((s) => s + 1); }; const tone = state === "ok" ? "var(--leaf-500)" : state === "fail" ? "var(--coral-500)" : "var(--teal-500)"; return (
Verifikasi Keamanan Geser potongan puzzle sampai pas sebelum masuk sebagai {role === "admin" ? "Direktur" : role === "marketing" ? "Marketing" : "User"}.
{/* rel geser — bisa dipakai juga tanpa menyeret potongan langsung */}
{ setX(Number(e.target.value)); if (state === "fail") setState("idle"); }} onMouseUp={check} onTouchEnd={check} onKeyUp={(e) => (e.key === "Enter" || e.key === " ") && check()} style={{ flex: 1, accentColor: tone, height: 28 }} aria-label="Geser potongan puzzle" />
{state === "ok" ? "Terverifikasi — membuka dashboard…" : state === "fail" ? `Belum pas, coba lagi. Percobaan gagal: ${tries}.` : "Seret potongan atau gunakan rel di bawah, lalu lepas."}
); } Object.assign(window, { PuzzleGate }); })();