diff --git a/components/layout/app-top-bar.tsx b/components/layout/app-top-bar.tsx
index e134cb0..46dac9e 100644
--- a/components/layout/app-top-bar.tsx
+++ b/components/layout/app-top-bar.tsx
@@ -3,7 +3,6 @@
import { usePathname } from "next/navigation";
import { HabitBrand } from "@/components/layout/habit-brand";
-import { HomeLogoLink } from "@/components/layout/home-logo-link";
import { routes } from "@/lib/routes";
import { cn } from "@/lib/utils";
@@ -26,12 +25,8 @@ export function AppTopBar({ className }: AppTopBarProps) {
paddingTop: "env(safe-area-inset-top, 0px)",
}}
>
-
-
-
+
+
);
diff --git a/components/layout/habit-brand.tsx b/components/layout/habit-brand.tsx
index 4ff6d16..2e16300 100644
--- a/components/layout/habit-brand.tsx
+++ b/components/layout/habit-brand.tsx
@@ -1,25 +1,35 @@
"use client";
+import Link from "next/link";
import { useState } from "react";
+import { routes } from "@/lib/routes";
import { cn } from "@/lib/utils";
const HABIT_LOGO_SRC = "/habit-logo.png";
type HabitBrandProps = {
className?: string;
+ href?: string;
+ "aria-current"?: "page" | undefined;
};
-export function HabitBrand({ className }: HabitBrandProps) {
+export function HabitBrand({
+ className,
+ href = routes.avatar,
+ "aria-current": ariaCurrent,
+}: HabitBrandProps) {
const [useText, setUseText] = useState(false);
return (
-
{!useText ? (
// eslint-disable-next-line @next/next/no-img-element
@@ -37,6 +47,6 @@ export function HabitBrand({ className }: HabitBrandProps) {
HaBit
)}
-
+
);
}
diff --git a/components/shop/shop-content.tsx b/components/shop/shop-content.tsx
index b910322..1c50a73 100644
--- a/components/shop/shop-content.tsx
+++ b/components/shop/shop-content.tsx
@@ -10,6 +10,7 @@ import { useToast } from "@/components/ui/toast-provider";
import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
import { getAvatarCustomization } from "@/lib/avatar-customization-storage";
import type { AvatarCustomization } from "@/lib/avatar-customization-storage";
+import { defaultAvatarCustomization } from "@/lib/avatar-customization-storage";
import { DEFAULT_GRAY_COLOR } from "@/lib/character/presets";
import { getRoomBackground, normalizeRoomBackgroundId } from "@/lib/room-backgrounds";
import {
@@ -145,26 +146,24 @@ export function ShopContent() {
const [error, setError] = useState
(null);
const [pendingItemId, setPendingItemId] = useState(null);
const [baseCustomization, setBaseCustomization] =
- useState(null);
+ useState(defaultAvatarCustomization);
const [previewItem, setPreviewItem] = useState(null);
const refreshShop = useCallback(async () => {
+ setError(null);
+
+ let nextEquippedItems: string[] = [];
+ let nextEquippedRoomBackground = "room-day";
+
try {
- setError(null);
- const [inventory, customization] = await Promise.all([
- getShopInventory(),
- getAvatarCustomization(),
- ]);
+ const inventory = await getShopInventory();
setItems(inventory.items);
setOwnedItemIds(inventory.ownedItemIds);
setEquippedItems(inventory.equippedItems);
setEquippedRoomBackground(inventory.equippedRoomBackground);
setCoins(inventory.coins);
- setBaseCustomization({
- ...customization,
- equippedItems: inventory.equippedItems,
- roomBackground: normalizeRoomBackgroundId(inventory.equippedRoomBackground),
- });
+ nextEquippedItems = inventory.equippedItems;
+ nextEquippedRoomBackground = inventory.equippedRoomBackground;
} catch (refreshError) {
setError(
refreshError instanceof Error
@@ -173,6 +172,21 @@ export function ShopContent() {
);
setCoins(0);
}
+
+ try {
+ const customization = await getAvatarCustomization();
+ setBaseCustomization({
+ ...customization,
+ equippedItems: nextEquippedItems,
+ roomBackground: normalizeRoomBackgroundId(nextEquippedRoomBackground),
+ });
+ } catch {
+ setBaseCustomization((current) => ({
+ ...current,
+ equippedItems: nextEquippedItems,
+ roomBackground: normalizeRoomBackgroundId(nextEquippedRoomBackground),
+ }));
+ }
}, []);
useEffect(() => {
@@ -242,12 +256,39 @@ export function ShopContent() {
const itemGridClassName = "grid grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3";
+ const previewOwned = previewItem
+ ? ownedItemIds.includes(previewItem.id)
+ : false;
+ const previewEquipped = previewItem
+ ? previewItem.type === "room"
+ ? equippedRoomBackground === previewItem.id
+ : equippedItems.includes(previewItem.id)
+ : false;
+ const previewCanAfford = previewItem
+ ? coins !== null && coins >= previewItem.price
+ : false;
+
return (
<>
setPreviewItem(null)}
+ onPurchase={() => {
+ if (previewItem) {
+ void handlePurchase(previewItem.id);
+ }
+ }}
+ onEquipToggle={() => {
+ if (previewItem) {
+ void handleEquipToggle(previewItem);
+ }
+ }}
/>
diff --git a/components/shop/shop-item-preview-modal.tsx b/components/shop/shop-item-preview-modal.tsx
index 8fb704b..a1969db 100644
--- a/components/shop/shop-item-preview-modal.tsx
+++ b/components/shop/shop-item-preview-modal.tsx
@@ -16,14 +16,28 @@ import {
type ShopItemPreviewModalProps = {
item: ShopItemRecord | null;
- baseCustomization: AvatarCustomization | null;
+ baseCustomization: AvatarCustomization;
+ coins: number | null;
+ owned: boolean;
+ equipped: boolean;
+ canAfford: boolean;
+ isPending: boolean;
onClose: () => void;
+ onPurchase: () => void;
+ onEquipToggle: () => void;
};
export function ShopItemPreviewModal({
item,
baseCustomization,
+ coins,
+ owned,
+ equipped,
+ canAfford,
+ isPending,
onClose,
+ onPurchase,
+ onEquipToggle,
}: ShopItemPreviewModalProps) {
useEffect(() => {
if (!item) {
@@ -42,7 +56,7 @@ export function ShopItemPreviewModal({
};
}, [item, onClose]);
- if (!item || !baseCustomization) {
+ if (!item) {
return null;
}
@@ -104,8 +118,54 @@ export function ShopItemPreviewModal({
- This preview does not equip or purchase the item.
+ Preview only — nothing is purchased or equipped until you confirm.
+
+
+
+ Price
+ {item.price} points
+
+
+ {owned ? (
+
+ ) : (
+ <>
+ {!canAfford && coins !== null ? (
+
+ You need {item.price - coins} more points to buy this item.
+ Keep completing habits to earn more.
+
+ ) : null}
+
+ >
+ )}
+
diff --git a/lib/avatar-customization-storage.ts b/lib/avatar-customization-storage.ts
index d7b4320..450a90a 100644
--- a/lib/avatar-customization-storage.ts
+++ b/lib/avatar-customization-storage.ts
@@ -1,3 +1,4 @@
+import { DEFAULT_STARTING_COINS } from "@/lib/coins";
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
import {
hexToHsl,
@@ -204,6 +205,7 @@ export async function getAvatarCustomization(): Promise