diff --git a/components/layout/habit-brand.tsx b/components/layout/habit-brand.tsx index b4840f8..41570aa 100644 --- a/components/layout/habit-brand.tsx +++ b/components/layout/habit-brand.tsx @@ -61,6 +61,7 @@ export function HabitBrand({ "font-pixel select-none uppercase tracking-widest text-primary", size === "hero" ? "text-base sm:text-lg" : "text-[10px]", )} + aria-hidden="true" > HaBit diff --git a/components/pet/pet-name-header.tsx b/components/pet/pet-name-header.tsx new file mode 100644 index 0000000..c5add58 --- /dev/null +++ b/components/pet/pet-name-header.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { Pencil } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +import { Input } from "@/components/ui/input"; +import { + DEFAULT_AVATAR_NAME, + saveAvatarName, +} from "@/lib/avatar-customization-storage"; +import { cn } from "@/lib/utils"; + +type PetNameHeaderProps = { + name: string; + moodLabel: string; + editable?: boolean; + onNameChange?: (name: string) => void; + className?: string; +}; + +export function PetNameHeader({ + name, + moodLabel, + editable = true, + onNameChange, + className, +}: PetNameHeaderProps) { + const [displayName, setDisplayName] = useState(name); + const [isEditing, setIsEditing] = useState(false); + const [draft, setDraft] = useState(name); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const inputRef = useRef(null); + + useEffect(() => { + setDisplayName(name); + if (!isEditing) { + setDraft(name); + } + }, [name, isEditing]); + + useEffect(() => { + if (isEditing) { + inputRef.current?.focus(); + inputRef.current?.select(); + } + }, [isEditing]); + + const startEditing = () => { + if (!editable || isSaving) { + return; + } + + setDraft(displayName); + setError(null); + setIsEditing(true); + }; + + const cancelEditing = () => { + setDraft(displayName); + setError(null); + setIsEditing(false); + }; + + const commitName = async () => { + const trimmed = draft.trim() || DEFAULT_AVATAR_NAME; + + if (trimmed === displayName) { + setIsEditing(false); + return; + } + + if (!editable) { + setIsEditing(false); + return; + } + + setIsSaving(true); + setError(null); + + try { + const saved = await saveAvatarName(trimmed); + setDisplayName(saved); + onNameChange?.(saved); + setIsEditing(false); + } catch (commitError) { + setError( + commitError instanceof Error + ? commitError.message + : "Could not save bit name.", + ); + setDraft(displayName); + } finally { + setIsSaving(false); + } + }; + + return ( +
{ + if (isEditing) { + event.stopPropagation(); + } + }} + > + {isEditing ? ( +
+ setDraft(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void commitName(); + } + + if (event.key === "Escape") { + event.preventDefault(); + cancelEditing(); + } + }} + onBlur={() => { + void commitName(); + }} + /> + {error ?

{error}

: null} +
+ ) : ( +
+

+ {displayName} +

+ {editable ? ( + + ) : null} +
+ )} +

+ {moodLabel} +

+
+ ); +}