"use client"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { api } from "@/lib/api"; import Image from "next/image"; import { GradientSpinner } from "@/components/ui/GradientSpinner"; export default function OnboardingPage() { const router = useRouter(); const [step, setStep] = useState(1); const [loading, setLoading] = useState(false); const [initialLoading, setInitialLoading] = useState(true); const [error, setError] = useState(""); const showPasswordMismatch = error === "Passwords don't match"; const showPasswordTooShort = error === "Password must be at least 6 characters"; // Step 1: Account creation const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); // Check if user is already logged in and skip to step 2 useEffect(() => { const checkExistingSession = async () => { try { const user = await api.getCurrentUser(); if (user && !user.onboardingComplete) { // User exists but hasn't completed onboarding - skip to step 2 setStep(2); } } catch (error) { // Not logged in, stay on step 1 } finally { setInitialLoading(false); } }; checkExistingSession(); }, []); // Step 2: Integrations const [lidarr, setLidarr] = useState({ url: "", apiKey: "", enabled: false, }); const [audiobookshelf, setAudiobookshelf] = useState({ url: "", apiKey: "", enabled: false, }); const [soulseek, setSoulseek] = useState({ username: "", password: "", enabled: false, }); // Step 3: Enrichment const [enrichmentEnabled, setEnrichmentEnabled] = useState(true); const handleRegister = async (e: React.FormEvent) => { e.preventDefault(); setError(""); if (password !== confirmPassword) { setError("Passwords don't match"); return; } if (password.length < 6) { setError("Password must be at least 6 characters"); return; } setLoading(true); try { const response = await api.post<{ token: string; user: any }>( "/onboarding/register", { username, password } ); // Store the JWT token for subsequent API calls if (response.token) { api.setToken(response.token); } setStep(2); } catch (err: any) { // Check if user already exists if (err.message?.includes("already taken")) { setError( "Username already taken. If this is you, please refresh and continue where you left off." ); } else { setError( err.response?.data?.error || err.message || "Failed to create account" ); } } finally { setLoading(false); } }; const testConnection = async ( type: "lidarr" | "audiobookshelf" | "soulseek" ) => { setError(""); setLoading(true); try { // Use dedicated test endpoints that actually verify the connection if (type === "lidarr") { if (!lidarr.url || !lidarr.apiKey) { throw new Error("URL and API key are required"); } await api.post("/system-settings/test-lidarr", { url: lidarr.url, apiKey: lidarr.apiKey, }); } else if (type === "audiobookshelf") { if (!audiobookshelf.url || !audiobookshelf.apiKey) { throw new Error("URL and API key are required"); } await api.post("/system-settings/test-audiobookshelf", { url: audiobookshelf.url, apiKey: audiobookshelf.apiKey, }); } else if (type === "soulseek") { if (!soulseek.username || !soulseek.password) { throw new Error("Username and password are required"); } await api.post("/system-settings/test-soulseek", { username: soulseek.username, password: soulseek.password, }); } setError(`${type} connected successfully!`); } catch (err: any) { const errorMessage = err.response?.data?.error || err.response?.data?.details || err.message || `Failed to connect to ${type}`; setError(errorMessage); } finally { setLoading(false); } }; const handleNextStep = async () => { setError(""); setLoading(true); try { if (step === 2) { // Save all integration configs await Promise.all([ api.post("/onboarding/lidarr", lidarr), api.post("/onboarding/audiobookshelf", audiobookshelf), api.post("/onboarding/soulseek", soulseek), ]); setStep(3); } else if (step === 3) { // Save enrichment preference and complete await api.post("/onboarding/enrichment", { enabled: enrichmentEnabled, }); await api.post("/onboarding/complete"); // Redirect to sync page router.push("/sync"); } } catch (err: any) { setError( err.response?.data?.error || "Failed to save configuration" ); } finally { setLoading(false); } }; return (
{/* Dark background (matches login) */}
{/* Show loading spinner while checking session */} {initialLoading ? (

Loading...

) : (
{/* Logo/Brand */}
Lidify

Lidify

Welcome to your personal music streaming platform

{/* Progress Steps */}
{[ { num: 1, label: "Account" }, { num: 2, label: "Integrations" }, { num: 3, label: "Enrichment" }, ].map((s, idx) => (
{s.num}
{s.label}
{idx < 2 && (
)}
))}
{/* Main Content Card */}
{step === 1 && (

Create Your Account

Let's get you set up with your personal music library

setUsername( e.target.value ) } className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-white placeholder:text-white/30 focus:outline-none focus:ring-2 focus:ring-brand/30 focus:border-transparent transition-all " placeholder="Choose a username" required minLength={3} />
setPassword( e.target.value ) } className={`w-full px-4 py-3 bg-white/5 border rounded-lg text-white placeholder:text-white/30 focus:outline-none focus:ring-2 focus:ring-brand/30 focus:border-transparent transition-all ${ showPasswordTooShort ? "border-red-500/50" : "border-white/10" }`} placeholder="At least 6 characters" required minLength={6} />
setConfirmPassword( e.target.value ) } className={`w-full px-4 py-3 bg-white/5 border rounded-lg text-white placeholder:text-white/30 focus:outline-none focus:ring-2 focus:ring-brand/30 focus:border-transparent transition-all ${ showPasswordMismatch ? "border-red-500/50" : "border-white/10" }`} placeholder="Confirm your password" required />
{error && (
{error}
)}
)} {step === 2 && (

Connect Your Services

Optional integrations to enhance your music library

{/* Lidarr */} } enabled={lidarr.enabled} onToggle={() => setLidarr({ ...lidarr, enabled: !lidarr.enabled, }) } url={lidarr.url} apiKey={lidarr.apiKey} onUrlChange={(url) => setLidarr({ ...lidarr, url, }) } onApiKeyChange={(apiKey) => setLidarr({ ...lidarr, apiKey, }) } onTest={() => testConnection("lidarr") } loading={loading} /> {/* Audiobookshelf */} } enabled={audiobookshelf.enabled} onToggle={() => setAudiobookshelf({ ...audiobookshelf, enabled: !audiobookshelf.enabled, }) } url={audiobookshelf.url} apiKey={audiobookshelf.apiKey} onUrlChange={(url) => setAudiobookshelf({ ...audiobookshelf, url, }) } onApiKeyChange={(apiKey) => setAudiobookshelf({ ...audiobookshelf, apiKey, }) } onTest={() => testConnection( "audiobookshelf" ) } loading={loading} /> {/* Soulseek */} setSoulseek({ ...soulseek, enabled: !soulseek.enabled, }) } username={soulseek.username} password={soulseek.password} onUsernameChange={(username) => setSoulseek({ ...soulseek, username, }) } onPasswordChange={(password) => setSoulseek({ ...soulseek, password, }) } onTest={() => testConnection("soulseek") } loading={loading} />
{error && (
{error.includes( "successfully" ) ? "" : ""}

{error}

)}
)} {step === 3 && (

Artist Enrichment

Enhance your library with additional metadata

What is enrichment?

Enrichment fetches additional metadata like artist bios, high-quality images, genres, and relationships from external sources. This powers smart features and provides a richer listening experience.

Better artist matching
Discover Weekly
Similar artists
!
Uses internet data

Enable artist enrichment

Recommended for the best experience

{error && (

{error}

)}
)}
{/* Footer */}

© 2025 Lidify. Your music, your way.

)}
); } interface IntegrationCardProps { title: string; description: string; localPort?: string; icon: React.ReactNode; enabled: boolean; onToggle: () => void; url: string; apiKey?: string; username?: string; password?: string; onUrlChange: (url: string) => void; onApiKeyChange?: (apiKey: string) => void; onUsernameChange?: (username: string) => void; onPasswordChange?: (password: string) => void; onTest: () => void; loading: boolean; useSoulseekCreds?: boolean; } function IntegrationCard({ title, description, localPort, icon, enabled, onToggle, url, apiKey, username, password, onUrlChange, onApiKeyChange, onUsernameChange, onPasswordChange, onTest, loading, useSoulseekCreds = false, }: IntegrationCardProps) { return (
{icon}

{title}

{description}

{enabled && (
onUrlChange(e.target.value)} placeholder={`Server URL (e.g., http://${ localPort || "localhost:PORT" })`} className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white text-sm placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-brand/30 focus:border-transparent transition-all " /> {useSoulseekCreds ? ( <> onUsernameChange?.(e.target.value) } placeholder="Soulseek Username" className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white text-sm placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-brand/30 focus:border-transparent transition-all " /> onPasswordChange?.(e.target.value) } placeholder="Soulseek Password" className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white text-sm placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-brand/30 focus:border-transparent transition-all " />

These are your Soulseek network credentials, not your Slskd login

) : ( onApiKeyChange?.(e.target.value) } placeholder="API Key" className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white text-sm placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-brand/30 focus:border-transparent transition-all " /> )}
)}
); } interface SoulseekCardProps { enabled: boolean; onToggle: () => void; username: string; password: string; onUsernameChange: (username: string) => void; onPasswordChange: (password: string) => void; onTest: () => void; loading: boolean; } function SoulseekCard({ enabled, onToggle, username, password, onUsernameChange, onPasswordChange, onTest, loading, }: SoulseekCardProps) { return (

Soulseek

Peer-to-peer music discovery

{enabled && (
onUsernameChange(e.target.value)} placeholder="Soulseek Username" className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white text-sm placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-brand/30 focus:border-transparent transition-all " /> onPasswordChange(e.target.value)} placeholder="Soulseek Password" className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white text-sm placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-brand/30 focus:border-transparent transition-all " />

Create an account at{" "} slsknet.org

)}
); }