190 lines
4.8 KiB
TypeScript
190 lines
4.8 KiB
TypeScript
"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 customer’s 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,
|
||
};
|
||
}
|