feat(onboarding): first-run checklist with dynamic crew step

A one-shot 'Get started' nudge on the map for freshly signed-up users: Add a spot / crew / wallet. The crew CTA adapts to whether any crew exists yet — 'Join a crew' (/crews) when there are some, 'Start your crew' (/create-crew) when there are none. Dismissal persists; logged-out visitors see the welcome banner instead.
This commit is contained in:
SkateSpots 2026-10-12 15:15:35 +07:00
parent 4674303f3b
commit b0c4684b4a
4 changed files with 77 additions and 0 deletions

View file

@ -0,0 +1,63 @@
import { X } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { useCrews } from '@/hooks/useCrews';
import { useTranslation } from '@/contexts/language';
const DISMISS_KEY = 'skatespots:first-run-dismissed';
/**
* One-shot "get started" nudge shown on the map for freshly signed-up users.
*
* The crew step is dynamic: when the community already has crews the CTA is
* "Join a crew" (→ /crews); when there are none it's "Start your crew"
* (→ /create-crew). Logged-out visitors see the welcome banner instead, and
* dismissal persists (localStorage).
*/
export function FirstRunChecklist() {
const { t } = useTranslation();
const { user } = useCurrentUser();
const [dismissed, setDismissed] = useLocalStorage<boolean>(DISMISS_KEY, false);
const crews = useCrews(1);
if (!user || dismissed) return null;
const hasCrews = (crews.data?.length ?? 0) > 0;
const crewTo = hasCrews ? '/crews' : '/create-crew';
const crewLabel = hasCrews ? t('mapUi.firstRunJoinCrew') : t('mapUi.firstRunStartCrew');
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"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
<div className="mt-2 flex flex-wrap gap-2">
<Link to="/create-spot">
<Button size="sm" className="h-8 bg-[#ccff00] font-bold text-black hover:bg-[#ccff00]/90">
{t('mapUi.firstRunAddSpot')}
</Button>
</Link>
<Link to={crewTo}>
<Button size="sm" variant="outline" className="h-8 border-white/20 font-bold">
{crewLabel}
</Button>
</Link>
<Link to="/wallet">
<Button size="sm" variant="outline" className="h-8 border-white/20 font-bold">
{t('mapUi.firstRunWallet')}
</Button>
</Link>
</div>
</div>
);
}

View file

@ -917,6 +917,11 @@ mapUi: {
// Welcome banner
newHere: 'New here?',
welcomeDesc: 'Discover skate spots, events & the community.',
firstRunTitle: 'Get started',
firstRunAddSpot: 'Add your first spot',
firstRunJoinCrew: 'Join a crew',
firstRunStartCrew: 'Start your crew',
firstRunWallet: 'Set up your wallet',
getOnZapstore: 'Get on Zapstore',
dismiss: 'Dismiss',
// Spot sheet / popups

View file

@ -917,6 +917,11 @@ mapUi: {
// Welcome banner
newHere: '¿Nuevo por aquí?',
welcomeDesc: 'Descubre spots, eventos y la comunidad.',
firstRunTitle: 'Empieza aquí',
firstRunAddSpot: 'Añade tu primer spot',
firstRunJoinCrew: 'Únete a una peña',
firstRunStartCrew: 'Crea tu peña',
firstRunWallet: 'Configura tu monedero',
getOnZapstore: 'Consíguelo en Zapstore',
dismiss: 'Descartar',
// Spot sheet / popups

View file

@ -7,6 +7,7 @@ const SpotMap = lazy(() => import('@/components/SpotMap').then(m => ({ default:
import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard';
import { MapFooterStrip } from '@/components/MapFooterStrip';
import { WeatherLegend } from '@/components/WeatherLegend';
import { FirstRunChecklist } from '@/components/FirstRunChecklist';
import { WeatherToggle } from '@/components/WeatherToggle';
import { RouteDialog } from '@/components/RouteDialog';
import { TrustFilter } from '@/components/TrustFilter';
@ -1118,6 +1119,9 @@ export default function MapPage() {
the toolbar (both in flow) because the toolbar floats over the map:
as a later sibling the banner would sit at the container's top and
the floating toolbar would paint over it. */}
{/* First-run "get started" nudge for freshly signed-up users (hidden for
logged-out visitors, who get the welcome banner). */}
<FirstRunChecklist />
{showWelcomeBanner && (
<div className="relative z-10 bg-gradient-to-r from-[#ff0040]/90 via-black/95 to-[#ccff00]/20 border-b border-[#ff0040]/40 backdrop-blur">
<div className="container px-4 py-2.5 flex items-center gap-3">