feat(frontend): Implement Onboarding Welcome Flow
Some checks failed
Deploy Elmeg / deploy (push) Has been cancelled
Some checks failed
Deploy Elmeg / deploy (push) Has been cancelled
This commit is contained in:
parent
24aec3b9b1
commit
bc12238937
1 changed files with 241 additions and 0 deletions
241
frontend/app/welcome/page.tsx
Normal file
241
frontend/app/welcome/page.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { motion, AnimatePresence } from "framer-motion"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
import { PartyPopper, UserCircle, Settings, CheckCircle2, ArrowRight, ArrowLeft } from "lucide-react"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { getApiUrl } from "@/lib/api-config"
|
||||||
|
|
||||||
|
const STEPS = [
|
||||||
|
{
|
||||||
|
id: "intro",
|
||||||
|
title: "Welcome to Elmeg",
|
||||||
|
description: "Let's get you set up to discover and track your favorite shows.",
|
||||||
|
icon: PartyPopper
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "profile",
|
||||||
|
title: "Create Your Profile",
|
||||||
|
description: "How should we call you in the community?",
|
||||||
|
icon: UserCircle
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "preferences",
|
||||||
|
title: "Customize Experience",
|
||||||
|
description: "Choose how you want to interact with the platform.",
|
||||||
|
icon: Settings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "finish",
|
||||||
|
title: "You're All Set!",
|
||||||
|
description: "Ready to dive in?",
|
||||||
|
icon: CheckCircle2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function WelcomePage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [currentStep, setCurrentStep] = useState(0)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
// Form State
|
||||||
|
const [username, setUsername] = useState("")
|
||||||
|
const [displayName, setDisplayName] = useState("")
|
||||||
|
const [wikiMode, setWikiMode] = useState(false)
|
||||||
|
const [publicProfile, setPublicProfile] = useState(true)
|
||||||
|
|
||||||
|
const handleNext = async () => {
|
||||||
|
if (currentStep === STEPS.length - 1) {
|
||||||
|
router.push("/profile") // Redirect to Dashboard
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation & Saving per step
|
||||||
|
if (currentStep === 1) {
|
||||||
|
if (!username) return alert("Username required")
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token")
|
||||||
|
const res = await fetch(`${getApiUrl()}/users/me`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
display_name: displayName || username
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error("Failed to save profile")
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
alert("Failed to save profile. Username might be taken.")
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
} else if (currentStep === 2) {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token")
|
||||||
|
const res = await fetch(`${getApiUrl()}/users/me/preferences`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
wiki_mode: wikiMode,
|
||||||
|
// If wikiMode is true, hide social stuff
|
||||||
|
show_comments: !wikiMode,
|
||||||
|
show_ratings: !wikiMode
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error("Failed to save preferences")
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentStep(prev => prev + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
setCurrentStep(prev => Math.max(0, prev - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepIcon = STEPS[currentStep].icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50/50 dark:bg-zinc-950 p-4">
|
||||||
|
<Card className="w-full max-w-md shadow-xl border-t-4 border-t-primary">
|
||||||
|
<CardHeader className="text-center pb-2">
|
||||||
|
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4 text-primary">
|
||||||
|
<StepIcon className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-2xl">{STEPS[currentStep].title}</CardTitle>
|
||||||
|
<CardDescription>{STEPS[currentStep].description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="py-6">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={currentStep}
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
{currentStep === 0 && (
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
We're glad you're here. This quick setup will help us personalize your experience.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="username">Username <span className="text-red-500">*</span></Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
placeholder="e.g. phantastic_fan"
|
||||||
|
value={username}
|
||||||
|
onChange={e => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">Unique handle for mentions and profile URL.</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="displayName">Display Name</Label>
|
||||||
|
<Input
|
||||||
|
id="displayName"
|
||||||
|
placeholder="e.g. Phish Fan 99"
|
||||||
|
value={displayName}
|
||||||
|
onChange={e => setDisplayName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between space-x-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="wiki-mode" className="text-base">Wiki Mode</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Hide all social features (comments, ratings, leaderboards). Just data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="wiki-mode"
|
||||||
|
checked={wikiMode}
|
||||||
|
onCheckedChange={setWikiMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between space-x-2 opacity-80 pointer-events-none">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label className="text-base">Public Profile</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Allow others to see your stats and attendence.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={publicProfile} />
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-center text-muted-foreground pt-4">You can change these later in settings.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 3 && (
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<p>Your profile is ready!</p>
|
||||||
|
<div className="p-4 bg-secondary/50 rounded-lg text-sm">
|
||||||
|
<p className="font-semibold">@{username}</p>
|
||||||
|
<p>{wikiMode ? "Wiki Mode: ON" : "Community Mode: ON"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardFooter className="flex justify-between">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleBack}
|
||||||
|
disabled={currentStep === 0 || loading}
|
||||||
|
className={currentStep === 0 ? "invisible" : ""}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" /> Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={handleNext} disabled={loading}>
|
||||||
|
{loading ? "Saving..." : currentStep === STEPS.length - 1 ? "Get Started" : "Next"}
|
||||||
|
{currentStep !== STEPS.length - 1 && <ArrowRight className="w-4 h-4 ml-2" />}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Progress Dots */}
|
||||||
|
<div className="absolute bottom-8 flex gap-2">
|
||||||
|
{STEPS.map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`w-2 h-2 rounded-full transition-colors ${index === currentStep ? "bg-primary" : "bg-primary/20"}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue