Files
HaBit/components/avatar/customize-page-content.tsx
Harkamal Randhawa 3470d444b4 Add customize-screen shop preview and purchase flow.
Locked items can be previewed on the pet from the editor, purchased with points in the preview modal, and opened in the full customize view from the shop.
2026-05-24 05:05:46 -06:00

115 lines
3.6 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import { CharacterCreator } from "@/components/character/character-creator";
import { useToast } from "@/components/ui/toast-provider";
import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
import {
getAvatarCustomization,
saveAvatarCustomization,
type AvatarCustomization,
} from "@/lib/avatar-customization-storage";
import { normalizeRoomBackgroundId } from "@/lib/room-backgrounds";
import type { ShopItemRecord } from "@/lib/shop-catalog";
import { getShopInventory } from "@/lib/shop-storage";
export function CustomizePageContent() {
const { toast } = useToast();
const [customization, setCustomization] = useState<AvatarCustomization | null>(
null,
);
const [avatarName, setAvatarName] = useState("");
const [isSavingAvatar, setIsSavingAvatar] = useState(false);
const [ownedVariantIds, setOwnedVariantIds] = useState<string[]>([]);
const [equippedItems, setEquippedItems] = useState<string[]>([]);
const [shopItems, setShopItems] = useState<ShopItemRecord[]>([]);
const [coins, setCoins] = useState<number | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
const loadCustomization = useCallback(async () => {
try {
setLoadError(null);
const [next, inventory] = await Promise.all([
getAvatarCustomization(),
getShopInventory(),
]);
setCustomization({
...next,
equippedItems: inventory.equippedItems,
roomBackground: normalizeRoomBackgroundId(
inventory.equippedRoomBackground,
),
});
setAvatarName(next.name);
setOwnedVariantIds(inventory.ownedItemIds);
setEquippedItems(inventory.equippedItems);
setShopItems(inventory.items);
setCoins(inventory.coins);
} catch (error) {
setLoadError(
error instanceof Error ? error.message : "Could not load your avatar.",
);
}
}, []);
useEffect(() => {
void loadCustomization();
window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, loadCustomization);
return () => {
window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, loadCustomization);
};
}, [loadCustomization]);
const handleSaveAvatar = async (nextCustomization: AvatarCustomization) => {
setIsSavingAvatar(true);
try {
await saveAvatarCustomization({
...nextCustomization,
name: avatarName.trim() || nextCustomization.name,
});
toast("Avatar updated.", "success");
await loadCustomization();
} catch (error) {
toast(
error instanceof Error ? error.message : "Could not save avatar.",
"error",
);
throw error;
} finally {
setIsSavingAvatar(false);
}
};
if (loadError) {
return <p className="text-xs text-red-500">{loadError}</p>;
}
if (!customization) {
return <p className="text-xs text-muted-foreground">Loading customization...</p>;
}
return (
<CharacterCreator
key={`${customization.variants.head}-${customization.variants.torso}-${customization.colors.skin.l}-${customization.roomBackground}-${avatarName}-${ownedVariantIds.join(",")}`}
initialCustomization={{
...customization,
name: avatarName,
}}
name={avatarName}
onNameChange={setAvatarName}
ownedVariantIds={ownedVariantIds}
shopItems={shopItems}
coins={coins}
equippedItems={equippedItems}
onShopDataChange={loadCustomization}
showNameField
saveLabel={isSavingAvatar ? "Saving..." : "Save changes"}
isSaving={isSavingAvatar}
onSave={handleSaveAvatar}
/>
);
}