From b0c4684b4a472f86cbef144b976d8710f68fed7a Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Mon, 12 Oct 2026 15:15:35 +0700 Subject: [PATCH] feat(onboarding): first-run checklist with dynamic crew step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/components/FirstRunChecklist.tsx | 63 ++++++++++++++++++++++++++++ src/i18n/en.ts | 5 +++ src/i18n/es.ts | 5 +++ src/pages/MapPage.tsx | 4 ++ 4 files changed, 77 insertions(+) create mode 100644 src/components/FirstRunChecklist.tsx diff --git a/src/components/FirstRunChecklist.tsx b/src/components/FirstRunChecklist.tsx new file mode 100644 index 0000000..29f2edc --- /dev/null +++ b/src/components/FirstRunChecklist.tsx @@ -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(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 ( +
+
+

{t('mapUi.firstRunTitle')}

+ +
+
+ + + + + + + + + +
+
+ ); +} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 2688551..9266f69 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -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 diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 7ba05f5..b77f67b 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -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 diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index 8e9aef3..94e868d 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -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). */} + {showWelcomeBanner && (