92 lines
3.8 KiB
TypeScript
92 lines
3.8 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { X } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
|
import { useTranslation } from '@/contexts/language';
|
|
|
|
const DISMISS_KEY = 'skatespots:first-run-dismissed';
|
|
/** Set once, the first time the tip has actually been shown. */
|
|
const SEEN_KEY = 'skatespots:first-run-seen';
|
|
/** The signup flow stamps this (ms). Only a *recent* signup counts as "first run". */
|
|
const SIGNUP_KEY = 'signup_completed';
|
|
const FRESH_WINDOW_MS = 2 * 24 * 60 * 60 * 1000; // 2 days
|
|
|
|
/**
|
|
* One-shot, frictionless first-run tip.
|
|
*
|
|
* It deliberately does NOT ask a new skater to create a spot, crew or wallet.
|
|
* The map is already populated; the only action is an optional "Near Me" (one
|
|
* location prompt) and a dismiss. Any content creation is left to the user's
|
|
* own intent, not onboarding.
|
|
*/
|
|
export function FirstRunChecklist({ onFindNearby }: { onFindNearby?: () => void }) {
|
|
const { t } = useTranslation();
|
|
const { user } = useCurrentUser();
|
|
const [dismissed, setDismissed] = useLocalStorage<boolean>(DISMISS_KEY, false);
|
|
const [seen, setSeen] = useLocalStorage<boolean>(SEEN_KEY, false);
|
|
|
|
const [fresh] = useState(() => {
|
|
try {
|
|
const at = Number(localStorage.getItem(SIGNUP_KEY) ?? 0);
|
|
return Number.isFinite(at) && at > 0 && Date.now() - at < FRESH_WINDOW_MS;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Capture at mount so marking it seen doesn't hide the tip in this session.
|
|
const seenAtMount = useRef(seen);
|
|
useEffect(() => {
|
|
if (user && fresh && !dismissed && !seenAtMount.current) setSeen(true);
|
|
}, [user, fresh, dismissed, setSeen]);
|
|
|
|
if (!user || dismissed || !fresh || seenAtMount.current) return null;
|
|
|
|
return (
|
|
<div className="pointer-events-auto relative z-10 mx-3 mt-2 rounded-2xl border border-white/12 bg-black/85 p-3 backdrop-blur md:mx-4">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<p className="text-xs font-black uppercase tracking-widest text-white/80">{t('mapUi.firstRunTitle')}</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => setDismissed(true)}
|
|
aria-label={t('mapUi.dismiss')}
|
|
className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full text-white/40 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00] focus-visible:ring-offset-2 focus-visible:ring-offset-black"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
<p className="mt-1 text-xs leading-relaxed text-gray-300">{t('mapUi.firstRunTip')}</p>
|
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
{onFindNearby && (
|
|
<Button
|
|
size="sm"
|
|
onClick={() => {
|
|
onFindNearby();
|
|
setDismissed(true);
|
|
}}
|
|
className="h-8 bg-[#ccff00] font-bold text-black hover:bg-[#ccff00]/90"
|
|
>
|
|
{t('mapPage.nearMe')}
|
|
</Button>
|
|
)}
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => setDismissed(true)}
|
|
className="h-8 border-white/20 font-bold"
|
|
>
|
|
{t('mapUi.gotIt')}
|
|
</Button>
|
|
<Link
|
|
to="/crews"
|
|
onClick={() => setDismissed(true)}
|
|
className="self-center rounded text-xs font-medium text-gray-500 underline underline-offset-2 hover:text-gray-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00] focus-visible:ring-offset-2 focus-visible:ring-offset-black"
|
|
>
|
|
{t('mapUi.firstRunJoinCrew')}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|