From a4e5cbdfc9efb81a568b72f22c4cc594fd8bf2db Mon Sep 17 00:00:00 2001 From: Driedoutjerky Date: Sat, 30 May 2026 15:12:25 +0900 Subject: [PATCH 01/10] feat:eggAPI init --- src/api/eggApi.js | 126 +++++++++++++++++++++++++++++++++ src/hooks/useEgg.js | 65 +++++++++++++++++ src/hooks/usePosts.js | 9 ++- src/pages/EggDashboardPage.jsx | 3 + src/pages/WritePostPage.jsx | 7 +- 5 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 src/api/eggApi.js create mode 100644 src/hooks/useEgg.js diff --git a/src/api/eggApi.js b/src/api/eggApi.js new file mode 100644 index 0000000..4968d6e --- /dev/null +++ b/src/api/eggApi.js @@ -0,0 +1,126 @@ +const EGG_STORAGE_KEY = "memory_egg_egg"; + +const defaultEgg = { + egg_id: 1, + user_id: 1, + stage: 1, + glow: 0, + warmth: 0, + weight: 0, + active_background_id: null, + active_music_id: null, + active_decoration_id: null, + updated_at: new Date().toISOString(), +}; + +function loadEggFromStorage() { + const savedEgg = localStorage.getItem(EGG_STORAGE_KEY); + + if (!savedEgg) { + localStorage.setItem(EGG_STORAGE_KEY, JSON.stringify(defaultEgg)); + return defaultEgg; + } + + const parsedEgg = JSON.parse(savedEgg); + + if (!parsedEgg || typeof parsedEgg !== "object") { + localStorage.setItem(EGG_STORAGE_KEY, JSON.stringify(defaultEgg)); + return defaultEgg; + } + + return parsedEgg; +} + +function saveEggToStorage(egg) { + localStorage.setItem(EGG_STORAGE_KEY, JSON.stringify(egg)); +} + +function clampStat(value) { + return Math.max(0, Math.min(100, value)); +} + +function calculateStage({ glow, warmth, weight }) { + const average = (glow + warmth + weight) / 3; + + if (average >= 80) { + return 4; + } + + if (average >= 55) { + return 3; + } + + if (average >= 30) { + return 2; + } + + return 1; +} + +function calculateStatsFromPosts(posts) { + const tagCounts = posts.reduce( + (counts, post) => { + const tag = post.tag; + + return { + ...counts, + [tag]: (counts[tag] || 0) + 1, + }; + }, + {} + ); + + return { + glow: clampStat((tagCounts.study || 0) * 5), + warmth: clampStat((tagCounts.reflection || 0) * 3), + weight: clampStat((tagCounts.food || 0) * 5), + }; +} + +export async function getEgg() { + return loadEggFromStorage(); +} + +export async function recalculateEggStatsFromPosts(posts) { + const egg = loadEggFromStorage(); + const calculatedStats = calculateStatsFromPosts(posts); + + const updatedEgg = { + ...egg, + glow: calculatedStats.glow, + warmth: calculatedStats.warmth, + weight: calculatedStats.weight, + stage: calculateStage(calculatedStats), + updated_at: new Date().toISOString(), + }; + + saveEggToStorage(updatedEgg); + + return updatedEgg; +} + +export async function equipEggItem({ itemType, itemId }) { + const egg = loadEggFromStorage(); + + const equipFieldByType = { + background: "active_background_id", + music: "active_music_id", + decoration: "active_decoration_id", + }; + + const fieldName = equipFieldByType[itemType]; + + if (!fieldName) { + throw new Error("Invalid egg item type."); + } + + const updatedEgg = { + ...egg, + [fieldName]: itemId, + updated_at: new Date().toISOString(), + }; + + saveEggToStorage(updatedEgg); + + return updatedEgg; +} \ No newline at end of file diff --git a/src/hooks/useEgg.js b/src/hooks/useEgg.js new file mode 100644 index 0000000..ec1f2bf --- /dev/null +++ b/src/hooks/useEgg.js @@ -0,0 +1,65 @@ +import { useCallback, useEffect, useState } from "react"; +import { + equipEggItem, + getEgg, + recalculateEggStatsFromPosts, +} from "../api/eggApi"; + +export function useEgg() { + const [egg, setEgg] = useState(null); + const [loading, setLoading] = useState(true); + + const reloadEgg = useCallback(async () => { + setLoading(true); + + const data = await getEgg(); + + setEgg(data); + setLoading(false); + + return data; + }, []); + + const recalculateFromPosts = useCallback(async (posts) => { + const updatedEgg = await recalculateEggStatsFromPosts(posts); + + setEgg(updatedEgg); + + return updatedEgg; + }, []); + + const equipItem = useCallback(async ({ itemType, itemId }) => { + const updatedEgg = await equipEggItem({ itemType, itemId }); + + setEgg(updatedEgg); + + return updatedEgg; + }, []); + + useEffect(() => { + let ignore = false; + + async function loadInitialEgg() { + const data = await getEgg(); + + if (!ignore) { + setEgg(data); + setLoading(false); + } + } + + loadInitialEgg(); + + return () => { + ignore = true; + }; + }, []); + + return { + egg, + loading, + reloadEgg, + recalculateFromPosts, + equipItem, + }; +} \ No newline at end of file diff --git a/src/hooks/usePosts.js b/src/hooks/usePosts.js index d1f9a1d..38e65d7 100644 --- a/src/hooks/usePosts.js +++ b/src/hooks/usePosts.js @@ -18,10 +18,15 @@ export function usePosts() { const addPost = useCallback(async (postData) => { const newPost = await createPost(postData); + const updatedPosts = await getAllPosts(); - setPosts((prevPosts) => [newPost, ...prevPosts]); + /*setPosts((prevPosts) => [newPost, ...prevPosts]);*/ + setPosts(Array.isArray(updatedPosts) ? updatedPosts : []); - return newPost; + return { + newPost, + updatedPosts: Array.isArray(updatedPosts) ? updatedPosts : [], + }; }, []); const removePost = useCallback(async (postId) => { diff --git a/src/pages/EggDashboardPage.jsx b/src/pages/EggDashboardPage.jsx index fc44595..29f1188 100644 --- a/src/pages/EggDashboardPage.jsx +++ b/src/pages/EggDashboardPage.jsx @@ -6,7 +6,10 @@ import notebookImage from "../assets/notebook.PNG"; import windowFrameImage from "../assets/windowframe.PNG"; import windowBackgroundImage from "../assets/background.png"; +import { useEgg } from "../hooks/useEgg"; + function EggDashboardPage() { + const { egg, loading } = useEgg(); return (
diff --git a/src/pages/WritePostPage.jsx b/src/pages/WritePostPage.jsx index 11ae099..74d1398 100644 --- a/src/pages/WritePostPage.jsx +++ b/src/pages/WritePostPage.jsx @@ -2,10 +2,12 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; /* import { createPost } from "../api/postsApi"; */ import { usePosts } from "../hooks/usePosts"; +import { useEgg } from "../hooks/useEgg"; import "./WritePostPage.css"; function WritePostPage() { const { addPost } = usePosts(); + const { recalculateFromPosts } = useEgg(); const [title, setTitle] = useState(""); const [content, setContent] = useState(""); @@ -27,7 +29,7 @@ function WritePostPage() { return; } - await addPost({ + const { newPost, updatedPosts } = await addPost({ title, content, tag, @@ -35,6 +37,9 @@ function WritePostPage() { visibility, }); + /*await checkPostForQuestCompletion(newPost);*/ + await recalculateFromPosts(updatedPosts); + alert("Post created! Redirecting you to Archive Page"); navigate("/archive"); } From aba39a45931b1abb084c4e9c5c028377a2ce7151 Mon Sep 17 00:00:00 2001 From: Driedoutjerky Date: Mon, 1 Jun 2026 21:07:00 +0900 Subject: [PATCH 02/10] feat: egg hook implemented and replaced direct api import --- src/App.jsx | 3 +- src/pages/EggDashboardPage.css | 169 ++++++++++++++++++++++++++++++--- src/pages/EggDashboardPage.jsx | 111 +++++++++++++++------- 3 files changed, 236 insertions(+), 47 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index bebc5fc..dedaed2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -19,8 +19,9 @@ function App() { return ( - } /> + } /> } /> + } /> } /> } /> diff --git a/src/pages/EggDashboardPage.css b/src/pages/EggDashboardPage.css index 12a810c..1e7934e 100644 --- a/src/pages/EggDashboardPage.css +++ b/src/pages/EggDashboardPage.css @@ -264,6 +264,103 @@ transform: rotate(12deg); transform-origin: bottom center; } + +/* Egg stats */ + +.dashboard-side-panel { + width: 430px; + justify-self: end; + align-self: start; + + display: flex; + flex-direction: column; + gap: 1rem; +} + +.egg-stats-card { + width: 100%; + padding: 1rem 1.1rem; + border: 1px solid #d9cfc1; + border-radius: 14px; + background: #fffdf8; + color: #2f241f; + box-shadow: 0 12px 28px rgba(72, 52, 38, 0.08); +} + +.egg-stats-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.85rem; +} + +.egg-stats-header span { + color: #6c5147; + font-size: 0.68rem; + font-weight: 900; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.egg-stats-header strong { + font-family: var(--font-serif); + color: #4d342c; + font-size: 0.95rem; +} + +.egg-stats-loading { + margin: 0; + color: #786a61; + font-size: 0.78rem; + font-style: italic; +} + +.egg-stat-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.egg-stat-row { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.egg-stat-label { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.egg-stat-label span { + color: #5f5249; + font-size: 0.72rem; + font-weight: 800; +} + +.egg-stat-label strong { + color: #2f241f; + font-size: 0.78rem; +} + +.egg-stat-bar { + height: 8px; + overflow: hidden; + border-radius: 999px; + background: #eee8df; +} + +.egg-stat-bar span { + display: block; + height: 100%; + border-radius: inherit; + background: #7a5f54; + transition: width 180ms ease; +} + /* Notebook */ /* @@ -310,10 +407,8 @@ .dashboard-notebook { position: relative; - width: 430px; + width: 100%; min-height: 560px; - justify-self: end; - align-self: end; color: #fff8ef; } @@ -513,8 +608,12 @@ aspect-ratio: 1.9 / 1; } - .dashboard-notebook { + .dashboard-side-panel { width: 440px; + } + + .dashboard-notebook { + width: 100%; min-height: 580px; } } @@ -578,13 +677,15 @@ max-width: 900px; aspect-ratio: 1.35 / 1; } - - .dashboard-notebook { + .dashboard-side-panel { width: min(100%, 430px); - min-height: auto; justify-self: center; align-self: start; - /*border-radius: 24px;*/ + } + + .dashboard-notebook { + width: 100%; + min-height: auto; } .bookmark { @@ -616,10 +717,13 @@ aspect-ratio: 1.32 / 1; } - .dashboard-notebook { + .dashboard-side-panel { width: 410px; + } + + .dashboard-notebook { + width: 100%; min-height: 500px; - align-self: end; } .quest-paper { @@ -633,6 +737,43 @@ } } +@media (min-width: 681px) and (max-width: 1050px) { + .dashboard-side-panel { + width: min(100%, 900px); + display: grid; + grid-template-columns: 220px minmax(0, 430px); + align-items: start; + justify-content: center; + gap: 1.2rem; + } + + .egg-stats-card { + min-height: 420px; + padding: 1.1rem; + } + + .egg-stats-header { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .egg-stat-list { + gap: 1.15rem; + } + + .egg-stat-row { + gap: 0.45rem; + } + + .egg-stat-bar { + height: 10px; + } + + .dashboard-notebook { + width: 100%; + } +} @media (max-width: 680px) { .dashboard-header { @@ -666,11 +807,13 @@ --scene-drop: 72px; } - .dashboard-notebook { + .dashboard-side-panel { width: min(100%, 390px); + } + + .dashboard-notebook { + width: 100%; min-height: auto; - justify-self: center; - align-self: start; } .notebook-title { diff --git a/src/pages/EggDashboardPage.jsx b/src/pages/EggDashboardPage.jsx index 29f1188..57743ee 100644 --- a/src/pages/EggDashboardPage.jsx +++ b/src/pages/EggDashboardPage.jsx @@ -51,39 +51,84 @@ function EggDashboardPage() { -
From 494d1e37c54042993002b8ae9b223b6bed239ce4 Mon Sep 17 00:00:00 2001 From: Driedoutjerky Date: Mon, 1 Jun 2026 21:19:52 +0900 Subject: [PATCH 03/10] feat: mock api logic for shop/inventory --- src/api/inventoryApi.js | 169 +++++++++++++++++ src/api/shopApi.js | 203 ++++++++++++++++++++ src/pages/InventoryPage.jsx | 4 +- src/pages/ShopPage.jsx | 360 ++++++++---------------------------- 4 files changed, 449 insertions(+), 287 deletions(-) create mode 100644 src/api/inventoryApi.js create mode 100644 src/api/shopApi.js diff --git a/src/api/inventoryApi.js b/src/api/inventoryApi.js new file mode 100644 index 0000000..db47ee2 --- /dev/null +++ b/src/api/inventoryApi.js @@ -0,0 +1,169 @@ +const USER_ITEMS_STORAGE_KEY = "memory_egg_user_items"; +const USER_ID = 1; + +function loadUserItemsFromStorage() { + const savedUserItems = localStorage.getItem(USER_ITEMS_STORAGE_KEY); + + if (!savedUserItems) { + localStorage.setItem(USER_ITEMS_STORAGE_KEY, JSON.stringify([])); + return []; + } + + const parsedUserItems = JSON.parse(savedUserItems); + + if (!Array.isArray(parsedUserItems)) { + localStorage.setItem(USER_ITEMS_STORAGE_KEY, JSON.stringify([])); + return []; + } + + return parsedUserItems; +} + +function saveUserItemsToStorage(userItems) { + localStorage.setItem(USER_ITEMS_STORAGE_KEY, JSON.stringify(userItems)); +} + +function buildInventoryView(userItem, shopItem) { + return { + user_item_id: userItem.user_item_id, + user_id: userItem.user_id, + item_id: userItem.item_id, + quantity: userItem.quantity, + is_equipped: userItem.is_equipped, + purchased_at: userItem.purchased_at, + + name: shopItem.name, + item_type: shopItem.item_type, + description: shopItem.description, + price: shopItem.price, + effect_type: shopItem.effect_type, + effect_value: shopItem.effect_value, + asset_url: shopItem.asset_url, + is_active: shopItem.is_active, + }; +} + +/* exported api logics*/ + +export async function getUserItems() { + return loadUserItemsFromStorage(); +} + +export async function getInventoryItems(shopItems) { + const userItems = loadUserItemsFromStorage(); + + return userItems + .map((userItem) => { + const matchingShopItem = shopItems.find( + (shopItem) => Number(shopItem.item_id) === Number(userItem.item_id) + ); + + if (!matchingShopItem) { + return null; + } + + return buildInventoryView(userItem, matchingShopItem); + }) + .filter(Boolean); +} + +export async function addUserItem(itemId) { + const userItems = loadUserItemsFromStorage(); + + const existingUserItem = userItems.find( + (userItem) => Number(userItem.item_id) === Number(itemId) + ); + + if (existingUserItem) { + const updatedUserItems = userItems.map((userItem) => { + if (Number(userItem.item_id) !== Number(itemId)) { + return userItem; + } + + return { + ...userItem, + quantity: userItem.quantity + 1, + }; + }); + + saveUserItemsToStorage(updatedUserItems); + + return updatedUserItems; + } + + const newUserItem = { + user_item_id: Date.now(), + user_id: USER_ID, + item_id: Number(itemId), + quantity: 1, + is_equipped: false, + purchased_at: new Date().toISOString(), + }; + + const updatedUserItems = [newUserItem, ...userItems]; + + saveUserItemsToStorage(updatedUserItems); + + return updatedUserItems; +} + +export async function equipUserItem(userItemId, shopItems) { + const userItems = loadUserItemsFromStorage(); + + const selectedUserItem = userItems.find( + (userItem) => Number(userItem.user_item_id) === Number(userItemId) + ); + + if (!selectedUserItem) { + throw new Error("Inventory item not found."); + } + + const selectedShopItem = shopItems.find( + (shopItem) => Number(shopItem.item_id) === Number(selectedUserItem.item_id) + ); + + if (!selectedShopItem) { + throw new Error("Shop item data not found."); + } + + const selectedItemType = selectedShopItem.item_type; + + const updatedUserItems = userItems.map((userItem) => { + const relatedShopItem = shopItems.find( + (shopItem) => Number(shopItem.item_id) === Number(userItem.item_id) + ); + + if (!relatedShopItem || relatedShopItem.item_type !== selectedItemType) { + return userItem; + } + + return { + ...userItem, + is_equipped: + Number(userItem.user_item_id) === Number(selectedUserItem.user_item_id), + }; + }); + + saveUserItemsToStorage(updatedUserItems); + + return updatedUserItems; +} + +export async function unequipUserItem(userItemId) { + const userItems = loadUserItemsFromStorage(); + + const updatedUserItems = userItems.map((userItem) => { + if (Number(userItem.user_item_id) !== Number(userItemId)) { + return userItem; + } + + return { + ...userItem, + is_equipped: false, + }; + }); + + saveUserItemsToStorage(updatedUserItems); + + return updatedUserItems; +} \ No newline at end of file diff --git a/src/api/shopApi.js b/src/api/shopApi.js new file mode 100644 index 0000000..8b0f2aa --- /dev/null +++ b/src/api/shopApi.js @@ -0,0 +1,203 @@ +import { + addUserItem, + getInventoryItems, + getUserItems, +} from "./inventoryApi"; +import { spendWill } from "./userApi"; + +const SHOP_ITEMS_STORAGE_KEY = "memory_egg_shop_items"; + +const defaultShopItems = [ + { + item_id: 1, + name: "Starry Night", + item_type: "background", + description: "A quiet night sky for your egg's resting place.", + price: 150, + effect_type: null, + effect_value: null, + asset_url: null, + is_active: true, + }, + { + item_id: 2, + name: "Good Morning", + item_type: "background", + description: "A warm morning background filled with soft light.", + price: 180, + effect_type: null, + effect_value: null, + asset_url: null, + is_active: true, + }, + { + item_id: 3, + name: "Dreamy Cloud", + item_type: "background", + description: "A gentle cloudy scene for slow reflection.", + price: 130, + effect_type: null, + effect_value: null, + asset_url: null, + is_active: true, + }, + { + item_id: 4, + name: "Warm Blanket", + item_type: "decoration", + description: "A soft blanket that gives the egg warmth.", + price: 120, + effect_type: "warmth", + effect_value: "15", + asset_url: null, + is_active: true, + }, + { + item_id: 5, + name: "Tiny Lamp", + item_type: "decoration", + description: "A small lamp that helps the egg glow softly.", + price: 180, + effect_type: "glow", + effect_value: "15", + asset_url: null, + is_active: true, + }, + { + item_id: 6, + name: "Shell Ribbon", + item_type: "decoration", + description: "A decorative ribbon that gives the egg weight.", + price: 220, + effect_type: "weight", + effect_value: "15", + asset_url: null, + is_active: true, + }, + { + item_id: 7, + name: "Rainy Lullaby", + item_type: "music", + description: "A quiet rainy melody for writing memories.", + price: 160, + effect_type: null, + effect_value: null, + asset_url: null, + is_active: true, + }, + { + item_id: 8, + name: "Morning Piano", + item_type: "music", + description: "A calm piano piece for beginning the day.", + price: 190, + effect_type: null, + effect_value: null, + asset_url: null, + is_active: true, + }, + { + item_id: 9, + name: "Soft Static", + item_type: "music", + description: "A soft ambient track for quiet focus.", + price: 100, + effect_type: null, + effect_value: null, + asset_url: null, + is_active: true, + }, +]; + +function loadShopItemsFromStorage() { + const savedShopItems = localStorage.getItem(SHOP_ITEMS_STORAGE_KEY); + + if (!savedShopItems) { + localStorage.setItem( + SHOP_ITEMS_STORAGE_KEY, + JSON.stringify(defaultShopItems) + ); + + return defaultShopItems; + } + + const parsedShopItems = JSON.parse(savedShopItems); + + if (!Array.isArray(parsedShopItems)) { + localStorage.setItem( + SHOP_ITEMS_STORAGE_KEY, + JSON.stringify(defaultShopItems) + ); + + return defaultShopItems; + } + + return parsedShopItems; +} + +function decorateShopItems(shopItems, userItems) { + return shopItems.map((shopItem) => { + const ownedUserItem = userItems.find( + (userItem) => Number(userItem.item_id) === Number(shopItem.item_id) + ); + + return { + ...shopItem, + owned: Boolean(ownedUserItem), + equipped: Boolean(ownedUserItem?.is_equipped), + user_item_id: ownedUserItem?.user_item_id ?? null, + }; + }); +} + +/* exported api logic */ + +export async function getRawShopItems() { + return loadShopItemsFromStorage(); +} + +export async function getShopItems() { + const shopItems = loadShopItemsFromStorage(); + const userItems = await getUserItems(); + + return decorateShopItems(shopItems, userItems); +} + +export async function purchaseShopItem(itemId) { + const shopItems = loadShopItemsFromStorage(); + + const selectedItem = shopItems.find( + (shopItem) => Number(shopItem.item_id) === Number(itemId) + ); + + if (!selectedItem) { + throw new Error("Shop item not found."); + } + + if (!selectedItem.is_active) { + throw new Error("This item is not available."); + } + + const userItems = await getUserItems(); + + const alreadyOwned = userItems.some( + (userItem) => Number(userItem.item_id) === Number(itemId) + ); + + if (alreadyOwned) { + throw new Error("You already own this item."); + } + + const updatedUser = await spendWill(selectedItem.price); + await addUserItem(selectedItem.item_id); + + const updatedShopItems = await getShopItems(); + const updatedInventoryItems = await getInventoryItems(shopItems); + + return { + purchasedItem: selectedItem, + user: updatedUser, + shopItems: updatedShopItems, + inventoryItems: updatedInventoryItems, + }; +} \ No newline at end of file diff --git a/src/pages/InventoryPage.jsx b/src/pages/InventoryPage.jsx index d9e31ce..97a04a8 100644 --- a/src/pages/InventoryPage.jsx +++ b/src/pages/InventoryPage.jsx @@ -3,12 +3,12 @@ import "./InventoryPage.css"; const inventoryCategories = [ { - id: "backgrounds", + id: "background", label: "Backgrounds", icon: "▱", }, { - id: "decorations", + id: "decoration", label: "Decorations", icon: "⚭", }, diff --git a/src/pages/ShopPage.jsx b/src/pages/ShopPage.jsx index 1628931..00fad61 100644 --- a/src/pages/ShopPage.jsx +++ b/src/pages/ShopPage.jsx @@ -1,14 +1,16 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { getShopItems, purchaseShopItem } from "../api/shopApi"; +import { getCurrentUser } from "../api/userApi"; import "./ShopPage.css"; const shopCategories = [ { - id: "backgrounds", + id: "background", label: "Backgrounds", icon: "▱", }, { - id: "decorations", + id: "decoration", label: "Decorations", icon: "⚭", }, @@ -19,295 +21,55 @@ const shopCategories = [ }, ]; -const shopItems = [ - { - item_id: 1, - name: "Starry Night", - item_type: "backgrounds", - description: "A quiet night sky for your egg's resting place.", - price: 0, - effect_type: "glow", - effect_value: 1, - asset_url: null, - is_active: true, - - // Temporary frontend-only mock fields. - owned: true, - equipped: true, - }, - { - item_id: 2, - name: "Good Morning", - item_type: "backgrounds", - description: "A warm morning background filled with soft light.", - price: 450, - effect_type: "warmth", - effect_value: 1, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 3, - name: "Dreamy Cloud", - item_type: "backgrounds", - description: "A gentle cloudy scene for slow reflection.", - price: 300, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 4, - name: "Test Item", - item_type: "backgrounds", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 5, - name: "Test Item", - item_type: "backgrounds", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 6, - name: "Test Item", - item_type: "backgrounds", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 7, - name: "Test Item", - item_type: "backgrounds", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 8, - name: "Test Item", - item_type: "backgrounds", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 9, - name: "Test Item", - item_type: "backgrounds", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 10, - name: "Test Item", - item_type: "backgrounds", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 11, - name: "Test Deco", - item_type: "decorations", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: true, - equipped: false, - }, - { - item_id: 12, - name: "Test Deco", - item_type: "decorations", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 13, - name: "Test Deco", - item_type: "decorations", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 14, - name: "Test Deco", - item_type: "decorations", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 15, - name: "Test Deco", - item_type: "decorations", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 16, - name: "Test Deco", - item_type: "decorations", - description: "Wow! a shop!", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 17, - name: "Test Music", - item_type: "music", - description: "Music is life", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - - owned: false, - equipped: false, - }, - { - item_id: 18, - name: "Invisible music", - item_type: "music", - description: "You shouldn't be able to see me.", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: false, - - owned: false, - equipped: false, - }, - { - item_id: 17, - name: "Invisible deco", - item_type: "decorations", - description: "You shouldn't be able to see me.", - price: 999, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: false, - - owned: false, - equipped: false, - }, -]; - function ShopPage() { - const [activeCategory, setActiveCategory] = useState("backgrounds"); - const [selectedItemId, setSelectedItemId] = useState(1); + const [activeCategory, setActiveCategory] = useState("background"); + const [shopItems, setShopItems] = useState([]); + const [selectedItemId, setSelectedItemId] = useState(null); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(""); + + + useEffect(() => { + async function loadShopPageData() { + setLoading(true); + + const [items, currentUser] = await Promise.all([ + getShopItems(), + getCurrentUser(), + ]); + + setShopItems(Array.isArray(items) ? items : []); + setUser(currentUser); + + const firstVisibleItem = items.find( + (item) => item.item_type === activeCategory && item.is_active + ); + + setSelectedItemId(firstVisibleItem?.item_id ?? null); + setLoading(false); + } + + loadShopPageData(); + }, [activeCategory]); const visibleItems = useMemo(() => { return shopItems.filter( (item) => item.item_type === activeCategory && item.is_active ); - }, [activeCategory]); + }, [activeCategory, shopItems]); const selectedItem = useMemo(() => { - const selected = shopItems.find((item) => item.item_id === selectedItemId); + const selected = shopItems.find( + (item) => Number(item.item_id) === Number(selectedItemId) + ); if (selected && selected.item_type === activeCategory && selected.is_active) { return selected; } return visibleItems[0] ?? null; - }, [activeCategory, selectedItemId, visibleItems]); + }, [activeCategory, selectedItemId, shopItems, visibleItems]); function handleCategoryChange(categoryId) { const firstItem = shopItems.find( @@ -315,16 +77,31 @@ function ShopPage() { ); setActiveCategory(categoryId); + setSelectedItemId(firstItem?.item_id ?? null); + } - if (firstItem) { - setSelectedItemId(firstItem.item_id); - } + async function handlePurchaseSelectedItem() { + if (!selectedItem) { + return; + } + + setErrorMessage(""); + + try { + const result = await purchaseShopItem(selectedItem.item_id); + + setShopItems(result.shopItems); + setUser(result.user); + setSelectedItemId(selectedItem.item_id); + } catch (error) { + setErrorMessage(error.message); } +} return (
-
✧ 1,250 Will
+
✧ {user ? user.will_balance : 0} Will
@@ -364,11 +141,11 @@ function ShopPage() {
{visibleItems.map((item) => ( )} + + {selectedItem?.owned && ( + + )} + + {errorMessage &&

{errorMessage}

}
From 882e87f509badc630d479fd060d18eca6c10da41 Mon Sep 17 00:00:00 2001 From: Driedoutjerky Date: Mon, 1 Jun 2026 21:25:18 +0900 Subject: [PATCH 04/10] feat: jsx refactored to use api logic. --- src/pages/InventoryPage.jsx | 319 +++++++----------------------------- 1 file changed, 63 insertions(+), 256 deletions(-) diff --git a/src/pages/InventoryPage.jsx b/src/pages/InventoryPage.jsx index 97a04a8..d586179 100644 --- a/src/pages/InventoryPage.jsx +++ b/src/pages/InventoryPage.jsx @@ -1,4 +1,10 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { getRawShopItems } from "../api/shopApi"; +import { + equipUserItem, + getInventoryItems, + unequipUserItem, +} from "../api/inventoryApi"; import "./InventoryPage.css"; const inventoryCategories = [ @@ -19,225 +25,34 @@ const inventoryCategories = [ }, ]; -const initialInventoryItems = [ - { - user_item_id: 1, - user_id: 1, - item_id: 1, - quantity: 1, - is_equipped: true, - purchased_at: "2026-05-01", - - name: "Starry Night", - item_type: "backgrounds", - description: "A quiet night sky for your egg's resting place.", - price: 0, - effect_type: "glow", - effect_value: 1, - asset_url: null, - is_active: true, - }, - { - user_item_id: 2, - user_id: 1, - item_id: 2, - quantity: 1, - is_equipped: false, - purchased_at: "2026-05-02", - - name: "Good Morning", - item_type: "backgrounds", - description: "A warm morning background filled with soft light.", - price: 450, - effect_type: "warmth", - effect_value: 1, - asset_url: null, - is_active: true, - }, - { - user_item_id: 3, - user_id: 1, - item_id: 3, - quantity: 1, - is_equipped: false, - purchased_at: "2026-05-03", - - name: "Dreamy Cloud", - item_type: "backgrounds", - description: "A gentle cloudy scene for slow reflection.", - price: 300, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - }, +function InventoryPage() { + const [activeCategory, setActiveCategory] = useState("background"); + const [inventoryItems, setInventoryItems] = useState([]); + const [shopItems, setShopItems] = useState([]); + const [selectedUserItemId, setSelectedUserItemId] = useState(null); + const [loading, setLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(""); - // Extra background test items - ...Array.from({ length: 9 }, (_, index) => ({ - user_item_id: 100 + index, - user_id: 1, - item_id: 100 + index, - quantity: 1, - is_equipped: false, - purchased_at: "2026-05-04", - - name: "Test Backgrounds", - item_type: "backgrounds", - description: "Test background item for scrolling.", - price: 100, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - })), + useEffect(() => { + async function loadInventoryPageData() { + setLoading(true); - { - user_item_id: 4, - user_id: 1, - item_id: 4, - quantity: 1, - is_equipped: true, - purchased_at: "2026-05-05", - - name: "Warm Blanket", - item_type: "decorations", - description: "A soft blanket that makes the egg feel warmer.", - price: 120, - effect_type: "warmth", - effect_value: 1, - asset_url: null, - is_active: true, - }, - { - user_item_id: 5, - user_id: 1, - item_id: 5, - quantity: 1, - is_equipped: false, - purchased_at: "2026-05-05", - - name: "Tiny Lamp", - item_type: "decorations", - description: "A small lamp that helps the egg glow softly.", - price: 180, - effect_type: "glow", - effect_value: 1, - asset_url: null, - is_active: true, - }, - { - user_item_id: 6, - user_id: 1, - item_id: 6, - quantity: 1, - is_equipped: false, - purchased_at: "2026-05-06", - - name: "Shell Ribbon", - item_type: "decorations", - description: "A decorative ribbon for the egg shell.", - price: 220, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - }, + const rawShopItems = await getRawShopItems(); + const items = await getInventoryItems(rawShopItems); - // Extra decoration test items - ...Array.from({ length: 9 }, (_, index) => ({ - user_item_id: 200 + index, - user_id: 1, - item_id: 200 + index, - quantity: 1, - is_equipped: false, - purchased_at: "2026-05-07", - - name: "Test Deco", - item_type: "decorations", - description: "Test decoration item for scrolling.", - price: 80, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - })), + setShopItems(rawShopItems); + setInventoryItems(Array.isArray(items) ? items : []); - { - user_item_id: 7, - user_id: 1, - item_id: 7, - quantity: 1, - is_equipped: true, - purchased_at: "2026-05-08", - - name: "Rainy Lullaby", - item_type: "music", - description: "A quiet rainy melody for writing memories.", - price: 250, - effect_type: "weight", - effect_value: 1, - asset_url: null, - is_active: true, - }, - { - user_item_id: 8, - user_id: 1, - item_id: 8, - quantity: 1, - is_equipped: false, - purchased_at: "2026-05-08", - - name: "Morning Piano", - item_type: "music", - description: "A calm piano piece for beginning the day.", - price: 320, - effect_type: "glow", - effect_value: 1, - asset_url: null, - is_active: true, - }, - { - user_item_id: 9, - user_id: 1, - item_id: 9, - quantity: 1, - is_equipped: false, - purchased_at: "2026-05-09", - - name: "Soft Static", - item_type: "music", - description: "A soft ambient track for quiet focus.", - price: 150, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - }, + const firstVisibleItem = items.find( + (item) => item.item_type === activeCategory && item.is_active + ); - // Extra music test items - ...Array.from({ length: 9 }, (_, index) => ({ - user_item_id: 300 + index, - user_id: 1, - item_id: 300 + index, - quantity: 1, - is_equipped: false, - purchased_at: "2026-05-10", - - name: "Test Music", - item_type: "music", - description: "Test music item for scrolling.", - price: 90, - effect_type: null, - effect_value: null, - asset_url: null, - is_active: true, - })), -]; + setSelectedUserItemId(firstVisibleItem?.user_item_id ?? null); + setLoading(false); + } -function InventoryPage() { - const [activeCategory, setActiveCategory] = useState("backgrounds"); - const [inventoryItems, setInventoryItems] = useState(initialInventoryItems); - const [selectedUserItemId, setSelectedUserItemId] = useState(1); + loadInventoryPageData(); + }, [activeCategory]); const visibleItems = useMemo(() => { return inventoryItems.filter( @@ -247,7 +62,7 @@ function InventoryPage() { const selectedItem = useMemo(() => { const selected = inventoryItems.find( - (item) => item.user_item_id === selectedUserItemId + (item) => Number(item.user_item_id) === Number(selectedUserItemId) ); if (selected && selected.item_type === activeCategory && selected.is_active) { @@ -263,47 +78,35 @@ function InventoryPage() { ); setActiveCategory(categoryId); - - if (firstItem) { - setSelectedUserItemId(firstItem.user_item_id); - } + setSelectedUserItemId(firstItem?.user_item_id ?? null); } - function handleToggleEquip() { + async function handleToggleEquip() { if (!selectedItem) { return; } - setInventoryItems((currentItems) => - currentItems.map((item) => { - if (item.item_type !== selectedItem.item_type) { - return item; - } - - if (item.user_item_id === selectedItem.user_item_id) { - return { - ...item, - is_equipped: !item.is_equipped, - }; - } - - /* - Placeholder behavior: - For now, only one item per category can be equipped at once. - - Later, if decorations can have multiple equipped items, - this logic can become category-specific. - */ - if (!selectedItem.is_equipped) { - return { - ...item, - is_equipped: false, - }; - } - - return item; - }) - ); + setErrorMessage(""); + + try { + const updatedUserItems = selectedItem.is_equipped + ? await unequipUserItem(selectedItem.user_item_id) + : await equipUserItem(selectedItem.user_item_id, shopItems); + + const rawShopItems = await getRawShopItems(); + const updatedInventoryItems = await getInventoryItems(rawShopItems); + + setShopItems(rawShopItems); + setInventoryItems(updatedInventoryItems); + + const stillSelectedItem = updatedInventoryItems.find( + (item) => Number(item.user_item_id) === Number(selectedItem.user_item_id) + ); + + setSelectedUserItemId(stillSelectedItem?.user_item_id ?? null); + } catch (error) { + setErrorMessage(error.message); + } } return ( @@ -354,13 +157,13 @@ function InventoryPage() {
{visibleItems.map((item) => ( + ) : ( + )} + + {errorMessage &&

{errorMessage}

} From 2c6f0a4579e9e37d8100d0a8bf954b95a919019e Mon Sep 17 00:00:00 2001 From: Driedoutjerky Date: Mon, 1 Jun 2026 21:37:31 +0900 Subject: [PATCH 05/10] feat:refactored jsx --- src/App.jsx | 4 +++ src/api/userApi.js | 66 ++++++++++++++++++++++++++++++++++ src/pages/InventoryPage.css | 19 ++++++++++ src/pages/InventoryPage.jsx | 72 +++++++++++++++++-------------------- src/pages/ShopPage.css | 12 +++++++ src/pages/ShopPage.jsx | 29 +++++---------- 6 files changed, 141 insertions(+), 61 deletions(-) create mode 100644 src/api/userApi.js diff --git a/src/App.jsx b/src/App.jsx index dedaed2..aa74504 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -24,6 +24,10 @@ function App() { } /> } /> } /> + } /> + } /> + } /> + ); diff --git a/src/api/userApi.js b/src/api/userApi.js new file mode 100644 index 0000000..425fa56 --- /dev/null +++ b/src/api/userApi.js @@ -0,0 +1,66 @@ +const USER_STORAGE_KEY = "memory_egg_user"; + +const defaultUser = { + user_id: 1, + email: "demo@nacimiento.app", + nickname: "Wanderer", + will_balance: 999, + created_at: new Date().toISOString(), +}; + +function loadUserFromStorage() { + const savedUser = localStorage.getItem(USER_STORAGE_KEY); + + if (!savedUser) { + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(defaultUser)); + return defaultUser; + } + + const parsedUser = JSON.parse(savedUser); + + if (!parsedUser || typeof parsedUser !== "object") { + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(defaultUser)); + return defaultUser; + } + + return parsedUser; +} + +function saveUserToStorage(user) { + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user)); +} + +export async function getCurrentUser() { + return loadUserFromStorage(); +} + +export async function addWill(amount) { + const user = loadUserFromStorage(); + + const updatedUser = { + ...user, + will_balance: user.will_balance + Number(amount), + }; + + saveUserToStorage(updatedUser); + + return updatedUser; +} + +export async function spendWill(amount) { + const user = loadUserFromStorage(); + const numericAmount = Number(amount); + + if (user.will_balance < numericAmount) { + throw new Error("Not enough Will."); + } + + const updatedUser = { + ...user, + will_balance: user.will_balance - numericAmount, + }; + + saveUserToStorage(updatedUser); + + return updatedUser; +} \ No newline at end of file diff --git a/src/pages/InventoryPage.css b/src/pages/InventoryPage.css index 11423ef..d719687 100644 --- a/src/pages/InventoryPage.css +++ b/src/pages/InventoryPage.css @@ -378,6 +378,25 @@ background: #5a2b1e; } +.inventory-empty-message, +.inventory-error-message { + margin: 0; + padding: 0.8rem; + color: #7c7167; + font-size: 0.82rem; + font-style: italic; +} + +.inventory-error-message { + color: #b34135; + font-weight: 800; +} + +.equip-item-button:disabled { + opacity: 0.45; + cursor: not-allowed; +} + /* Responsive */ @media (max-width: 900px) { diff --git a/src/pages/InventoryPage.jsx b/src/pages/InventoryPage.jsx index d586179..695437a 100644 --- a/src/pages/InventoryPage.jsx +++ b/src/pages/InventoryPage.jsx @@ -155,48 +155,40 @@ function InventoryPage() {
- {visibleItems.map((item) => ( -
- - ))} + + {item.name} + + {item.is_equipped && ( + Equipped + )} + + )) + )}
diff --git a/src/pages/ShopPage.css b/src/pages/ShopPage.css index 79af2bb..0e53af3 100644 --- a/src/pages/ShopPage.css +++ b/src/pages/ShopPage.css @@ -369,6 +369,18 @@ background: #60483f; } +.shop-error-message { + margin: 0; + color: #b34135; + font-size: 0.75rem; + font-weight: 800; +} + +.buy-item-button:disabled { + opacity: 0.45; + cursor: not-allowed; +} + /* Responsive */ @media (max-width: 900px) { diff --git a/src/pages/ShopPage.jsx b/src/pages/ShopPage.jsx index 00fad61..bf01461 100644 --- a/src/pages/ShopPage.jsx +++ b/src/pages/ShopPage.jsx @@ -144,33 +144,20 @@ function ShopPage() { className={`shop-item-card ${ Number(selectedItem?.item_id) === Number(item.item_id) ? "selected" : "" }`} - type="button" key={item.item_id} + type="button" onClick={() => setSelectedItemId(item.item_id)} > - {item.equipped && Equipped} -
- {item.asset_url ? ( - - ) : ( - - )} + {item.asset_url ? {item.name} : }
-
-
- {item.name} - {item.description && ( -

{item.description}

- )} -
- - {item.owned ? ( - Owned - ) : ( - {item.price} ✦ - )} + {item.name} + {item.price} Will + +
+ {item.owned && Owned} + {item.equipped && Equipped}
))} From bcd2308476a1d63148ac664a8938eaaeea62bb2a Mon Sep 17 00:00:00 2001 From: Driedoutjerky Date: Mon, 1 Jun 2026 21:51:29 +0900 Subject: [PATCH 06/10] fix:Shop Page UI --- src/pages/ShopPage.css | 59 +++++++++++++++++++--- src/pages/ShopPage.jsx | 108 +++++++++++++++++++++++++---------------- 2 files changed, 118 insertions(+), 49 deletions(-) diff --git a/src/pages/ShopPage.css b/src/pages/ShopPage.css index 0e53af3..63bb3ba 100644 --- a/src/pages/ShopPage.css +++ b/src/pages/ShopPage.css @@ -9,11 +9,12 @@ /* Header */ .shop-header { - height: 42px; + height: 72px; display: flex; align-items: center; justify-content: space-between; - padding: 0 1.8rem; + gap: 1rem; + padding: 0 2rem; background: var(--color-bg-soft); border-bottom: 1px solid var(--color-border); } @@ -90,11 +91,33 @@ transform: translateX(-50%); } +.shop-return-link { + min-height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 1rem; + border: 1px solid #cdbeb4; + border-radius: 999px; + background: #fffdf8; + color: #5d463c; + font-size: 0.78rem; + font-weight: 800; + line-height: 1; + white-space: nowrap; + text-decoration: none; +} + +.shop-return-link:hover { + background: #f3ede5; + color: #4a342b; +} + /* Shop window */ .shop-window { width: min(100% - 3rem, 1120px); - height: calc(100dvh - 42px - 5rem); + height: calc(100dvh - 72px - 5rem); min-height: 560px; margin: 2.5rem auto; display: grid; @@ -381,12 +404,20 @@ cursor: not-allowed; } +.shop-footer-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 1rem; + margin-left: auto; +} + /* Responsive */ @media (max-width: 900px) { .shop-window { width: min(100% - 2rem, 720px); - height: calc(100dvh - 42px - 2rem); + height: calc(100dvh - 72px - 2rem); min-height: 0; margin: 1rem auto; grid-template-rows: 64px minmax(0, 1fr) 86px; @@ -433,8 +464,16 @@ @media (max-width: 560px) { .shop-header { - height: 54px; - padding: 0 1rem; + height: auto; + min-height: 60px; + padding: 0.7rem 1rem; + gap: 0.75rem; + } + + .shop-return-link { + min-height: 30px; + padding: 0 0.75rem; + font-size: 0.7rem; } .will-balance { @@ -445,7 +484,7 @@ .shop-window { width: min(100% - 1rem, 440px); - height: calc(100dvh - 54px - 1rem); + height: calc(100dvh - 60px - 1rem); margin: 0.5rem auto; border-radius: 18px; grid-template-rows: 58px minmax(0, 1fr) 76px; @@ -531,4 +570,10 @@ min-height: 40px; font-size: 0.76rem; } + + .shop-footer-actions { + flex-direction: column; + align-items: flex-end; + gap: 0.35rem; + } } \ No newline at end of file diff --git a/src/pages/ShopPage.jsx b/src/pages/ShopPage.jsx index bf01461..6c00f1a 100644 --- a/src/pages/ShopPage.jsx +++ b/src/pages/ShopPage.jsx @@ -115,8 +115,8 @@ function ShopPage() {

▦ Egg Shop

- - ⊙ + + Return To Egg
@@ -139,28 +139,45 @@ function ShopPage() {
- {visibleItems.map((item) => ( - - ))} + {loading ? ( +

Loading shop...

+ ) : visibleItems.length === 0 ? ( +

No items available.

+ ) : ( + visibleItems.map((item) => ( + + )) + )}
@@ -170,24 +187,31 @@ function ShopPage() { Close - {selectedItem && !selectedItem.owned && ( - - )} - - {selectedItem?.owned && ( - - )} - - {errorMessage &&

{errorMessage}

} +
+ {(errorMessage || + (selectedItem && user && user.will_balance < selectedItem.price)) && ( +

+ {errorMessage || "Not enough Will."} +

+ )} + + {selectedItem && !selectedItem.owned && ( + + )} + + {selectedItem?.owned && ( + + )} +
From c09eb1979f0f3b9d6d92c4d5d3f78a5f65028e5d Mon Sep 17 00:00:00 2001 From: Driedoutjerky Date: Mon, 1 Jun 2026 22:05:36 +0900 Subject: [PATCH 07/10] fix:Inventory Page UI --- src/pages/InventoryPage.css | 47 ++++++++++++++++++++++++++++++------- src/pages/InventoryPage.jsx | 20 ++++++++++------ 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/pages/InventoryPage.css b/src/pages/InventoryPage.css index d719687..cff1278 100644 --- a/src/pages/InventoryPage.css +++ b/src/pages/InventoryPage.css @@ -120,9 +120,10 @@ } .inventory-window-header { + height: 72px; display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; padding: 0 2rem; border-bottom: 1px solid #ddd3c7; } @@ -132,6 +133,7 @@ font-family: var(--font-serif); font-size: 1.9rem; color: #6a5148; + line-height: 1; } .inventory-info-button { @@ -304,7 +306,7 @@ } .inventory-item-info strong { - display: inline-block; + display: block; color: #2f241f; font-size: 0.9rem; } @@ -318,7 +320,7 @@ } .inventory-item-description { - max-width: 170px; + max-width: 160px; margin: 0.25rem 0 0; color: #7c7167; font-size: 0.68rem; @@ -397,6 +399,33 @@ cursor: not-allowed; } +.effect-label { + min-height: 22px; + display: inline-flex; + align-items: center; + padding: 0 0.65rem; + border-radius: 999px; + background: #eadbd2; + color: #6a4c42; + font-size: 0.7rem; + font-weight: 900; + white-space: nowrap; +} + +.inventory-empty-message, +.inventory-error-message { + margin: 0; + padding: 1rem; + color: #7c7167; + font-size: 0.85rem; + font-style: italic; +} + +.inventory-error-message { + color: #b34135; + font-weight: 800; +} + /* Responsive */ @media (max-width: 900px) { @@ -516,8 +545,9 @@ .inventory-item-info { min-height: auto; - display: block; - margin-top: 1rem; + display: grid; + grid-template-columns: 1fr auto; + align-items: end; } .inventory-item-info strong { @@ -533,9 +563,6 @@ .inventory-item-description { max-width: none; - margin: 0.3rem 0 0; - font-size: 0.72rem; - line-height: 1.35; } .inventory-footer { @@ -548,4 +575,8 @@ min-height: 40px; font-size: 0.76rem; } + + .effect-label { + justify-self: end; + } } \ No newline at end of file diff --git a/src/pages/InventoryPage.jsx b/src/pages/InventoryPage.jsx index 695437a..347a5fe 100644 --- a/src/pages/InventoryPage.jsx +++ b/src/pages/InventoryPage.jsx @@ -131,9 +131,6 @@ function InventoryPage() {

▰ Inventory

- - ⊙ -
@@ -181,11 +178,20 @@ function InventoryPage() { )}
- {item.name} + {item.is_equipped && Equipped} + +
+
+ {item.name} +

{item.description}

+
- {item.is_equipped && ( - Equipped - )} + {item.effect_type && item.effect_value && ( + + +{item.effect_value} {item.effect_type} + + )} +
)) )} From 63fecc06fac773d73b0cce9a209b556ef05e3246 Mon Sep 17 00:00:00 2001 From: Driedoutjerky Date: Mon, 1 Jun 2026 22:15:33 +0900 Subject: [PATCH 08/10] refactor: use hooks instead of direct api --- src/hooks/useInventory.js | 106 ++++++++++++++++++++++++++++++++++++ src/hooks/useShop.js | 78 ++++++++++++++++++++++++++ src/pages/InventoryPage.jsx | 58 +++++--------------- src/pages/ShopPage.jsx | 62 ++++++--------------- 4 files changed, 216 insertions(+), 88 deletions(-) create mode 100644 src/hooks/useInventory.js create mode 100644 src/hooks/useShop.js diff --git a/src/hooks/useInventory.js b/src/hooks/useInventory.js new file mode 100644 index 0000000..865f01f --- /dev/null +++ b/src/hooks/useInventory.js @@ -0,0 +1,106 @@ +import { useCallback, useEffect, useState } from "react"; +import { getRawShopItems } from "../api/shopApi"; +import { + equipUserItem, + getInventoryItems, + unequipUserItem, +} from "../api/inventoryApi"; + +export function useInventory() { + const [inventoryItems, setInventoryItems] = useState([]); + const [shopItems, setShopItems] = useState([]); + const [loading, setLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(""); + + const reloadInventory = useCallback(async () => { + setLoading(true); + + const rawShopItems = await getRawShopItems(); + const items = await getInventoryItems(rawShopItems); + + setShopItems(Array.isArray(rawShopItems) ? rawShopItems : []); + setInventoryItems(Array.isArray(items) ? items : []); + setLoading(false); + + return { + shopItems: Array.isArray(rawShopItems) ? rawShopItems : [], + inventoryItems: Array.isArray(items) ? items : [], + }; + }, []); + + const equipItem = useCallback(async (userItemId) => { + setErrorMessage(""); + + try { + const rawShopItems = await getRawShopItems(); + + await equipUserItem(userItemId, rawShopItems); + + const updatedInventoryItems = await getInventoryItems(rawShopItems); + + setShopItems(Array.isArray(rawShopItems) ? rawShopItems : []); + setInventoryItems( + Array.isArray(updatedInventoryItems) ? updatedInventoryItems : [] + ); + + return updatedInventoryItems; + } catch (error) { + setErrorMessage(error.message); + throw error; + } + }, []); + + const unequipItem = useCallback(async (userItemId) => { + setErrorMessage(""); + + try { + await unequipUserItem(userItemId); + + const rawShopItems = await getRawShopItems(); + const updatedInventoryItems = await getInventoryItems(rawShopItems); + + setShopItems(Array.isArray(rawShopItems) ? rawShopItems : []); + setInventoryItems( + Array.isArray(updatedInventoryItems) ? updatedInventoryItems : [] + ); + + return updatedInventoryItems; + } catch (error) { + setErrorMessage(error.message); + throw error; + } + }, []); + + useEffect(() => { + let ignore = false; + + async function loadInitialInventory() { + setLoading(true); + + const rawShopItems = await getRawShopItems(); + const items = await getInventoryItems(rawShopItems); + + if (!ignore) { + setShopItems(Array.isArray(rawShopItems) ? rawShopItems : []); + setInventoryItems(Array.isArray(items) ? items : []); + setLoading(false); + } + } + + loadInitialInventory(); + + return () => { + ignore = true; + }; + }, []); + + return { + inventoryItems, + shopItems, + loading, + errorMessage, + equipItem, + unequipItem, + reloadInventory, + }; +} \ No newline at end of file diff --git a/src/hooks/useShop.js b/src/hooks/useShop.js new file mode 100644 index 0000000..f23c3da --- /dev/null +++ b/src/hooks/useShop.js @@ -0,0 +1,78 @@ +import { useCallback, useEffect, useState } from "react"; +import { getShopItems, purchaseShopItem } from "../api/shopApi"; +import { getCurrentUser } from "../api/userApi"; + +export function useShop() { + const [shopItems, setShopItems] = useState([]); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(""); + + const reloadShop = useCallback(async () => { + setLoading(true); + + const [items, currentUser] = await Promise.all([ + getShopItems(), + getCurrentUser(), + ]); + + setShopItems(Array.isArray(items) ? items : []); + setUser(currentUser); + setLoading(false); + + return { + shopItems: Array.isArray(items) ? items : [], + user: currentUser, + }; + }, []); + + const purchaseItem = useCallback(async (itemId) => { + setErrorMessage(""); + + try { + const result = await purchaseShopItem(itemId); + + setShopItems(Array.isArray(result.shopItems) ? result.shopItems : []); + setUser(result.user); + + return result; + } catch (error) { + setErrorMessage(error.message); + throw error; + } + }, []); + + useEffect(() => { + let ignore = false; + + async function loadInitialShop() { + setLoading(true); + + const [items, currentUser] = await Promise.all([ + getShopItems(), + getCurrentUser(), + ]); + + if (!ignore) { + setShopItems(Array.isArray(items) ? items : []); + setUser(currentUser); + setLoading(false); + } + } + + loadInitialShop(); + + return () => { + ignore = true; + }; + }, []); + + return { + shopItems, + user, + loading, + errorMessage, + purchaseItem, + reloadShop, + }; +} \ No newline at end of file diff --git a/src/pages/InventoryPage.jsx b/src/pages/InventoryPage.jsx index 347a5fe..9635fb6 100644 --- a/src/pages/InventoryPage.jsx +++ b/src/pages/InventoryPage.jsx @@ -1,10 +1,5 @@ -import { useEffect, useMemo, useState } from "react"; -import { getRawShopItems } from "../api/shopApi"; -import { - equipUserItem, - getInventoryItems, - unequipUserItem, -} from "../api/inventoryApi"; +import { useMemo, useState } from "react"; +import { useInventory } from "../hooks/useInventory"; import "./InventoryPage.css"; const inventoryCategories = [ @@ -27,32 +22,15 @@ const inventoryCategories = [ function InventoryPage() { const [activeCategory, setActiveCategory] = useState("background"); - const [inventoryItems, setInventoryItems] = useState([]); - const [shopItems, setShopItems] = useState([]); const [selectedUserItemId, setSelectedUserItemId] = useState(null); - const [loading, setLoading] = useState(true); - const [errorMessage, setErrorMessage] = useState(""); - useEffect(() => { - async function loadInventoryPageData() { - setLoading(true); - - const rawShopItems = await getRawShopItems(); - const items = await getInventoryItems(rawShopItems); - - setShopItems(rawShopItems); - setInventoryItems(Array.isArray(items) ? items : []); - - const firstVisibleItem = items.find( - (item) => item.item_type === activeCategory && item.is_active - ); - - setSelectedUserItemId(firstVisibleItem?.user_item_id ?? null); - setLoading(false); - } - - loadInventoryPageData(); - }, [activeCategory]); + const { + inventoryItems, + loading, + errorMessage, + equipItem, + unequipItem, + } = useInventory(); const visibleItems = useMemo(() => { return inventoryItems.filter( @@ -86,26 +64,18 @@ function InventoryPage() { return; } - setErrorMessage(""); - try { - const updatedUserItems = selectedItem.is_equipped - ? await unequipUserItem(selectedItem.user_item_id) - : await equipUserItem(selectedItem.user_item_id, shopItems); - - const rawShopItems = await getRawShopItems(); - const updatedInventoryItems = await getInventoryItems(rawShopItems); - - setShopItems(rawShopItems); - setInventoryItems(updatedInventoryItems); + const updatedInventoryItems = selectedItem.is_equipped + ? await unequipItem(selectedItem.user_item_id) + : await equipItem(selectedItem.user_item_id); const stillSelectedItem = updatedInventoryItems.find( (item) => Number(item.user_item_id) === Number(selectedItem.user_item_id) ); setSelectedUserItemId(stillSelectedItem?.user_item_id ?? null); - } catch (error) { - setErrorMessage(error.message); + } catch { + // errorMessage is handled inside useInventory } } diff --git a/src/pages/ShopPage.jsx b/src/pages/ShopPage.jsx index 6c00f1a..46244d3 100644 --- a/src/pages/ShopPage.jsx +++ b/src/pages/ShopPage.jsx @@ -1,6 +1,5 @@ -import { useEffect, useMemo, useState } from "react"; -import { getShopItems, purchaseShopItem } from "../api/shopApi"; -import { getCurrentUser } from "../api/userApi"; +import { useMemo, useState } from "react"; +import { useShop } from "../hooks/useShop"; import "./ShopPage.css"; const shopCategories = [ @@ -23,35 +22,15 @@ const shopCategories = [ function ShopPage() { const [activeCategory, setActiveCategory] = useState("background"); - const [shopItems, setShopItems] = useState([]); const [selectedItemId, setSelectedItemId] = useState(null); - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const [errorMessage, setErrorMessage] = useState(""); - - useEffect(() => { - async function loadShopPageData() { - setLoading(true); - - const [items, currentUser] = await Promise.all([ - getShopItems(), - getCurrentUser(), - ]); - - setShopItems(Array.isArray(items) ? items : []); - setUser(currentUser); - - const firstVisibleItem = items.find( - (item) => item.item_type === activeCategory && item.is_active - ); - - setSelectedItemId(firstVisibleItem?.item_id ?? null); - setLoading(false); - } - - loadShopPageData(); - }, [activeCategory]); + const { + shopItems, + user, + loading, + errorMessage, + purchaseItem, + } = useShop(); const visibleItems = useMemo(() => { return shopItems.filter( @@ -81,22 +60,17 @@ function ShopPage() { } async function handlePurchaseSelectedItem() { - if (!selectedItem) { - return; - } - - setErrorMessage(""); - - try { - const result = await purchaseShopItem(selectedItem.item_id); + if (!selectedItem) { + return; + } - setShopItems(result.shopItems); - setUser(result.user); - setSelectedItemId(selectedItem.item_id); - } catch (error) { - setErrorMessage(error.message); + try { + await purchaseItem(selectedItem.item_id); + setSelectedItemId(selectedItem.item_id); + } catch { + // errorMessage is handled inside useShop + } } -} return (
From 0fe3287fb83e57ebac39d3a91e7c18ea2a339a33 Mon Sep 17 00:00:00 2001 From: Driedoutjerky Date: Mon, 1 Jun 2026 22:32:18 +0900 Subject: [PATCH 09/10] fix:egg stats not impacted by posts anymore --- src/api/eggApi.js | 103 +++++++++++++++--------------------- src/hooks/useEgg.js | 21 ++------ src/pages/WritePostPage.jsx | 3 -- 3 files changed, 48 insertions(+), 79 deletions(-) diff --git a/src/api/eggApi.js b/src/api/eggApi.js index 4968d6e..5dcbf86 100644 --- a/src/api/eggApi.js +++ b/src/api/eggApi.js @@ -28,95 +28,80 @@ function loadEggFromStorage() { return defaultEgg; } - return parsedEgg; + return { + ...defaultEgg, + ...parsedEgg, + stage: 1, + }; } function saveEggToStorage(egg) { localStorage.setItem(EGG_STORAGE_KEY, JSON.stringify(egg)); } -function clampStat(value) { - return Math.max(0, Math.min(100, value)); +function getEmptyStats() { + return { + glow: 0, + warmth: 0, + weight: 0, + }; } -function calculateStage({ glow, warmth, weight }) { - const average = (glow + warmth + weight) / 3; +function getDecorationStats(decorationItem) { + const stats = getEmptyStats(); - if (average >= 80) { - return 4; + if (!decorationItem || decorationItem.item_type !== "decoration") { + return stats; } - if (average >= 55) { - return 3; + if (!decorationItem.effect_type || !decorationItem.effect_value) { + return stats; } - if (average >= 30) { - return 2; + if (!Object.hasOwn(stats, decorationItem.effect_type)) { + return stats; } - return 1; + return { + ...stats, + [decorationItem.effect_type]: Number(decorationItem.effect_value), + }; } -function calculateStatsFromPosts(posts) { - const tagCounts = posts.reduce( - (counts, post) => { - const tag = post.tag; - - return { - ...counts, - [tag]: (counts[tag] || 0) + 1, - }; - }, - {} +function findEquippedItemByType(inventoryItems, itemType) { + return inventoryItems.find( + (item) => item.item_type === itemType && item.is_equipped ); - - return { - glow: clampStat((tagCounts.study || 0) * 5), - warmth: clampStat((tagCounts.reflection || 0) * 3), - weight: clampStat((tagCounts.food || 0) * 5), - }; } export async function getEgg() { return loadEggFromStorage(); } -export async function recalculateEggStatsFromPosts(posts) { - const egg = loadEggFromStorage(); - const calculatedStats = calculateStatsFromPosts(posts); - - const updatedEgg = { - ...egg, - glow: calculatedStats.glow, - warmth: calculatedStats.warmth, - weight: calculatedStats.weight, - stage: calculateStage(calculatedStats), - updated_at: new Date().toISOString(), - }; - - saveEggToStorage(updatedEgg); - - return updatedEgg; -} - -export async function equipEggItem({ itemType, itemId }) { +export async function recalculateEggFromInventory(inventoryItems) { const egg = loadEggFromStorage(); - const equipFieldByType = { - background: "active_background_id", - music: "active_music_id", - decoration: "active_decoration_id", - }; - - const fieldName = equipFieldByType[itemType]; + const equippedBackground = findEquippedItemByType( + inventoryItems, + "background" + ); + const equippedMusic = findEquippedItemByType(inventoryItems, "music"); + const equippedDecoration = findEquippedItemByType( + inventoryItems, + "decoration" + ); - if (!fieldName) { - throw new Error("Invalid egg item type."); - } + const decorationStats = getDecorationStats(equippedDecoration); const updatedEgg = { ...egg, - [fieldName]: itemId, + stage: 1, + glow: decorationStats.glow, + warmth: decorationStats.warmth, + weight: decorationStats.weight, + active_background_id: equippedBackground?.item_id ?? null, + active_music_id: equippedMusic?.item_id ?? null, + active_decoration_id: equippedDecoration?.item_id ?? null, updated_at: new Date().toISOString(), }; diff --git a/src/hooks/useEgg.js b/src/hooks/useEgg.js index ec1f2bf..676d285 100644 --- a/src/hooks/useEgg.js +++ b/src/hooks/useEgg.js @@ -1,9 +1,5 @@ import { useCallback, useEffect, useState } from "react"; -import { - equipEggItem, - getEgg, - recalculateEggStatsFromPosts, -} from "../api/eggApi"; +import { getEgg, recalculateEggFromInventory } from "../api/eggApi"; export function useEgg() { const [egg, setEgg] = useState(null); @@ -20,16 +16,8 @@ export function useEgg() { return data; }, []); - const recalculateFromPosts = useCallback(async (posts) => { - const updatedEgg = await recalculateEggStatsFromPosts(posts); - - setEgg(updatedEgg); - - return updatedEgg; - }, []); - - const equipItem = useCallback(async ({ itemType, itemId }) => { - const updatedEgg = await equipEggItem({ itemType, itemId }); + const recalculateFromInventory = useCallback(async (inventoryItems) => { + const updatedEgg = await recalculateEggFromInventory(inventoryItems); setEgg(updatedEgg); @@ -59,7 +47,6 @@ export function useEgg() { egg, loading, reloadEgg, - recalculateFromPosts, - equipItem, + recalculateFromInventory, }; } \ No newline at end of file diff --git a/src/pages/WritePostPage.jsx b/src/pages/WritePostPage.jsx index 74d1398..d3c0342 100644 --- a/src/pages/WritePostPage.jsx +++ b/src/pages/WritePostPage.jsx @@ -2,12 +2,10 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; /* import { createPost } from "../api/postsApi"; */ import { usePosts } from "../hooks/usePosts"; -import { useEgg } from "../hooks/useEgg"; import "./WritePostPage.css"; function WritePostPage() { const { addPost } = usePosts(); - const { recalculateFromPosts } = useEgg(); const [title, setTitle] = useState(""); const [content, setContent] = useState(""); @@ -38,7 +36,6 @@ function WritePostPage() { }); /*await checkPostForQuestCompletion(newPost);*/ - await recalculateFromPosts(updatedPosts); alert("Post created! Redirecting you to Archive Page"); navigate("/archive"); From 59534ce8b05a1c1d2f4b3abfd56eada4ee0a07d3 Mon Sep 17 00:00:00 2001 From: Driedoutjerky Date: Mon, 1 Jun 2026 22:45:50 +0900 Subject: [PATCH 10/10] feat:egg stats change depending on deco stats --- src/pages/InventoryPage.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/InventoryPage.jsx b/src/pages/InventoryPage.jsx index 9635fb6..b3cde83 100644 --- a/src/pages/InventoryPage.jsx +++ b/src/pages/InventoryPage.jsx @@ -1,5 +1,6 @@ import { useMemo, useState } from "react"; import { useInventory } from "../hooks/useInventory"; +import { useEgg } from "../hooks/useEgg"; import "./InventoryPage.css"; const inventoryCategories = [ @@ -23,6 +24,7 @@ const inventoryCategories = [ function InventoryPage() { const [activeCategory, setActiveCategory] = useState("background"); const [selectedUserItemId, setSelectedUserItemId] = useState(null); + const { recalculateFromInventory } = useEgg(); const { inventoryItems, @@ -69,13 +71,15 @@ function InventoryPage() { ? await unequipItem(selectedItem.user_item_id) : await equipItem(selectedItem.user_item_id); + await recalculateFromInventory(updatedInventoryItems); + const stillSelectedItem = updatedInventoryItems.find( (item) => Number(item.user_item_id) === Number(selectedItem.user_item_id) ); setSelectedUserItemId(stillSelectedItem?.user_item_id ?? null); } catch { - // errorMessage is handled inside useInventory + // errorMessage is handled inside useInventory / useEgg } }