fediversion/frontend/components/onboarding/band-onboarding.tsx
fullsizemalt 7b8ba4b54c
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
feat: User Personalization, Playlists, Recommendations, and DSO Importer
2025-12-29 16:28:43 -08:00

224 lines
8.4 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { useAuth } from "@/contexts/auth-context"
import { getApiUrl } from "@/lib/api-config"
interface Vertical {
id: number
name: string
slug: string
description: string | null
}
const StarIcon = ({ filled }: { filled: boolean }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill={filled ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="w-5 h-5"
>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
)
export function BandOnboarding({ onComplete }: { onComplete?: () => void }) {
const [verticals, setVerticals] = useState<Vertical[]>([])
const [loading, setLoading] = useState(true)
const [selectedBands, setSelectedBands] = useState<number[]>([])
const [headliners, setHeadliners] = useState<number[]>([])
const [step, setStep] = useState<"select" | "tier">("select")
const [submitting, setSubmitting] = useState(false)
const { token } = useAuth()
const router = useRouter()
useEffect(() => {
async function fetchVerticals() {
try {
const res = await fetch(`${getApiUrl()}/verticals`)
if (res.ok) {
const data = await res.json()
setVerticals(data)
}
} catch (error) {
console.error("Failed to fetch verticals", error)
} finally {
setLoading(false)
}
}
fetchVerticals()
}, [])
const toggleBand = (id: number) => {
setSelectedBands(prev =>
prev.includes(id) ? prev.filter(b => b !== id) : [...prev, id]
)
// Remove from headliners if deselected
if (headliners.includes(id)) {
setHeadliners(prev => prev.filter(h => h !== id))
}
}
const toggleHeadliner = (id: number) => {
setHeadliners(prev => {
if (prev.includes(id)) {
return prev.filter(h => h !== id)
}
if (prev.length >= 3) {
return prev // Limit 3
}
return [...prev, id]
})
}
const handleContinue = () => {
if (step === "select" && selectedBands.length > 0) {
setStep("tier")
} else {
handleSubmit()
}
}
const handleSubmit = async () => {
setSubmitting(true)
try {
// Strategy:
// 1. Bulk add all as 'standard'
// 2. Update headliners to 'headliner'
const bulkRes = await fetch(`${getApiUrl()}/verticals/preferences/bulk`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({
vertical_ids: selectedBands,
display_mode: "standard"
}),
})
if (!bulkRes.ok) throw new Error("Failed to save preferences")
// Now set tiers
await Promise.all(headliners.map(async (vid) => {
await fetch(`${getApiUrl()}/verticals/preferences/${vid}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({
tier: "headliner",
priority: 100 // High priority
}),
})
}))
if (onComplete) {
onComplete()
} else {
router.push("/")
}
} catch (error) {
console.error("Error saving preferences", error)
} finally {
setSubmitting(false)
}
}
if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-muted-foreground">Loading bands...</div>
</div>
)
}
return (
<div className="max-w-2xl mx-auto space-y-6">
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold">
{step === "select" ? "Pick Your Bands" : "Who are your Headliners?"}
</h1>
<p className="text-muted-foreground">
{step === "select"
? "Select the bands you follow. You can change this anytime."
: "Pick up to 3 favorites to feature on your home page."
}
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-8">
{verticals.map((vertical) => (
<button
key={vertical.id}
onClick={() => step === "select" ? toggleBand(vertical.id) : null}
disabled={step === "tier" && !selectedBands.includes(vertical.id)}
className={`
relative p-4 rounded-xl border-2 text-left transition-all
${step === "select"
? (selectedBands.includes(vertical.id)
? "border-primary bg-primary/10"
: "border-border hover:border-primary/50")
: (selectedBands.includes(vertical.id)
? "border-primary/50 opacity-100"
: "border-border opacity-20 grayscale")
}
${step === "tier" && selectedBands.includes(vertical.id) ? "cursor-default" : ""}
`}
>
<div className="font-bold">{vertical.name}</div>
<div className="text-sm text-muted-foreground">{vertical.description}</div>
{step === "tier" && selectedBands.includes(vertical.id) && (
<div
onClick={(e) => {
e.stopPropagation()
toggleHeadliner(vertical.id)
}}
className={`
absolute top-2 right-2 p-1 rounded-full cursor-pointer transition-colors z-10
${headliners.includes(vertical.id) ? "bg-yellow-500 text-black" : "bg-muted text-muted-foreground hover:bg-muted/80"}
`}
>
<StarIcon filled={headliners.includes(vertical.id)} />
</div>
)}
</button>
))}
</div>
<div className="flex justify-between items-center bg-card p-4 rounded-lg border">
<div className="text-sm font-medium">
{step === "select"
? `${selectedBands.length} selected`
: `${headliners.length}/3 Headliners selected`
}
</div>
<div className="flex gap-2">
{step === "tier" && (
<Button variant="ghost" onClick={() => setStep("select")}>
Back
</Button>
)}
<Button
onClick={handleContinue}
disabled={submitting || (step === "select" && selectedBands.length === 0)}
size="lg"
>
{submitting ? "Saving..." : (step === "select" ? "Next: Choose Headliners" : "Finish")}
</Button>
</div>
</div>
</div>
)
}