@@ -58,13 +145,13 @@ function ProfilePage() {
Posts Written
- 27
+ {posts.length}
◎
Quests Completed
- 27
+ {completedQuestCount}
◎
@@ -72,13 +159,17 @@ function ProfilePage() {
Most Used Tags
-
- {["food", "study", "reflection"].map((tag) => (
- -
- #{tag}
-
- ))}
-
+ {mostUsedTags.length === 0 ? (
+ No tags yet.
+ ) : (
+
+ {mostUsedTags.map((tag) => (
+ -
+ #{tag}
+
+ ))}
+
+ )}
@@ -88,10 +179,10 @@ function ProfilePage() {
Glow
- 2.00
+ {glow}
-
+
Clarity of thought is increasing.
@@ -99,10 +190,10 @@ function ProfilePage() {
Warmth
- 1.29
+ {warmth}
-
+
Emotional processing requires attention.
@@ -110,10 +201,10 @@ function ProfilePage() {
Weight
- 2.32
+ {weight}
-
+
A heavy accumulation of experiences.
@@ -125,9 +216,9 @@ function ProfilePage() {
diff --git a/src/pages/RegisterPage.css b/src/pages/RegisterPage.css
index 49311b7..4b86467 100644
--- a/src/pages/RegisterPage.css
+++ b/src/pages/RegisterPage.css
@@ -126,6 +126,19 @@
font-weight: 700;
}
+.register-error-message {
+ margin: 0.6rem 0 0;
+ color: #b34135;
+ font-size: 0.82rem;
+ font-weight: 800;
+ line-height: 1.35;
+}
+
+.register-button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
/* responsive */
@media (max-width: 860px) {
.register-page {
diff --git a/src/pages/RegisterPage.jsx b/src/pages/RegisterPage.jsx
index 3b4e945..4e86591 100644
--- a/src/pages/RegisterPage.jsx
+++ b/src/pages/RegisterPage.jsx
@@ -1,6 +1,48 @@
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { registerUser } from "../api/userApi";
import "./RegisterPage.css";
function RegisterPage() {
+ const navigate = useNavigate();
+
+ const [nickname, setNickname] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [errorMessage, setErrorMessage] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+
+ async function handleSubmit(event) {
+ event.preventDefault();
+
+ if (submitting) {
+ return;
+ }
+
+ setErrorMessage("");
+
+ if (!nickname.trim() || !email.trim() || !password.trim()) {
+ setErrorMessage("Please fill in all fields.");
+ return;
+ }
+
+ setSubmitting(true);
+
+ try {
+ await registerUser({
+ nickname,
+ email,
+ password,
+ });
+
+ navigate("/nest");
+ } catch (error) {
+ setErrorMessage(error.message || "Failed to create account.");
+ setSubmitting(false);
+ }
+ }
+
return (
@@ -20,29 +62,61 @@ function RegisterPage() {
“Write your memories. Care for your egg. Your forgotten self is waiting.”
-
diff --git a/src/pages/ShopPage.css b/src/pages/ShopPage.css
index 63bb3ba..3ec2b80 100644
--- a/src/pages/ShopPage.css
+++ b/src/pages/ShopPage.css
@@ -242,8 +242,12 @@
.shop-item-card {
position: relative;
- min-height: 270px;
+ height: 310px;
padding: 1rem;
+ display: grid;
+ grid-template-rows: 170px minmax(0, 1fr);
+ gap: 1rem;
+ overflow: hidden;
border: 1px solid #d7c5bd;
border-radius: 18px;
background: #fffdf8;
@@ -252,7 +256,6 @@
cursor: pointer;
box-shadow: 0 8px 16px rgba(70, 51, 38, 0.04);
}
-
.shop-item-card:hover {
border-color: #a8877a;
box-shadow: 0 12px 24px rgba(70, 51, 38, 0.1);
@@ -292,11 +295,33 @@
}
.shop-item-image img {
+ display: block;
+ image-rendering: pixelated;
+}
+
+.shop-item-image-background img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.shop-item-image-decoration img {
+ width: 78%;
+ height: 78%;
+ object-fit: contain;
+}
+
+.shop-item-image-music img {
width: 100%;
height: 100%;
object-fit: cover;
+ image-rendering: auto;
}
+.shop-item-image-music span {
+ font-size: 2rem;
+ color: #7a5b50;
+}
.shop-image-placeholder {
width: 52px;
height: 52px;
@@ -305,26 +330,40 @@
}
.shop-item-info {
- min-height: 58px;
- display: flex;
- align-items: flex-end;
- justify-content: space-between;
- gap: 1rem;
- margin-top: 1rem;
+ min-height: 0;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: end;
+ gap: 0.8rem;
}
+.shop-item-info > div {
+ min-width: 0;
+ overflow: hidden;
+}
+
+
.shop-item-info strong {
display: block;
+ overflow: hidden;
color: #2f241f;
font-size: 0.9rem;
+ white-space: nowrap;
+ text-overflow: ellipsis;
}
+
.shop-item-description {
- max-width: 160px;
+ max-width: 100%;
margin: 0.25rem 0 0;
color: #7c7167;
font-size: 0.68rem;
line-height: 1.35;
+
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
}
.owned-label {
@@ -347,6 +386,13 @@
white-space: nowrap;
}
+.owned-label,
+.price-label {
+ align-self: end;
+ justify-self: end;
+ white-space: nowrap;
+}
+
/* Footer */
.shop-footer {
@@ -454,7 +500,8 @@
}
.shop-item-card {
- min-height: 230px;
+ height: 270px;
+ grid-template-rows: 135px minmax(0, 1fr);
}
.shop-item-image {
@@ -523,9 +570,10 @@
}
.shop-item-card {
- min-height: 260px;
+ height: 280px;
padding: 1rem;
border-radius: 14px;
+ grid-template-rows: 125px minmax(0, 1fr);
}
.shop-item-image {
@@ -538,7 +586,6 @@
grid-template-columns: 1fr auto;
align-items: end;
gap: 0.8rem;
- margin-top: 1rem;
}
.shop-item-info strong {
@@ -551,6 +598,11 @@
margin: 0.3rem 0 0;
font-size: 0.72rem;
line-height: 1.35;
+
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
}
.owned-label,
diff --git a/src/pages/ShopPage.jsx b/src/pages/ShopPage.jsx
index 46244d3..5b33c59 100644
--- a/src/pages/ShopPage.jsx
+++ b/src/pages/ShopPage.jsx
@@ -1,6 +1,13 @@
import { useMemo, useState } from "react";
import { useShop } from "../hooks/useShop";
import "./ShopPage.css";
+import {
+ getBackgroundAsset,
+ getCosmeticAsset,
+ getMusicCoverAsset,
+} from "../assets/assetRegistry";
+
+import { Link } from "react-router-dom";
const shopCategories = [
{
@@ -20,6 +27,26 @@ const shopCategories = [
},
];
+function getShopItemImage(item) {
+ if (!item?.asset_key) {
+ return item?.asset_url || null;
+ }
+
+ if (item.item_type === "background") {
+ return getBackgroundAsset(item.asset_key);
+ }
+
+ if (item.item_type === "decoration") {
+ return getCosmeticAsset(item.asset_key);
+ }
+
+ if (item.item_type === "music") {
+ return getMusicCoverAsset(item.asset_key);
+ }
+
+ return item.asset_url || null;
+}
+
function ShopPage() {
const [activeCategory, setActiveCategory] = useState("background");
const [selectedItemId, setSelectedItemId] = useState(null);
@@ -50,6 +77,20 @@ function ShopPage() {
return visibleItems[0] ?? null;
}, [activeCategory, selectedItemId, shopItems, visibleItems]);
+ const userWillBalance = Number(user?.will_balance || 0);
+
+ const cannotAffordSelectedItem =
+ selectedItem && userWillBalance < Number(selectedItem.price);
+
+ const shouldShowNotEnoughWill =
+ selectedItem && !selectedItem.owned && cannotAffordSelectedItem;
+
+ const isBuyButtonDisabled =
+ !user ||
+ !selectedItem ||
+ selectedItem.owned ||
+ cannotAffordSelectedItem;
+
function handleCategoryChange(categoryId) {
const firstItem = shopItems.find(
(item) => item.item_type === categoryId && item.is_active
@@ -60,7 +101,7 @@ function ShopPage() {
}
async function handlePurchaseSelectedItem() {
- if (!selectedItem) {
+ if (!selectedItem || selectedItem.owned || cannotAffordSelectedItem) {
return;
}
@@ -74,24 +115,13 @@ function ShopPage() {
return (
-
@@ -127,11 +157,11 @@ function ShopPage() {
type="button"
onClick={() => setSelectedItemId(item.item_id)}
>
-
- {item.asset_url ? (
-

+
+ {getShopItemImage(item) ? (
+
})
) : (
-
▧
+
{item.item_type === "music" ? "♪" : "▧"}
)}
@@ -157,34 +187,27 @@ function ShopPage() {
diff --git a/src/pages/ViewPostPage.jsx b/src/pages/ViewPostPage.jsx
index ead1232..0ecf0ed 100644
--- a/src/pages/ViewPostPage.jsx
+++ b/src/pages/ViewPostPage.jsx
@@ -1,24 +1,48 @@
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
-import { deletePost, getPostById } from "../api/postsApi";
+import { usePosts } from "../hooks/usePosts";
import "./ViewPostPage.css";
+import { Link } from "react-router-dom";
+
function ViewPostPage() {
const { id } = useParams();
const navigate = useNavigate();
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
+ const { getPost, removePost } = usePosts();
useEffect(() => {
+ let ignore = false;
+
async function loadPost() {
- const foundPost = await getPostById(id);
+ setLoading(true);
+
+ try {
+ const foundPost = await getPost(id);
- setPost(foundPost || null);
- setLoading(false);
+ if (!ignore) {
+ setPost(foundPost || null);
+ }
+ } catch (error) {
+ console.warn("Failed to load post:", error);
+
+ if (!ignore) {
+ setPost(null);
+ }
+ } finally {
+ if (!ignore) {
+ setLoading(false);
+ }
+ }
}
loadPost();
- }, [id]);
+
+ return () => {
+ ignore = true;
+ };
+ }, [id, getPost]);
if (loading) {
return (
@@ -32,38 +56,37 @@ function ViewPostPage() {
return (
Post not found.
- ← Back to Archive
+ ← Back to Archive
);
}
- async function handleDelete() {
- /*
- console.log("Delete button clicked");
- console.log("Current post:", post);
- */
- const confirmed = window.confirm("Delete this memory?");
- /*
- console.log("Confirmed:", confirmed);
- */
+ async function handleDeletePost() {
+ if (!post) {
+ return;
+ }
+
+ const confirmed = window.confirm("Delete this post?");
+
if (!confirmed) {
return;
}
- await deletePost(post.post_id);
- /*
- console.log("Deleted post id:", post.post_id);
- */
- navigate("/archive");
+ try {
+ await removePost(post.post_id || post.id);
+ navigate("/archive", { replace: true });
+ } catch (error) {
+ console.warn("Failed to delete post:", error);
+ }
}
return (
@@ -190,7 +253,7 @@ function WritePostPage() {
{quest.status}
-
+ {/* REMOVED: quest completion is automated.
{quest.status === "completed" && (