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:
parent
4674303f3b
commit
b0c4684b4a
4 changed files with 77 additions and 0 deletions
63
src/components/FirstRunChecklist.tsx
Normal file
63
src/components/FirstRunChecklist.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue