Files
nearle_web_nextjs/src/hooks/useMerchantJourney.ts
2026-07-23 10:26:49 +05:30

190 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useReducedMotion } from "framer-motion";
// The eight beats of a merchant's life on Nearle. The visitor watches the whole
// arc — join → digitize → sell → grow — play out once, then can replay/drive it.
export type MerchantStageId =
| "join"
| "digitize"
| "products"
| "browse"
| "orders"
| "packed"
| "rider"
| "grows";
export type MerchantStep = {
id: MerchantStageId;
num: string;
label: string; // short label for the progress rail
icon: string; // Material Symbol for the rail node
title: string;
desc: string;
};
export const MERCHANT_STEPS: MerchantStep[] = [
{
id: "join",
num: "01",
label: "Join",
icon: "handshake",
title: "Merchant Joins Nearle",
desc: "Your neighbourhood shop signs up in minutes — no tech skills needed.",
},
{
id: "digitize",
num: "02",
label: "Digitize",
icon: "qr_code_2",
title: "Store Digitized",
desc: "A quick QR scan turns your shelves into a searchable digital storefront.",
},
{
id: "products",
num: "03",
label: "Products",
icon: "inventory_2",
title: "Products Added",
desc: "AI reads a shelf photo and builds your live, shoppable catalogue.",
},
{
id: "browse",
num: "04",
label: "Browse",
icon: "search",
title: "Customers Browse",
desc: "Nearby shoppers discover and search your store in natural language.",
},
{
id: "orders",
num: "05",
label: "Orders",
icon: "receipt_long",
title: "Orders Received",
desc: "Orders land directly with you — an instant alert for every sale.",
},
{
id: "packed",
num: "06",
label: "Packed",
icon: "package_2",
title: "Products Packed",
desc: "You pack the order yourself, staying at the centre of every sale.",
},
{
id: "rider",
num: "07",
label: "Delivery",
icon: "two_wheeler",
title: "Nearle Rider Delivers",
desc: "A Nearle rider picks up and delivers to your customers door.",
},
{
id: "grows",
num: "08",
label: "Growth",
icon: "trending_up",
title: "Business Grows",
desc: "Demand insights and repeat orders compound into steady local growth.",
},
];
const AUTOPLAY_MS = 3200; // each stage stays readable for ~3.2s
export type MerchantJourney = {
steps: MerchantStep[];
index: number;
step: MerchantStep;
started: boolean;
autoplaying: boolean;
atEnd: boolean;
reduced: boolean;
start: () => void;
goTo: (i: number) => void;
next: () => void;
prev: () => void;
replay: () => void;
};
// Autoplays through the stages exactly once on scroll-into-view, then rests on
// the final stage. Any manual input (rail node, arrow, swipe) cancels autoplay
// and hands control to the visitor. Reduced motion → manual from the start.
export function useMerchantJourney(): MerchantJourney {
const reduced = useReducedMotion() ?? false;
const [started, setStarted] = useState(false);
const [index, setIndex] = useState(0);
const [autoplaying, setAutoplaying] = useState(false);
const last = MERCHANT_STEPS.length - 1;
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const clear = useCallback(() => {
if (timer.current) clearTimeout(timer.current);
timer.current = null;
}, []);
useEffect(() => clear, [clear]);
// Auto-advance one stage at a time; simply stop scheduling (don't loop) at the
// end so the journey rests on the final stage. No synchronous setState here.
useEffect(() => {
if (!started || reduced || !autoplaying || index >= last) return;
timer.current = setTimeout(() => setIndex((i) => Math.min(i + 1, last)), AUTOPLAY_MS);
return clear;
}, [started, reduced, autoplaying, index, last, clear]);
const start = useCallback(() => {
setStarted((was) => {
if (!was && !reduced) setAutoplaying(true);
return true;
});
}, [reduced]);
const goTo = useCallback(
(i: number) => {
clear();
setAutoplaying(false);
setStarted(true);
setIndex(Math.max(0, Math.min(i, last)));
},
[clear, last]
);
const next = useCallback(() => {
clear();
setAutoplaying(false);
setStarted(true);
setIndex((i) => Math.min(i + 1, last));
}, [clear, last]);
const prev = useCallback(() => {
clear();
setAutoplaying(false);
setStarted(true);
setIndex((i) => Math.max(i - 1, 0));
}, [clear]);
const replay = useCallback(() => {
clear();
setIndex(0);
setStarted(true);
setAutoplaying(!reduced);
}, [clear, reduced]);
return {
steps: MERCHANT_STEPS,
index,
step: MERCHANT_STEPS[index],
started,
autoplaying,
atEnd: index >= last,
reduced,
start,
goTo,
next,
prev,
replay,
};
}