avatar to main merge

I have completed the character creation screen on the avatar branch and am preparing to merge it into main.
This commit is contained in:
pricewowen
2026-05-23 23:24:36 -06:00
parent 8957ecb81d
commit 61f98866e8
39 changed files with 2361 additions and 436 deletions

View File

@@ -0,0 +1,94 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { CharacterCreator } from "@/components/character/character-creator";
import { useToast } from "@/components/ui/toast-provider";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
defaultAvatarCustomization,
saveAvatarCustomization,
type AvatarCustomization,
} from "@/lib/avatar-customization-storage";
import { routes } from "@/lib/routes";
export function OnboardingCustomizeForm() {
const router = useRouter();
const { toast } = useToast();
const [avatarName, setAvatarName] = useState(defaultAvatarCustomization.name);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSave = async (customization: AvatarCustomization) => {
setIsSubmitting(true);
try {
await saveAvatarCustomization(
{
...customization,
name: avatarName.trim() || customization.name,
},
{ completeOnboarding: true },
);
toast("Avatar confirmed. Welcome to Habit Pet!", "success");
router.push(routes.avatar);
router.refresh();
} catch (saveError) {
toast(
saveError instanceof Error
? saveError.message
: "Could not save your avatar.",
"error",
);
throw saveError;
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-svh bg-background">
<main className="mx-auto flex min-h-svh max-w-6xl flex-col justify-center px-4 py-10">
<div className="mb-8 space-y-2 text-center">
<p className="text-sm uppercase tracking-[0.2em] text-muted-foreground">
Step 2 of 2
</p>
<h1 className="text-3xl font-semibold tracking-tight">
Create your Habit Pet
</h1>
<p className="mx-auto max-w-2xl text-sm text-muted-foreground">
Customize your pet&apos;s look, give them a name, and confirm before
entering the app.
</p>
</div>
<Card className="border-border/60 shadow-sm">
<CardHeader>
<CardTitle className="text-base">Avatar customization</CardTitle>
<CardDescription>
Pick styles and colors for each layer. You can change these later
from the Pet tab.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<CharacterCreator
initialCustomization={defaultAvatarCustomization}
name={avatarName}
onNameChange={setAvatarName}
showNameField
saveLabel={isSubmitting ? "Saving..." : "Confirm avatar"}
isSaving={isSubmitting}
onSave={handleSave}
/>
</CardContent>
</Card>
</main>
</div>
);
}

View File

@@ -0,0 +1,119 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { CharacterLayerPreview } from "@/components/character/character-layer-preview";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { defaultAvatarCustomization } from "@/lib/avatar-customization-storage";
import {
focusTopicOptions,
saveOnboardingPreferences,
} from "@/lib/profile-preferences-storage";
import { routes } from "@/lib/routes";
export function OnboardingQuizForm() {
const router = useRouter();
const [focusTopic, setFocusTopic] = useState<string>("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleContinue = async () => {
if (!focusTopic) {
return;
}
setIsSubmitting(true);
setError(null);
try {
await saveOnboardingPreferences(focusTopic);
router.push(routes.onboardingCustomize);
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">
<CharacterLayerPreview
colors={defaultAvatarCustomization.colors}
variants={defaultAvatarCustomization.variants}
scale={6}
compact
className="max-w-[12rem]"
/>
<div>
<p className="text-sm uppercase tracking-[0.2em] text-muted-foreground">
Step 1 of 2
</p>
<h1 className="mt-2 text-2xl font-semibold tracking-tight">
Welcome to Habit Pet
</h1>
<p className="mt-2 text-sm text-muted-foreground">
Choose a focus area, then design your pet in the next step.
</p>
</div>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">
What habit do you most want to build first?
</CardTitle>
<CardDescription>
This helps personalize your daily tasks.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{focusTopicOptions.map((option) => {
const selected = focusTopic === option;
return (
<button
key={option}
type="button"
onClick={() => setFocusTopic(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={!focusTopic || isSubmitting}
onClick={() => void handleContinue()}
>
{isSubmitting ? "Saving..." : "Continue to avatar"}
</Button>
</CardContent>
</Card>
</main>
</div>
);
}