import { useState, useEffect, useMemo, useCallback } from "react"; import { Link, useNavigate } from "react-router-dom"; import Layout from "@/components/Layout"; import LoadingScreen from "@/components/LoadingScreen"; import { SkeletonOnboardingStep } from "@/components/Skeleton"; import UserTypeSelection from "@/components/onboarding/UserTypeSelection"; import PersonalInfo from "@/components/onboarding/PersonalInfo"; import Experience from "@/components/onboarding/Experience"; import Interests from "@/components/onboarding/Interests"; import RealmSelection from "@/components/onboarding/RealmSelection"; import CreatorProfile from "@/components/onboarding/CreatorProfile"; import Welcome from "@/components/onboarding/Welcome"; import { useAuth } from "@/contexts/AuthContext"; import { aethexUserService, aethexAchievementService, type AethexUserProfile, type AethexAchievement, } from "@/lib/aethex-database-adapter"; import { aethexToast } from "@/lib/aethex-toast"; export type UserType = "game-developer" | "client" | "member" | "customer"; export interface OnboardingData { userType: UserType | null; personalInfo: { firstName: string; lastName: string; email: string; password?: string; confirmPassword?: string; company?: string; }; experience: { level: string; skills: string[]; previousProjects?: string; }; interests: { primaryGoals: string[]; preferredServices: string[]; }; creatorProfile: { bio?: string; skills: string[]; primaryArm?: string; }; } const initialData: OnboardingData = { userType: null, personalInfo: { firstName: "", lastName: "", email: "", password: "", confirmPassword: "", company: "", }, experience: { level: "", skills: [], previousProjects: "", }, interests: { primaryGoals: [], preferredServices: [], }, creatorProfile: { bio: "", skills: [], primaryArm: "", }, }; export default function Onboarding() { // Helper: link to existing account to avoid accidental new account creation // Show a small banner that sends users to login with a next param back to onboarding const signInExistingHref = "/login?next=/onboarding"; const [currentStep, setCurrentStep] = useState(0); const [data, setData] = useState(initialData); const [isLoading, setIsLoading] = useState(true); const [isTransitioning, setIsTransitioning] = useState(false); const [isFinishing, setIsFinishing] = useState(false); const navigate = useNavigate(); const { user, refreshProfile } = useAuth(); const steps = [ { title: "Choose Your Path", component: UserTypeSelection }, { title: "Personal Information", component: PersonalInfo }, { title: "Experience Level", component: Experience }, { title: "Interests & Goals", component: Interests }, { title: "Choose Your Realm", component: RealmSelection }, { title: "Creator Profile Setup", component: CreatorProfile }, { title: "Welcome to AeThex", component: Welcome }, ]; const ONBOARDING_STORAGE_KEY = "aethex_onboarding_progress_v1"; const [hydrated, setHydrated] = useState(false); const [achievementPreview, setAchievementPreview] = useState(null); const mapProfileToOnboardingData = useCallback( ( profile: AethexUserProfile | null, interests: string[], ): OnboardingData => { const email = profile?.email || user?.email || ""; const fullName = profile?.full_name?.trim() || ""; const nameParts = fullName ? fullName.split(/\s+/).filter(Boolean) : []; const firstName = nameParts.shift() || ""; const lastName = nameParts.join(" "); const normalizedType = (() => { const value = (profile as any)?.user_type; switch (value) { case "game_developer": return "game-developer"; case "client": return "client"; case "community_member": return "member"; case "customer": return "customer"; default: return null; } })(); const storedPreferred = Array.isArray((profile as any)?.preferred_services) && ((profile as any)?.preferred_services as string[]).length > 0 ? ((profile as any)?.preferred_services as string[]) : []; const normalizedInterests = Array.isArray(interests) ? interests : []; const profileSkills = Array.isArray((profile as any)?.skills) && ((profile as any)?.skills as string[]).length > 0 ? ((profile as any)?.skills as string[]) : []; return { userType: normalizedType, personalInfo: { firstName: firstName || (profile?.username ?? email.split("@")[0] ?? ""), lastName, email, company: (profile as any)?.company || "", }, experience: { level: ((profile as any)?.experience_level as string) || "", skills: profileSkills, previousProjects: profile?.bio || "", }, interests: { primaryGoals: normalizedInterests, preferredServices: storedPreferred.length > 0 ? storedPreferred : normalizedInterests, }, }; }, [user?.email], ); useEffect(() => { let active = true; const hydrate = async () => { const achievementsPromise = aethexAchievementService .getAllAchievements() .catch(() => [] as AethexAchievement[]); let nextData: OnboardingData = { ...initialData, personalInfo: { ...initialData.personalInfo, email: user?.email || "", }, }; let nextStep = 0; // Do not restore from localStorage; clear any legacy key if (typeof window !== "undefined") { try { window.localStorage.removeItem(ONBOARDING_STORAGE_KEY); } catch {} } if (user?.id) { try { const [profile, interests] = await Promise.all([ aethexUserService.getCurrentUser(), aethexUserService.getUserInterests(user.id), ]); nextData = mapProfileToOnboardingData(profile, interests || []); } catch (error) { console.warn("Unable to hydrate onboarding profile:", error); } } const achievements = await achievementsPromise; if (!active) return; const welcomeBadge = achievements.find( (achievement) => achievement.id === "ach_welcome" || achievement.name === "Welcome to AeThex", ) || null; setData(nextData); setCurrentStep(nextStep); setAchievementPreview(welcomeBadge); setHydrated(true); setIsLoading(false); }; hydrate(); return () => { active = false; }; }, [user, steps.length, mapProfileToOnboardingData]); useEffect(() => { // Disable local persistence for onboarding (but not while finishing) if (typeof window === "undefined" || isFinishing) return; try { window.localStorage.removeItem(ONBOARDING_STORAGE_KEY); } catch {} }, [hydrated, isFinishing]); const updateData = useCallback((newData: Partial) => { setData((prev) => ({ ...prev, ...newData, personalInfo: { ...prev.personalInfo, ...(newData.personalInfo ?? {}), }, experience: { ...prev.experience, ...(newData.experience ?? {}), }, interests: { ...prev.interests, ...(newData.interests ?? {}), }, creatorProfile: { ...prev.creatorProfile, ...(newData.creatorProfile ?? {}), }, })); }, []); const nextStep = () => { if (currentStep < steps.length - 1) { setIsTransitioning(true); setTimeout(() => { setCurrentStep((prev) => prev + 1); setIsTransitioning(false); }, 150); } }; const prevStep = () => { if (currentStep > 0) { setIsTransitioning(true); setTimeout(() => { setCurrentStep((prev) => prev - 1); setIsTransitioning(false); }, 150); } }; const CurrentStepComponent = steps[currentStep].component; // Precompute decorative particles using useMemo at top-level to avoid hooks in JSX const particles = useMemo(() => { if (typeof window === "undefined") return []; return Array.from({ length: 8 }).map(() => ({ left: `${Math.floor(Math.random() * 100)}%`, top: `${Math.floor(Math.random() * 100)}%`, delay: `${Math.random().toFixed(2)}s`, duration: `${3 + Math.floor(Math.random() * 2)}s`, })); }, []); const finishOnboarding = async () => { if (!user) { navigate("/login"); return; } setIsFinishing(true); try { const userTypeMap: Record = { "game-developer": "game_developer", client: "client", member: "community_member", customer: "customer", }; const normalizedFirst = data.personalInfo.firstName?.trim() || user.email?.split("@")[0] || "user"; const normalizedLast = data.personalInfo.lastName?.trim() || ""; const payload = { username: normalizedFirst.replace(/\s+/g, "_"), full_name: `${normalizedFirst} ${normalizedLast}`.trim(), user_type: (userTypeMap[data.userType || "member"] as any) || "game_developer", experience_level: (data.experience.level as any) || "beginner", bio: data.experience.previousProjects?.trim() || undefined, } as any; // Ensure profile via server (uses service role) const ensureResp = await fetch(`/api/profile/ensure`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: user.id, profile: payload }), }); if (!ensureResp.ok) { const text = await ensureResp.text().catch(() => ""); let parsedError: any; try { parsedError = JSON.parse(text); } catch {} const primaryMessage = parsedError?.error || text || `HTTP ${ensureResp.status}`; try { await aethexUserService.updateProfile(user.id, payload as any); } catch (fallbackError: any) { const fallbackMessage = fallbackError?.message || fallbackError?.toString?.() || ""; const combined = [primaryMessage, fallbackMessage] .filter(Boolean) .join(" | "); throw new Error( combined || "Unable to complete profile setup. Please try again.", ); } } // Fire-and-forget interests via server const interests = Array.from( new Set([ ...(data.interests.primaryGoals || []), ...(data.interests.preferredServices || []), ]), ); // Create creator profile if they provided primary arm const creatorProfilePromise = data.creatorProfile.primaryArm ? fetch(`/api/creators`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: user.id, username: payload.username, bio: data.creatorProfile.bio || null, avatar_url: null, // Can be added later in profile settings experience_level: data.experience.level || "junior", primary_arm: data.creatorProfile.primaryArm, arm_affiliations: [data.creatorProfile.primaryArm], skills: data.creatorProfile.skills || [], is_discoverable: true, }), }) : Promise.resolve(); Promise.allSettled([ interests.length ? fetch(`/api/interests`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: user.id, interests }), }) : Promise.resolve(), aethexAchievementService.checkAndAwardOnboardingAchievement(user.id), creatorProfilePromise, ]).catch(() => undefined); // Mark onboarding complete locally (UI fallback) try { localStorage.setItem("onboarding_complete", "1"); localStorage.removeItem(ONBOARDING_STORAGE_KEY); } catch {} // Refresh profile in background (don't block on this) // If it fails, the dashboard will handle showing stale data temporarily refreshProfile().catch((err) => { console.warn("Profile refresh failed after onboarding:", err); }); // Success toast aethexToast.success({ title: "You're all set!", description: "Profile setup complete. Welcome to your dashboard.", }); // Navigate immediately (don't wait for profile refresh) navigate("/dashboard", { replace: true }); // Ensure we navigate away even if React routing has issues if (typeof window !== "undefined") { setTimeout(() => { if (window.location.pathname.includes("onboarding")) { window.location.href = "/dashboard"; } }, 500); } } catch (e) { function formatError(err: any) { if (!err) return "Unknown error"; if (typeof err === "string") return err; if (err instanceof Error) return err.message + (err.stack ? `\n${err.stack}` : ""); if ((err as any).message) return (err as any).message; try { return JSON.stringify(err); } catch { return String(err); } } const formatted = formatError(e as any); console.error("Finalize onboarding failed:", formatted, e); aethexToast.error({ title: "Onboarding failed", description: formatted || "Please try again", }); } finally { setIsFinishing(false); } }; if (isLoading) { return ( ); } return (
{/* Progress Bar */}

Join AeThex

Step {currentStep + 1} of {steps.length}
Already have an account?{" "} Sign In
{/* Step Indicators */}
{steps.map((_, index) => (
))}
{/* Step Content */}

{steps[currentStep].title}

{isTransitioning ? ( ) : (
{currentStep === steps.length - 1 ? ( ) : steps[currentStep].title === "Choose Your Realm" ? ( updateData({ creatorProfile: { ...data.creatorProfile, primaryArm: realm, }, }) } onNext={nextStep} /> ) : ( )}
)}
{/* Floating particles effect (performance-friendly) */}
{particles.map((p, i) => (
))}
); }