From 5f059ab3e540d3f1be1062e4212531578a7f147b Mon Sep 17 00:00:00 2001 From: sadik5622 Date: Wed, 7 Oct 2026 12:19:16 +0600 Subject: [PATCH] Enhance guided tour with new steps for API keys, blocklist, and webhooks - Added tour steps for the API keys, blocklist, and webhooks pages to improve user onboarding. - Updated PageHeader components to include tour identifiers for better navigation. - Enhanced the TourOverlay functionality to support new tour steps and improve user interaction. --- frontend/app/(console)/api-keys/page.tsx | 4 +- frontend/app/(console)/blocklist/page.tsx | 4 +- frontend/app/(console)/webhooks/page.tsx | 4 +- frontend/components/tour/steps.ts | 41 ++++++++++++ frontend/components/tour/tour.tsx | 76 +++++++++++++++-------- 5 files changed, 98 insertions(+), 31 deletions(-) diff --git a/frontend/app/(console)/api-keys/page.tsx b/frontend/app/(console)/api-keys/page.tsx index d04d490..d26abf5 100644 --- a/frontend/app/(console)/api-keys/page.tsx +++ b/frontend/app/(console)/api-keys/page.tsx @@ -146,12 +146,12 @@ export default function ApiKeysPage() { return ( <> - setCreating(true)}>Make a key} /> - + {(rows) => rows.length ? ( diff --git a/frontend/app/(console)/blocklist/page.tsx b/frontend/app/(console)/blocklist/page.tsx index a72083f..00ac8fa 100644 --- a/frontend/app/(console)/blocklist/page.tsx +++ b/frontend/app/(console)/blocklist/page.tsx @@ -129,7 +129,7 @@ export default function BlocklistPage() { return ( <> - - +
Type diff --git a/frontend/app/(console)/webhooks/page.tsx b/frontend/app/(console)/webhooks/page.tsx index 765e2d0..11157db 100644 --- a/frontend/app/(console)/webhooks/page.tsx +++ b/frontend/app/(console)/webhooks/page.tsx @@ -101,14 +101,14 @@ export default function WebhooksPage() { const pages = Math.max(1, Math.ceil(total / 25)); return ( <> - setCreating(true)}>Add endpoint} /> {error &&
} {note &&

{note}

} - + {(rows) => rows.length ? ( diff --git a/frontend/components/tour/steps.ts b/frontend/components/tour/steps.ts index 28929a1..5043cb1 100644 --- a/frontend/components/tour/steps.ts +++ b/frontend/components/tour/steps.ts @@ -160,6 +160,20 @@ export const TOUR_STEPS: TourStep[] = [ bodyBn: "প্রতারণা নিশ্চিত হলে প্রতারকের অ্যাকাউন্ট ফ্রিজ করে বাকি টাকা ক্ষতিগ্রস্তদের ফেরত দেওয়া হয়। টাকা পৌঁছানো পর্যন্ত প্রতিটা রিফান্ড ট্র্যাক করা যায়।", }, + { + id: "blocklist", + chapter: "Operations", + route: "/blocklist", + target: "blocklist-list", + placement: "top", + action: "hover", + roles: REVIEW, + title: "Known scam numbers", + body: "Wallets, phone numbers and links already used by scammers. If someone tries to pay one, they are asked to check and wait first.", + titleBn: "চেনা প্রতারকদের তালিকা", + bodyBn: "প্রতারকেরা আগে যেসব ওয়ালেট, ফোন নম্বর আর লিংক ব্যবহার করেছে, সেগুলো এখানে। কেউ এগুলোতে টাকা পাঠাতে গেলে আগে যাচাই করে একটু অপেক্ষা করতে বলা হয়।", + }, + // Investigation { id: "network", @@ -276,6 +290,33 @@ export const TOUR_STEPS: TourStep[] = [ bodyBn: "প্রতিটা সিদ্ধান্ত, ফ্রিজ আর রিফান্ড এখানে লেখা থাকে, কে করেছে আর কোথা থেকে করেছে সহ। রেকর্ড ছাড়া কিছুই হয় না।", }, + { + id: "webhooks", + chapter: "Governance", + route: "/webhooks", + target: "webhooks-endpoints", + placement: "bottom", + action: "hover", + roles: AUDIT, + title: "Instant alerts to other systems", + body: "When a payment is held or a wallet is frozen, FraudLens can tell your other systems right away, and text the customer if SMS is on.", + titleBn: "অন্য সিস্টেমে সাথে সাথে খবর", + bodyBn: "কোনো পেমেন্ট আটকালে বা ওয়ালেট ফ্রিজ হলে FraudLens সাথে সাথে আপনার অন্য সিস্টেমকে জানাতে পারে, আর SMS চালু থাকলে গ্রাহককেও মেসেজ পাঠায়।", + }, + { + id: "api-keys", + chapter: "Governance", + route: "/api-keys", + target: "api-keys-list", + placement: "bottom", + action: "hover", + roles: AUDIT, + title: "Keys for partner apps", + body: "Give a wallet app its own key to check payments with FraudLens. Each key has a daily limit, shows its usage, and can be switched off anytime.", + titleBn: "পার্টনার অ্যাপের চাবি", + bodyBn: "কোনো ওয়ালেট অ্যাপকে আলাদা একটা চাবি (API key) দিন, যাতে তারা FraudLens দিয়ে পেমেন্ট চেক করতে পারে। প্রতিটার দৈনিক সীমা আছে, ব্যবহার দেখা যায়, আর যেকোনো সময় বন্ধ করা যায়।", + }, + // Customer demo { id: "nav-phone", diff --git a/frontend/components/tour/tour.tsx b/frontend/components/tour/tour.tsx index 52252c8..bb2a488 100644 --- a/frontend/components/tour/tour.tsx +++ b/frontend/components/tour/tour.tsx @@ -585,7 +585,8 @@ function TourOverlay({ const isReady = finished || ready?.nonce === nonce; const target = isReady && !finished ? (ready?.el ?? null) : null; const action = step?.action; - const showCursor = !!target && action !== "none" && !reduced; + // The guide's cursor never leaves while the tour runs, so the tour always reads as live. + const showCursor = !reduced && !finished; const rootRef = useRef(null); const holeRef = useRef(null); @@ -604,7 +605,9 @@ function TourOverlay({ reduced: false, nonce: -1, movingUntil: 0, - cursor: { x: -1, y: -1, fromX: 0, fromY: 0, ctrlX: 0, ctrlY: 0, t0: 0, dur: 0, animating: false, bend: 1, arrivedAt: 0 }, + cursor: { x: -1, y: -1, fromX: 0, fromY: 0, ctrlX: 0, ctrlY: 0, t0: 0, dur: 0, animating: false, bend: 1, arrivedAt: 0, baseX: 0, baseY: 0 }, + /** The card is showing, so the cursor can rest on its Next button when there is no target. */ + ready: false, }); useEffect(() => { @@ -615,22 +618,14 @@ function TourOverlay({ if (l.target !== target || l.nonce !== nonce) { l.movingUntil = performance.now() + (reduced ? 0 : MOVE_MS); l.nonce = nonce; - // Start a fresh cursor glide toward the new target. + // Start a fresh cursor glide; the frame loop picks its destination once there is one. const c = l.cursor; - if (target) { - if (c.x < 0) { - c.x = window.innerWidth * 0.62; - c.y = window.innerHeight * 0.82; - } - c.fromX = c.x; - c.fromY = c.y; - c.t0 = performance.now(); - c.bend = -c.bend; - c.animating = true; - c.dur = 0; - } + c.bend = -c.bend; + c.animating = true; + c.dur = 0; } l.target = target; + l.ready = isReady; }); // Resolve the step: navigate, wait for the target, scroll it into view. @@ -759,11 +754,25 @@ function TourOverlay({ // Cursor: glide on a bezier toward the (possibly moving) target centre, then stick to it. const cur = cursorRef.current; const c = l.cursor; - if (cur && box) { - const tx = box.left + box.width / 2; - const ty = box.top + box.height / 2; + // No target: rest on the card's Next button, as if about to press it. + let aim: Box | null = box; + const nextBtn = nextRef.current; + if (!aim && l.ready && nextBtn) { + const r = nextBtn.getBoundingClientRect(); + if (r.width > 0) aim = { left: r.left, top: r.top, width: r.width, height: r.height }; + } + if (cur && c.x < 0) { + c.x = c.baseX = vw * 0.62; + c.y = c.baseY = vh * 0.82; + } + if (cur && aim) { + const tx = aim.left + aim.width * (box ? 0.5 : 0.62); + const ty = aim.top + aim.height * (box ? 0.5 : 0.7); if (c.animating) { if (c.dur === 0) { + c.fromX = c.x; + c.fromY = c.y; + c.t0 = now; const dist = Math.hypot(tx - c.fromX, ty - c.fromY); c.dur = l.reduced ? 1 : Math.min(Math.max(dist * 1.1, 520), 1150); const mx = (c.fromX + tx) / 2; @@ -784,16 +793,33 @@ function TourOverlay({ c.arrivedAt = now; setArrived(l.nonce); } - } else if (l.action === "hover" && !l.reduced && (box.width > 220 || box.height > 140)) { - // A big panel: the cursor reads it, sweeping a slow figure-eight across it like a person would. - const k = (now - c.arrivedAt) / 1000; - const amp = Math.min(k / 0.9, 1); - c.x = tx + Math.max(box.width / 2 - 40, 0) * 0.78 * amp * Math.sin(k * 0.85); - c.y = ty + Math.max(box.height / 2 - 28, 0) * 0.6 * amp * Math.sin(k * 1.7); - } else { + } else if (l.reduced) { c.x = tx; c.y = ty; + } else { + const k = (now - c.arrivedAt) / 1000; + const amp = Math.min(k / 0.9, 1); // ease into the motion from the landing point + if (box && l.action === "hover" && (box.width > 220 || box.height > 140)) { + // A big panel: the cursor reads it, sweeping a slow figure-eight across it like a person would. + c.x = tx + Math.max(box.width / 2 - 40, 0) * 0.78 * amp * Math.sin(k * 0.85); + c.y = ty + Math.max(box.height / 2 - 28, 0) * 0.6 * amp * Math.sin(k * 1.7); + } else { + // Anything smaller: a restless hand, drifting inside the target on two out-of-step waves. + const ax = Math.min(Math.max(aim.width / 2 - 8, 6), 24) * amp; + const ay = Math.min(Math.max(aim.height / 2 - 6, 5), 12) * amp; + c.x = tx + ax * (0.7 * Math.sin(k * 1.3) + 0.3 * Math.sin(k * 3.1 + 1)); + c.y = ty + ay * (0.7 * Math.sin(k * 1.8 + 0.6) + 0.3 * Math.sin(k * 2.7)); + } } + c.baseX = c.x; + c.baseY = c.y; + } else if (cur && !l.reduced) { + // Waiting for the next page to render: hover in place instead of freezing. + const k = now / 1000; + c.x = c.baseX + 7 * Math.sin(k * 1.9); + c.y = c.baseY + 5 * Math.sin(k * 2.6 + 0.8); + } + if (cur && c.x >= 0) { cur.style.transform = `translate(${c.x}px, ${c.y}px)`; } };