SkateSpots/src/components/FirstRunChecklist.tsx

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>
);
}