Merge character customization branch and resolve avatar page conflict

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
masongga
2026-05-24 00:17:14 -06:00
60 changed files with 3023 additions and 405 deletions

View File

@@ -1,19 +1,17 @@
import { AvatarStatusCard } from "@/components/avatar/avatar-status-card";
import { DailyQuizLinkButton } from "@/components/daily-quiz/daily-quiz-link-button";
import { HabitTracker } from "@/components/habits/habit-tracker";
import { Suspense } from "react";
import { AvatarPageContent } from "@/components/avatar/avatar-page-content";
import { AppShell } from "@/components/layout/app-shell";
export default function AvatarPage() {
return (
<AppShell
title="Your Habit Pet"
description="Today's tasks are personalized by AI from your quiz, journal, and preferences."
description="Customize your pet, complete AI-personalized daily tasks, and track their mood from your wellness check-in."
>
<div className="flex flex-col gap-6">
<AvatarStatusCard />
<HabitTracker />
<DailyQuizLinkButton />
</div>
<Suspense fallback={<p className="text-sm text-muted-foreground">Loading...</p>}>
<AvatarPageContent />
</Suspense>
</AppShell>
);
}

View File

@@ -0,0 +1,7 @@
import { redirect } from "next/navigation";
import { routes } from "@/lib/routes";
export default function CharacterCreatorPage() {
redirect(`${routes.avatar}?tab=customize`);
}

View File

@@ -3,6 +3,7 @@ import { Geist } from "next/font/google";
import { ThemeProvider } from "next-themes";
import { GlobalBottomNav } from "@/components/layout/global-bottom-nav";
import { ToastProvider } from "@/components/ui/toast-provider";
import "./globals.css";
const defaultUrl = process.env.VERCEL_URL
@@ -35,8 +36,10 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<div className="min-h-svh pb-24">{children}</div>
<GlobalBottomNav />
<ToastProvider>
<div className="min-h-svh pb-24">{children}</div>
<GlobalBottomNav />
</ToastProvider>
</ThemeProvider>
</body>
</html>

View File

@@ -0,0 +1,14 @@
import { Suspense } from "react";
import { OnboardingCustomizeAuthGate } from "@/components/layout/onboarding-customize-auth-gate";
import { OnboardingCustomizeForm } from "@/components/onboarding/onboarding-customize-form";
export default function OnboardingCustomizePage() {
return (
<Suspense fallback={<div className="min-h-svh bg-background" />}>
<OnboardingCustomizeAuthGate>
<OnboardingCustomizeForm />
</OnboardingCustomizeAuthGate>
</Suspense>
);
}

View File

@@ -1,141 +1,14 @@
"use client";
import { Suspense } from "react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { PixelAvatar } from "@/components/avatar/pixel-avatar";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import {
avatarVibeOptions,
focusTopicOptions,
saveOnboardingPreferences,
} from "@/lib/profile-preferences-storage";
import { routes } from "@/lib/routes";
const questions = [
{
id: "focus",
prompt: "What habit do you most want to build first?",
options: focusTopicOptions,
},
{
id: "avatarVibe",
prompt: "Pick a starter vibe for your pet.",
options: avatarVibeOptions,
},
] as const;
import { OnboardingQuizAuthGate } from "@/components/layout/onboarding-quiz-auth-gate";
import { OnboardingQuizForm } from "@/components/onboarding/onboarding-quiz-form";
export default function OnboardingQuizPage() {
const router = useRouter();
const [step, setStep] = useState(0);
const [answers, setAnswers] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const currentQuestion = questions[step];
const isLastStep = step === questions.length - 1;
const handleSelect = (value: string) => {
setAnswers((current) => ({
...current,
[currentQuestion.id]: value,
}));
};
const handleContinue = async () => {
if (!answers[currentQuestion.id]) {
return;
}
if (!isLastStep) {
setStep((current) => current + 1);
return;
}
setIsSubmitting(true);
setError(null);
try {
await saveOnboardingPreferences(answers.focus, answers.avatarVibe);
router.push(routes.avatar);
router.refresh();
} catch (submitError) {
setError(
submitError instanceof Error
? submitError.message
: "Could not save onboarding answers.",
);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-svh bg-background">
<main className="mx-auto flex min-h-svh max-w-lg flex-col justify-center px-5 py-10">
<div className="mb-8 flex flex-col items-center gap-4 text-center">
<PixelAvatar mood="neutral" size="md" />
<div>
<p className="text-sm uppercase tracking-[0.2em] text-muted-foreground">
Step {step + 1} of {questions.length}
</p>
<h1 className="mt-2 text-2xl font-semibold tracking-tight">
Welcome to Habit Pet
</h1>
</div>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">{currentQuestion.prompt}</CardTitle>
<CardDescription>
This helps personalize your pet and starting habits.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{currentQuestion.options.map((option) => {
const selected = answers[currentQuestion.id] === option;
return (
<button
key={option}
type="button"
onClick={() => handleSelect(option)}
className={`w-full rounded-lg border px-4 py-3 text-left text-sm transition-colors ${
selected
? "border-primary bg-primary/10"
: "hover:bg-muted"
}`}
>
<Label className="cursor-pointer">{option}</Label>
</button>
);
})}
{error ? <p className="text-sm text-red-500">{error}</p> : null}
<Button
className="w-full"
disabled={!answers[currentQuestion.id] || isSubmitting}
onClick={handleContinue}
>
{isSubmitting
? "Saving..."
: isLastStep
? "Meet your pet"
: "Continue"}
</Button>
</CardContent>
</Card>
</main>
</div>
<Suspense fallback={<div className="min-h-svh bg-background" />}>
<OnboardingQuizAuthGate>
<OnboardingQuizForm />
</OnboardingQuizAuthGate>
</Suspense>
);
}