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