diff --git a/frontend/src/components/Onboarding.tsx b/frontend/src/components/Onboarding.tsx new file mode 100644 index 0000000..36fbff0 --- /dev/null +++ b/frontend/src/components/Onboarding.tsx @@ -0,0 +1,225 @@ +import { useState, useEffect, useCallback } from 'react' +import { useNavigate } from 'react-router-dom' +import { Disc3, ListMusic, Compass, Sparkles, Rocket } from 'lucide-react' + +interface OnboardingProps { + onComplete: () => void +} + +interface Step { + icon: typeof Disc3 + title: string + description: string + details?: string[] + buttons?: { label: string; path: string; primary?: boolean }[] +} + +const steps: Step[] = [ + { + icon: Disc3, + title: 'Welcome to Vynl!', + description: + 'Your AI-powered music discovery companion. Let\u2019s show you around.', + }, + { + icon: ListMusic, + title: 'Start by importing your music', + description: + 'Paste a YouTube Music playlist URL, import from Last.fm, or just type in songs you love.', + details: ['YouTube Music playlists', 'Last.fm library sync', 'Manual song & artist entry'], + }, + { + icon: Compass, + title: 'Discover new music', + description: + 'Choose a discovery mode, set your mood, and let AI find songs you\u2019ll love. Every recommendation links to YouTube Music.', + details: [ + 'Sonic Twin \u2013 your musical doppelg\u00e4nger', + 'Era Bridge \u2013 cross decades', + 'Deep Cuts \u2013 hidden gems', + 'Rising \u2013 emerging artists', + 'Surprise Me \u2013 random delight', + ], + }, + { + icon: Sparkles, + title: 'Explore more', + description: 'Vynl is packed with ways to dig deeper into music.', + details: [ + 'Crate Digger \u2013 swipe through discoveries', + 'Rabbit Hole \u2013 follow connected songs', + 'Playlist Generator \u2013 AI-built playlists', + 'Artist Deep Dive \u2013 click any artist name', + ], + }, + { + icon: Rocket, + title: 'You\u2019re all set!', + description: + 'Import a playlist or just type what you\u2019re in the mood for on the Discover page.', + buttons: [ + { label: 'Import Music', path: '/playlists' }, + { label: 'Start Discovering', path: '/discover', primary: true }, + ], + }, +] + +export default function Onboarding({ onComplete }: OnboardingProps) { + const navigate = useNavigate() + const [currentStep, setCurrentStep] = useState(0) + const [direction, setDirection] = useState<'next' | 'prev'>('next') + const [animating, setAnimating] = useState(false) + const [visible, setVisible] = useState(false) + + const isLastStep = currentStep === steps.length - 1 + + // Fade in on mount + useEffect(() => { + requestAnimationFrame(() => setVisible(true)) + document.body.classList.add('overflow-hidden') + return () => document.body.classList.remove('overflow-hidden') + }, []) + + const goTo = useCallback( + (next: number, dir: 'next' | 'prev') => { + if (animating) return + setDirection(dir) + setAnimating(true) + setTimeout(() => { + setCurrentStep(next) + setAnimating(false) + }, 200) + }, + [animating], + ) + + const handleNext = () => { + if (isLastStep) { + onComplete() + return + } + goTo(currentStep + 1, 'next') + } + + const handleCTA = (path: string) => { + onComplete() + navigate(path) + } + + const step = steps[currentStep] + const Icon = step.icon + + const translateClass = animating + ? direction === 'next' + ? 'opacity-0 translate-x-8' + : 'opacity-0 -translate-x-8' + : 'opacity-100 translate-x-0' + + return ( +
+ {step.description} +
+ + {/* Details list */} + {step.details && ( +