diff --git a/package-lock.json b/package-lock.json index aae39d6..3645bc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-router-dom": "^7.16.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -1068,6 +1069,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2141,6 +2155,44 @@ "react": "^19.2.6" } }, + "node_modules/react-router": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.16.0.tgz", + "integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.16.0.tgz", + "integrity": "sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==", + "license": "MIT", + "dependencies": { + "react-router": "7.16.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/rolldown": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", @@ -2191,6 +2243,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index c67d618..07ff890 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-router-dom": "^7.16.0" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/src/App.jsx b/src/App.jsx index 6d5448c..bebc5fc 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,6 +2,7 @@ //
tests whether centered layout works. //
tests whether reusable panel and vertical spacing classes work. //

tests title style +import { BrowserRouter, Routes, Route } from "react-router-dom"; import OpeningPage from "./pages/OpeningPage.jsx"; import RegisterPage from "./pages/RegisterPage.jsx"; @@ -15,25 +16,16 @@ import ShopPage from "./pages/ShopPage.jsx"; import MemoryArchivePage from "./pages/MemoryArchivePage.jsx"; function App() { - return ; - - /* - return ( -
-
-
-

Nacimiento - My Egg

-

- Frontend setup is ready. -

- -
-
-
+ return ( + + + } /> + } /> + } /> + } /> + + ); - */ } export default App; \ No newline at end of file diff --git a/src/api/mockData.js b/src/api/mockData.js new file mode 100644 index 0000000..348fe52 --- /dev/null +++ b/src/api/mockData.js @@ -0,0 +1,32 @@ +export const mockPosts = [ + { + post_id: 1, + user_id: 1, + title: "The First Crack", + content: + "This morning, I found a tiny crack on the surface of the egg. Is it a sign that my will is starting to take shape?", + image_url: null, + tag: "growth", + visibility: "public", + word_count: 24, + will_reward: 12, + created_at: "2026-05-29", + updated_at: "2026-05-29", + }, + { + post_id: 2, + user_id: 1, + title: "Study Day", + content: + "I studied React today. Hooks are still confusing, but I am starting to understand how state controls the page.", + image_url: null, + tag: "study", + visibility: "private", + word_count: 19, + will_reward: 10, + created_at: "2026-05-29", + updated_at: "2026-05-29", + }, +]; + + diff --git a/src/api/postsApi.js b/src/api/postsApi.js new file mode 100644 index 0000000..0132ca7 --- /dev/null +++ b/src/api/postsApi.js @@ -0,0 +1,126 @@ +const STORAGE_KEY = "memory_egg_posts"; + +const defaultPosts = [ + { + post_id: 1, + user_id: 1, + title: "The First Crack", + content: + "This morning, I found a tiny crack on the surface of the egg. Is it a sign that my will is starting to take shape?", + image_url: null, + tag: "growth", + visibility: "public", + word_count: 24, + will_reward: 12, + created_at: "2026-05-29", + updated_at: "2026-05-29", + }, + { + post_id: 2, + user_id: 1, + title: "Study Day", + content: + "I studied React today. Hooks are still confusing, but I am starting to understand how state controls the page.", + image_url: null, + tag: "study", + visibility: "private", + word_count: 19, + will_reward: 10, + created_at: "2026-05-29", + updated_at: "2026-05-29", + }, +]; + + +/* localStorage for testing WritePostPage and MemoryARchivePage interaction */ + +function loadPostsFromStorage() { + const savedPosts = localStorage.getItem(STORAGE_KEY); + + if (!savedPosts) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultPosts)); + return defaultPosts; + } + + const parsedPosts = JSON.parse(savedPosts); + + if (!Array.isArray(parsedPosts)) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultPosts)); + return defaultPosts; + } + + return parsedPosts; +} + +function savePostsToStorage(posts) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(posts)); +} + + + + +function countWords(text) { + const trimmed = text.trim(); + + if (!trimmed) { + return 0; + } + + return trimmed.split(/\s+/).length; +} + +function calculateWillReward(wordCount) { + return Math.max(1, Math.floor(wordCount / 10)); +} + +export async function getAllPosts() { + return loadPostsFromStorage(); +} + +export async function getPostById(postId) { + const posts = loadPostsFromStorage(); + + return posts.find((post) => post.post_id === Number(postId)); +} + +export async function createPost(postData) { + const posts = loadPostsFromStorage(); + const wordCount = countWords(postData.content); + + const newPost = { + post_id: Date.now(), + user_id: 1, + title: postData.title, + content: postData.content, + image_url: postData.image_url || null, + tag: postData.tag, + visibility: postData.visibility, + word_count: wordCount, + will_reward: calculateWillReward(wordCount), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + + const updatedPosts = [newPost, ...posts]; + + savePostsToStorage(updatedPosts); + + return newPost; +} + +export async function deletePost(postId) { + const posts = loadPostsFromStorage(); + /* + console.log("Before delete:", posts); + console.log("Trying to delete postId:", postId); + */ + const updatedPosts = posts.filter( + (post) => Number(post.post_id) !== Number(postId) + ); + /* + console.log("After delete:", updatedPosts); + */ + savePostsToStorage(updatedPosts); + + return true; +} \ No newline at end of file diff --git a/src/api/questsApi.js b/src/api/questsApi.js new file mode 100644 index 0000000..d361375 --- /dev/null +++ b/src/api/questsApi.js @@ -0,0 +1,154 @@ +import { addWill } from "./userApi"; + +const QUEST_STORAGE_KEY = "memory_egg_quests"; + +const defaultQuests = [ + { + quest_id: 1, + title: "Write a Study Memory", + description: "Write one post with the study tag.", + quest_type: "tag", + required_tag: "study", + required_word_count: 1, + requires_image: false, + reward_will: 10, + status: "assigned", + assigned_date: new Date().toISOString().slice(0, 10), + completed_post_id: null, + completed_at: null, + claimed_at: null, + }, + { + quest_id: 2, + title: "A Small Reflection", + description: "Write a reflection post with at least 20 words.", + quest_type: "word_count", + required_tag: "reflection", + required_word_count: 20, + requires_image: false, + reward_will: 15, + status: "assigned", + assigned_date: new Date().toISOString().slice(0, 10), + completed_post_id: null, + completed_at: null, + claimed_at: null, + }, + { + quest_id: 3, + title: "Capture a Memory", + description: "Write any post with an image attached.", + quest_type: "image", + required_tag: null, + required_word_count: 1, + requires_image: true, + reward_will: 20, + status: "assigned", + assigned_date: new Date().toISOString().slice(0, 10), + completed_post_id: null, + completed_at: null, + claimed_at: null, + }, +]; + +function loadQuestsFromStorage() { + const savedQuests = localStorage.getItem(QUEST_STORAGE_KEY); + + if (!savedQuests) { + localStorage.setItem(QUEST_STORAGE_KEY, JSON.stringify(defaultQuests)); + return defaultQuests; + } + + const parsedQuests = JSON.parse(savedQuests); + + if (!Array.isArray(parsedQuests)) { + localStorage.setItem(QUEST_STORAGE_KEY, JSON.stringify(defaultQuests)); + return defaultQuests; + } + + return parsedQuests; +} + +function saveQuestsToStorage(quests) { + localStorage.setItem(QUEST_STORAGE_KEY, JSON.stringify(quests)); +} + +function doesPostCompleteQuest(post, quest) { + const tagMatches = !quest.required_tag || post.tag === quest.required_tag; + + const wordCountMatches = + !quest.required_word_count || + post.word_count >= quest.required_word_count; + + const imageMatches = !quest.requires_image || Boolean(post.image_url); + + return tagMatches && wordCountMatches && imageMatches; +} + +export async function getTodayQuests() { + return loadQuestsFromStorage(); +} + + + +/* API logic to be replaced by backend */ + +export async function checkPostAgainstQuests(post) { + const quests = loadQuestsFromStorage(); + + const updatedQuests = quests.map((quest) => { + if (quest.status !== "assigned") { + return quest; + } + + if (!doesPostCompleteQuest(post, quest)) { + return quest; + } + + return { + ...quest, + status: "completed", + completed_post_id: post.post_id, + completed_at: new Date().toISOString(), + }; + }); + + saveQuestsToStorage(updatedQuests); + + return updatedQuests; +} + +export async function claimQuestReward(questId) { + const quests = loadQuestsFromStorage(); + + const selectedQuest = quests.find( + (quest) => Number(quest.quest_id) === Number(questId) + ); + + if (!selectedQuest) { + throw new Error("Quest not found."); + } + + if (selectedQuest.status !== "completed") { + throw new Error("Quest is not completed yet."); + } + + const updatedQuests = quests.map((quest) => { + if (Number(quest.quest_id) !== Number(questId)) { + return quest; + } + + return { + ...quest, + status: "claimed", + claimed_at: new Date().toISOString(), + }; + }); + saveQuestsToStorage(updatedQuests); + + const updatedUser = await addWill(selectedQuest.reward_will); + + return { + reward_will: selectedQuest.reward_will, + user: updatedUser, + }; +} \ No newline at end of file diff --git a/src/api/userApi.js b/src/api/userApi.js new file mode 100644 index 0000000..7e5124d --- /dev/null +++ b/src/api/userApi.js @@ -0,0 +1,68 @@ +const USER_STORAGE_KEY = "memory_egg_user"; + +const defaultUser = { + user_id: 1, + email: "demo@nacimiento.app", + nickname: "Wanderer", + will_balance: 0, + 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)); +} + + +/* Exported API logic to be replaced by backend */ + +export async function getCurrentUser() { + return loadUserFromStorage(); +} + +export async function addWill(amount) { + const user = loadUserFromStorage(); + + const updatedUser = { + ...user, + will_balance: user.will_balance + amount, + }; + + saveUserToStorage(updatedUser); + + return updatedUser; +} + +export async function spendWill(amount) { + const user = loadUserFromStorage(); + + if (user.will_balance < amount) { + throw new Error("Not enough Will."); + } + + const updatedUser = { + ...user, + will_balance: user.will_balance - amount, + }; + + saveUserToStorage(updatedUser); + + return updatedUser; +} \ No newline at end of file diff --git a/src/hooks/useCurrentUser.js b/src/hooks/useCurrentUser.js new file mode 100644 index 0000000..65c60eb --- /dev/null +++ b/src/hooks/useCurrentUser.js @@ -0,0 +1,43 @@ +import { useCallback, useEffect, useState } from "react"; +import { getCurrentUser } from "../api/userApi"; + +export function useCurrentUser() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + const reloadUser = useCallback(async () => { + setLoading(true); + + const data = await getCurrentUser(); + + setUser(data); + setLoading(false); + + return data; + }, []); + + useEffect(() => { + let ignore = false; + + async function loadInitialUser() { + const data = await getCurrentUser(); + + if (!ignore) { + setUser(data); + setLoading(false); + } + } + + loadInitialUser(); + + return () => { + ignore = true; + }; + }, []); + + return { + user, + loading, + reloadUser, + }; +} \ No newline at end of file diff --git a/src/hooks/usePosts.js b/src/hooks/usePosts.js new file mode 100644 index 0000000..d1f9a1d --- /dev/null +++ b/src/hooks/usePosts.js @@ -0,0 +1,65 @@ +import { useCallback, useEffect, useState } from "react"; +import { createPost, deletePost, getAllPosts } from "../api/postsApi"; + +export function usePosts() { + const [posts, setPosts] = useState([]); + const [loading, setLoading] = useState(true); + + const reloadPosts = useCallback(async () => { + setLoading(true); + + const data = await getAllPosts(); + + setPosts(Array.isArray(data) ? data : []); + setLoading(false); + + return data; + }, []); + + const addPost = useCallback(async (postData) => { + const newPost = await createPost(postData); + + setPosts((prevPosts) => [newPost, ...prevPosts]); + + return newPost; + }, []); + + const removePost = useCallback(async (postId) => { + await deletePost(postId); + + setPosts((prevPosts) => + prevPosts.filter((post) => Number(post.post_id) !== Number(postId)) + ); + + return true; + }, []); + + useEffect(() => { + let ignore = false; + + async function loadInitialPosts() { + setLoading(true); + + const data = await getAllPosts(); + + if (!ignore) { + setPosts(Array.isArray(data) ? data : []); + setLoading(false); + } + } + + loadInitialPosts(); + + return () => { + ignore = true; + }; + }, []); + + return { + posts, + loading, + addPost, + removePost, + reloadPosts, + }; +} \ No newline at end of file diff --git a/src/hooks/useQuests.js b/src/hooks/useQuests.js new file mode 100644 index 0000000..6807c2a --- /dev/null +++ b/src/hooks/useQuests.js @@ -0,0 +1,67 @@ +import { useCallback, useEffect, useState } from "react"; +import { + checkPostAgainstQuests, + claimQuestReward, + getTodayQuests, +} from "../api/questsApi"; + +export function useQuests() { + const [quests, setQuests] = useState([]); + const [loading, setLoading] = useState(true); + + const reloadQuests = useCallback(async () => { + setLoading(true); + + const data = await getTodayQuests(); + + setQuests(Array.isArray(data) ? data : []); + setLoading(false); + + return data; + }, []); + + const checkPostForQuestCompletion = useCallback(async (post) => { + const updatedQuests = await checkPostAgainstQuests(post); + + setQuests(Array.isArray(updatedQuests) ? updatedQuests : []); + + return updatedQuests; + }, []); + + const claimReward = useCallback(async (questId) => { + await claimQuestReward(questId); + + const updatedQuests = await getTodayQuests(); + + setQuests(Array.isArray(updatedQuests) ? updatedQuests : []); + + return updatedQuests; + }, []); + + useEffect(() => { + let ignore = false; + + async function loadInitialQuests() { + const data = await getTodayQuests(); + + if (!ignore) { + setQuests(Array.isArray(data) ? data : []); + setLoading(false); + } + } + + loadInitialQuests(); + + return () => { + ignore = true; + }; + }, []); + + return { + quests, + loading, + reloadQuests, + checkPostForQuestCompletion, + claimReward, + }; +} \ No newline at end of file diff --git a/src/pages/MemoryArchivePage.css b/src/pages/MemoryArchivePage.css index 7cb9154..377719d 100644 --- a/src/pages/MemoryArchivePage.css +++ b/src/pages/MemoryArchivePage.css @@ -172,6 +172,18 @@ font-weight: 800; } +.archive-shelf-nav .archive-write-link { + background: #fffdf8; + color: #6c5147; + border: 1px solid #d9cfc1; + box-shadow: 0 10px 20px rgba(72, 52, 38, 0.05); +} + +.archive-shelf-nav .archive-write-link:hover { + background: #f6efe5; + color: #4d342c; +} + .archive-progress-card { margin-top: 3.5rem; min-height: 120px; @@ -561,6 +573,18 @@ .preview-actions { margin-top: 0.9rem; } + + .archive-shelf-nav { + flex-direction: row; + gap: 0.6rem; + } + + .archive-shelf-nav a { + flex: 1; + min-height: 40px; + justify-content: center; + padding: 0 0.8rem; + } } @media (max-width: 560px) { @@ -612,4 +636,13 @@ padding: 1.2rem; overflow: hidden; } + .archive-shelf-nav { + gap: 0.45rem; + } + + .archive-shelf-nav a { + min-height: 38px; + font-size: 0.72rem; + padding: 0 0.6rem; + } } \ No newline at end of file diff --git a/src/pages/MemoryArchivePage.jsx b/src/pages/MemoryArchivePage.jsx index 03ba401..08eb2cb 100644 --- a/src/pages/MemoryArchivePage.jsx +++ b/src/pages/MemoryArchivePage.jsx @@ -1,304 +1,7 @@ import { useState } from "react"; +import { usePosts } from "../hooks/usePosts"; import "./MemoryArchivePage.css"; -/* This is just example of archivePosts. Also tests if post list scroll works finely. */ -const archivePosts = [ - { - post_id: 1, - user_id: 1, - title: "Nov 2", - content: - "I'm studying Web Programming. This is so fun as I could create my imagination into something real.", - image_url: null, - tag: "study", - visibility: "private", - word_count: 128, - will_reward: 12, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 2, - user_id: 1, - title: "The Storm of chips", - content: - "I love potato chips. A bottle of Coca-cola goes well with this ngl.", - image_url: - "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=400&q=80", - tag: "food", - visibility: "public", - word_count: 286, - will_reward: 18, - created_at: "2025-11-08", - updated_at: "2025-11-08", - }, - { - post_id: 3, - user_id: 1, - title: "The First Crack", - content: - "This morning, I found a tiny crack on the surface of the egg. Is it a sign that my will is starting to take shape? I feel a swell of emotion in my heart. It feels real now. The weight of existence is shifting into something new.", - image_url: null, - tag: "growth", - visibility: "public", - word_count: 342, - will_reward: 50, - created_at: "2025-11-24", - updated_at: "2025-11-24", - }, - { - post_id: 4, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 5, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 6, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 7, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 8, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 9, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 10, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 11, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 12, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 13, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 14, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 15, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 16, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 17, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 18, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 19, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 20, - user_id: 1, - title: "Example", - content: - "Welcome to the Nacimiento. This is driedoutjerky who's working on the frontend. It's my first time using React and actually feels quite similar to the html. I need to make css anyway.", - image_url: null, - tag: "reflection", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: "2025-11-02", - updated_at: "2025-11-02", - }, - { - post_id: 21, - user_id: 1, - title: "Unknown Memory", - content: - "A memory waiting to be found. The page is still blank, but something about it feels familiar.", - image_url: null, - tag: "mystery", - visibility: "private", - word_count: 0, - will_reward: 0, - created_at: null, - updated_at: null, - }, -]; function formatPostDate(dateString) { if (!dateString) { @@ -387,7 +90,50 @@ function getTagClassName(tag) { function MemoryArchivePage() { - const [selectedPost, setSelectedPost] = useState(archivePosts[2]); + const { posts: archivePosts, loading, removePost } = usePosts(); + const [selectedPostId, setSelectedPostId] = useState(null); + + const selectedPost = + archivePosts.find( + (post) => Number(post.post_id) === Number(selectedPostId) + ) || + archivePosts[0] || + null; + + async function handleDeleteSelectedPost() { + const confirmed = window.confirm("Delete this memory?"); + + if (!confirmed || !selectedPost) { + return; + } + + await removePost(selectedPost.post_id); + setSelectedPostId(null); + } + + if (loading) { + return ( +
+

Loading memories...

+
+ ); + } + + if (archivePosts.length === 0 || !selectedPost) { + return ( +
+
+
Nacimiento
+
+ +
+

No memories yet

+

Write your first notebook post to fill the archive.

+ Write a post +
+
+ ); + } return (
@@ -423,12 +169,16 @@ function MemoryArchivePage() { ▧ Main Shelf + + + ✎ Write Post +

Archive Progress

-

20 memories found

+

{archivePosts.length} memories found

@@ -438,10 +188,10 @@ function MemoryArchivePage() {

diff --git a/src/pages/ViewPostPage.jsx b/src/pages/ViewPostPage.jsx index 96c0f55..b1433ea 100644 --- a/src/pages/ViewPostPage.jsx +++ b/src/pages/ViewPostPage.jsx @@ -1,6 +1,62 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { deletePost, getPostById } from "../api/postsApi"; import "./ViewPostPage.css"; function ViewPostPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const [post, setPost] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function loadPost() { + const foundPost = await getPostById(id); + + setPost(foundPost || null); + setLoading(false); + } + + loadPost(); + }, [id]); + + if (loading) { + return ( +
+

Loading memory...

+
+ ); + } + + if (!post) { + return ( +
+

Post not found.

+ ← 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); + */ + if (!confirmed) { + return; + } + + await deletePost(post.post_id); + /* + console.log("Deleted post id:", post.post_id); + */ + navigate("/archive"); + } + return (
@@ -27,106 +83,30 @@ function ViewPostPage() {
-

Reflections on the project Nacimiento

+

{post.title}

- ▣ May 26, 2026 - study - ◉ private - ⌁ 342 words - ✧ +15 Will + ▣ {new Date(post.created_at).toLocaleDateString()} + {post.tag} + ◉ {post.visibility} + ⌁ {post.word_count} words + ✧ +{post.will_reward} Will
-

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

- -

- Tomorrow, I plan to focus on the structure of the new project. But - for today, sitting with these scattered thoughts feels sufficient. - The progress egg glows a little brighter, a small validation of - simply being present. -

+ {post.content.split("\n").map((paragraph, index) => ( +

{paragraph}

+ ))}
-
diff --git a/src/pages/WritePostPage.css b/src/pages/WritePostPage.css index 179a7d2..d9ff117 100644 --- a/src/pages/WritePostPage.css +++ b/src/pages/WritePostPage.css @@ -1,5 +1,8 @@ -.write-post-page { +.app-page.write-post-page { min-height: 100vh; + min-height: 100dvh; + width: 100%; + overflow-x: hidden; --write-bg: var(--color-bg); --write-bg-soft: var(--color-bg-soft); @@ -13,11 +16,12 @@ --write-toggle-bg: #f4dfce; --write-button-text: #fff8ef; - background: var(--write-bg); + background-color: var(--write-bg); color: var(--write-text); + transition: background-color 180ms ease, color 180ms ease; } -.write-post-page.visibility-private { +.app-page.write-post-page.visibility-private { --write-bg: #eee9df; --write-bg-soft: #f4efe6; --write-card-bg: #fffdf8; @@ -30,7 +34,7 @@ --write-toggle-bg: #f4dfce; } -.write-post-page.visibility-public { +.app-page.write-post-page.visibility-public { --write-bg: #eef1e7; --write-bg-soft: #f6f7ef; --write-card-bg: #fffef7; @@ -43,7 +47,7 @@ --write-toggle-bg: #e2ecd7; } -.write-post-page.visibility-anonymous { +.app-page.write-post-page.visibility-anonymous { --write-bg: #e9e8eb; --write-bg-soft: #f2f0f4; --write-card-bg: #fffafe; @@ -72,7 +76,7 @@ .write-brand { font-family: var(--font-serif); font-size: 1.05rem; - color: #4d342c; + color: var(--write-accent-dark); } .write-nav { @@ -88,8 +92,8 @@ justify-content: center; padding: 0 0.8rem; border-radius: 999px; - background: #ded8cc; - color: #5b4b41; + background: var(--write-toggle-bg); + color: var(--write-accent-dark); font-size: 0.75rem; font-weight: 700; } @@ -111,19 +115,19 @@ font-size: 0.55rem; font-weight: 700; letter-spacing: 0.14em; - color: #5f473d; + color: var(--write-text-soft); } .write-user-text strong { font-family: var(--font-serif); font-size: 0.9rem; - color: #3c2a24; + color: var(--write-accent-dark); } .write-avatar { width: 32px; height: 32px; - border: 1.3px solid #7b6658; + border: 1.3px solid var(--write-accent); border-radius: 50%; position: relative; } @@ -135,7 +139,7 @@ left: 50%; width: 8px; height: 8px; - border: 1.3px solid #7b6658; + border: 1.3px solid var(--write-accent); border-radius: 50%; transform: translateX(-50%); } @@ -147,7 +151,7 @@ bottom: 5px; width: 18px; height: 10px; - border: 1.3px solid #7b6658; + border: 1.3px solid var(--write-accent); border-bottom: none; border-radius: 999px 999px 0 0; transform: translateX(-50%); @@ -210,11 +214,11 @@ width: 100%; padding: 0 0 0.9rem; border: none; - border-bottom: 1px solid #e7ded2; + border-bottom: 1px solid var(--write-card-border); background: transparent; font-family: var(--font-serif); font-size: 1.75rem; - color: #2f241f; + color: var(--write-text); } .write-title-input::placeholder { @@ -223,18 +227,13 @@ .write-title-input:focus { outline: none; - border-bottom-color: #bda183; -} - -.write-body-label { - display: block; - margin-top: 1.25rem; + border-bottom-color: var(--write-accent); } .write-body-label span { display: block; margin-bottom: 0.35rem; - color: #beb6ac; + color: var(--write-text-soft); font-size: 0.8rem; } @@ -255,7 +254,7 @@ } .write-body-input:focus { - outline: 1px solid #c6aa8a; + outline: 1px solid var(--write-accent); } /* Image upload */ @@ -295,6 +294,55 @@ color: #9b9188; } +/* Tag selector */ + +.write-tag-field { + display: flex; + flex-direction: column; + gap: 0.45rem; + width: min(100%, 260px); + color: var(--write-text-soft); + font-size: 0.68rem; + font-weight: 900; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.write-tag-select-wrap { + position: relative; +} + +.write-tag-select-wrap::after { + content: "⌄"; + position: absolute; + top: 50%; + right: 0.9rem; + transform: translateY(-50%); + color: var(--write-accent); + pointer-events: none; + font-size: 0.8rem; +} + +.write-tag-select { + width: 100%; + min-height: 38px; + appearance: none; + border: 1px solid var(--write-card-border); + border-radius: 999px; + background: var(--write-card-bg); + color: var(--write-text); + padding: 0 2.3rem 0 1rem; + font-size: 0.78rem; + font-weight: 800; + cursor: pointer; + box-shadow: 0 10px 20px rgba(73, 54, 41, 0.04); +} + +.write-tag-select:focus { + outline: 2px solid color-mix(in srgb, var(--write-accent) 40%, transparent); + border-color: var(--write-accent); +} + /* Bottom bar */ .write-bottom-bar { @@ -376,7 +424,7 @@ .memory-stats-card h2 { margin: 0 0 1rem; - color: #62544c; + color: var(--write-accent-dark); font-size: 0.75rem; font-weight: 800; letter-spacing: 0.12em; @@ -395,23 +443,128 @@ } .memory-stat-row span { - color: #8a7d73; + color: var(--write-text-soft); font-size: 0.68rem; } .memory-stat-row strong { - color: #3a2a23; + color: var(--write-text); font-size: 0.85rem; } .memory-note { margin: 1.35rem 0 0 0.55rem; - color: #7f756c; + color: var(--write-text-soft); font-size: 0.72rem; font-style: italic; line-height: 1.45; } +.write-quest-card { + margin-top: 1rem; + padding: 1rem; + background: var(--write-card-bg); + border: 1px solid var(--write-card-border); + border-radius: 8px; + box-shadow: 0 10px 24px rgba(73, 54, 41, 0.04); +} + +.write-quest-card h2 { + margin: 0 0 1rem; + color: var(--write-accent-dark); + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.write-quest-empty { + margin: 0; + color: var(--write-text-soft); + font-size: 0.72rem; + line-height: 1.4; +} + +.write-quest-list { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.write-quest-item { + display: flex; + flex-direction: column; + gap: 0.55rem; + padding-bottom: 0.8rem; + border-bottom: 1px solid var(--write-card-border); +} + +.write-quest-item:last-child { + padding-bottom: 0; + border-bottom: none; +} + +.write-quest-item strong { + display: block; + color: var(--write-text); + font-size: 0.76rem; + line-height: 1.25; +} + +.write-quest-item p { + margin: 0.25rem 0 0; + color: var(--write-text-soft); + font-size: 0.68rem; + line-height: 1.35; +} + +.quest-status { + width: fit-content; + min-height: 18px; + display: inline-flex; + align-items: center; + margin-top: 0.45rem; + padding: 0 0.45rem; + border-radius: 999px; + background: var(--write-toggle-bg); + color: var(--write-accent-dark); + font-size: 0.55rem; + font-weight: 900; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.quest-status-completed { + background: #f2e6cd; + color: #7a5a2f; +} + +.quest-status-claimed { + background: #dcefd0; + color: #3e6a34; +} + +.write-quest-item button { + min-height: 30px; + border: none; + border-radius: 999px; + background: var(--write-accent-dark); + color: var(--write-button-text); + font-size: 0.68rem; + font-weight: 800; + cursor: pointer; +} + +.write-quest-item button:hover { + background: var(--write-accent); +} + +.write-quest-item small { + color: var(--write-text-soft); + font-size: 0.65rem; + font-style: italic; +} + /* Accessibility utility */ .sr-only { @@ -486,4 +639,12 @@ .memory-note { margin-left: 0; } + + .write-quest-card { + margin-top: 1rem; + } + + .write-quest-item button { + width: 100%; + } } \ No newline at end of file diff --git a/src/pages/WritePostPage.jsx b/src/pages/WritePostPage.jsx index 721da18..e022170 100644 --- a/src/pages/WritePostPage.jsx +++ b/src/pages/WritePostPage.jsx @@ -1,8 +1,56 @@ import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +/* import { createPost } from "../api/postsApi"; */ +import { usePosts } from "../hooks/usePosts"; +/* import { checkPostAgainstQuests } from "../api/questsApi"; */ +import { useCurrentUser } from "../hooks/useCurrentUser"; +import { useQuests } from "../hooks/useQuests"; import "./WritePostPage.css"; function WritePostPage() { + const { addPost } = usePosts(); + const { user, reloadUser } = useCurrentUser(); + const { quests, checkPostForQuestCompletion, claimReward } = useQuests(); + + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [tag, setTag] = useState("reflection"); + /*const [imageUrl, setImageUrl] = useState("");*/ const [visibility, setVisibility] = useState("private"); + + const navigate = useNavigate(); + + {/*NOTE: LOGICS SHOULD BE IMPLEMENTED IN BACKEND. THESE ARE TEMPORARY PLACEHOLDERS */} + const wordCount = content.trim() ? content.trim().split(/\s+/).length : 0; + const estimatedWill = Math.max(1, Math.floor(wordCount / 10)); + + async function handleSubmit(event) { + event.preventDefault(); /*Stop page from refreshing in form submit*/ + + if (!title.trim() || !content.trim()) { + alert("Please write both title and content."); + return; + } + + const newPost = await addPost({ + title, + content, + tag, + image_url: null, + visibility, + }); + + await checkPostForQuestCompletion(newPost); + + alert("Post created! Redirecting you to Archive Page"); + navigate("/archive"); + } + + async function handleClaimQuest(questId) { + await claimReward(questId); + await reloadUser(); + } + return (
@@ -29,7 +77,7 @@ function WritePostPage() {

Take a moment to reflect and write.

-
+
@@ -47,6 +97,8 @@ function WritePostPage() { id="post-body" className="write-body-input" placeholder="Start typing here..." + value={content} + onChange={(event) => setContent(event.target.value)} /> @@ -57,6 +109,23 @@ function WritePostPage() {
+ +
Post visibility @@ -95,7 +164,7 @@ function WritePostPage() {
-
@@ -108,17 +177,56 @@ function WritePostPage() {
Word Count - ↝ 0 + ↝ {wordCount}
Estimated Will - ↝ 0 + ↝ {estimatedWill}
+ +
+ Current Will + ↝ {user ? user.will_balance : 0} +
+ + +
+

Today’s Quests

+ + {quests.length === 0 ? ( +

No quests assigned.

+ ) : ( +
+ {quests.map((quest) => ( +
+
+ {quest.title} +

{quest.description}

+ + {quest.status} + +
+ + {quest.status === "completed" && ( + + )} + + {quest.status === "claimed" && Reward claimed} +
+ ))} +
+ )}

- Writing helps your egg
+ Writing helps your egg +
grow stronger.

diff --git a/src/styles/global.css b/src/styles/global.css index 66f7287..ab54a28 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -9,11 +9,13 @@ box-sizing: border-box; } -html { /*fill the screen with root page area*/ +html, +body, +#root { min-height: 100%; } -body { /* Default page background, text color, font + removes browser margin. */ +body { min-height: 100%; margin: 0; background: var(--color-bg); @@ -21,6 +23,12 @@ body { /* Default page background, text color, font + removes browser margin. */ font-family: var(--font-sans); } +.app-page { + min-height: 100vh; + min-height: 100dvh; + width: 100%; +} + button, input, textarea,