From 653c36c886e6dee6c85d8eec5cc903ed9e6d28cb Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 6 Mar 2026 14:14:45 -0800 Subject: [PATCH] feat: add more profile stats and auto-populate edit modal Exposes four new stats on public profiles (bosses defeated, quests completed, adventurers recruited, achievements unlocked) with corresponding visibility toggles. The edit modal now auto-populates the character name, bio, and settings from the server on open. --- apps/api/src/routes/profile.ts | 29 ++- .../src/components/game/EditProfileModal.tsx | 172 ++++++++++-------- apps/web/src/components/game/ProfilePage.tsx | 24 +++ packages/types/src/interfaces/Api.ts | 4 + .../types/src/interfaces/ProfileSettings.ts | 8 + 5 files changed, 158 insertions(+), 79 deletions(-) diff --git a/apps/api/src/routes/profile.ts b/apps/api/src/routes/profile.ts index bb73944..ac4d361 100644 --- a/apps/api/src/routes/profile.ts +++ b/apps/api/src/routes/profile.ts @@ -3,25 +3,25 @@ import type { ProfileSettings, UpdateProfileRequest, } from "@elysium/types"; +import { DEFAULT_PROFILE_SETTINGS } from "@elysium/types"; import { Hono } from "hono"; import { prisma } from "../db/client.js"; -import { DEFAULT_PROFILE_SETTINGS } from "@elysium/types"; import { authMiddleware } from "../middleware/auth.js"; export const profileRouter = new Hono(); const parseProfileSettings = (raw: unknown): ProfileSettings => { - if ( - raw !== null && - typeof raw === "object" && - !Array.isArray(raw) - ) { + if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) { const obj = raw as Record; return { showTotalGold: obj.showTotalGold !== false, showTotalClicks: obj.showTotalClicks !== false, showPrestige: obj.showPrestige !== false, showGuildFounded: obj.showGuildFounded !== false, + showBossesDefeated: obj.showBossesDefeated !== false, + showQuestsCompleted: obj.showQuestsCompleted !== false, + showAdventurersRecruited: obj.showAdventurersRecruited !== false, + showAchievementsUnlocked: obj.showAchievementsUnlocked !== false, }; } return { ...DEFAULT_PROFILE_SETTINGS }; @@ -43,6 +43,13 @@ profileRouter.get("/:discordId", async (context) => { const prestigeCount = state?.prestige.count ?? 0; const profileSettings = parseProfileSettings(player.profileSettings); + const bossesDefeated = state?.bosses.filter((b) => b.status === "defeated").length ?? 0; + const questsCompleted = state?.quests.filter((q) => q.status === "completed").length ?? 0; + const adventurersRecruited = + state?.adventurers.reduce((sum, a) => sum + a.count, 0) ?? 0; + const achievementsUnlocked = + (state?.achievements ?? []).filter((a) => a.unlockedAt !== null).length; + return context.json({ characterName: player.characterName, username: player.username, @@ -52,6 +59,10 @@ profileRouter.get("/:discordId", async (context) => { prestigeCount, totalGoldEarned: player.totalGoldEarned, totalClicks: player.totalClicks, + bossesDefeated, + questsCompleted, + adventurersRecruited, + achievementsUnlocked, createdAt: player.createdAt, }); }); @@ -67,6 +78,10 @@ profileRouter.put("/", authMiddleware, async (context) => { showTotalClicks: body.profileSettings?.showTotalClicks !== false, showPrestige: body.profileSettings?.showPrestige !== false, showGuildFounded: body.profileSettings?.showGuildFounded !== false, + showBossesDefeated: body.profileSettings?.showBossesDefeated !== false, + showQuestsCompleted: body.profileSettings?.showQuestsCompleted !== false, + showAdventurersRecruited: body.profileSettings?.showAdventurersRecruited !== false, + showAchievementsUnlocked: body.profileSettings?.showAchievementsUnlocked !== false, }; if (!characterName) { @@ -75,7 +90,7 @@ profileRouter.put("/", authMiddleware, async (context) => { const updated = await prisma.player.update({ where: { discordId }, - data: { characterName, bio, profileSettings }, + data: { characterName, bio, profileSettings: profileSettings as object }, }); return context.json({ diff --git a/apps/web/src/components/game/EditProfileModal.tsx b/apps/web/src/components/game/EditProfileModal.tsx index 5c61d21..62318f4 100644 --- a/apps/web/src/components/game/EditProfileModal.tsx +++ b/apps/web/src/components/game/EditProfileModal.tsx @@ -1,5 +1,6 @@ import type { ProfileSettings } from "@elysium/types"; -import { useState } from "react"; +import { DEFAULT_PROFILE_SETTINGS } from "@elysium/types"; +import { useEffect, useState } from "react"; import { updateProfile } from "../../api/client.js"; import { useGame } from "../../context/GameContext.js"; @@ -11,6 +12,10 @@ const STAT_TOGGLES: { key: keyof ProfileSettings; label: string; icon: string }[ { key: "showTotalGold", label: "Total Gold Earned", icon: "🪙" }, { key: "showTotalClicks", label: "Total Clicks", icon: "👆" }, { key: "showPrestige", label: "Prestige Level", icon: "⭐" }, + { key: "showBossesDefeated", label: "Bosses Defeated", icon: "💀" }, + { key: "showQuestsCompleted", label: "Quests Completed", icon: "📜" }, + { key: "showAdventurersRecruited", label: "Adventurers Recruited", icon: "⚔️" }, + { key: "showAchievementsUnlocked", label: "Achievements Unlocked", icon: "🏆" }, { key: "showGuildFounded", label: "Guild Founded Date", icon: "📅" }, ]; @@ -20,16 +25,35 @@ export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX. const [characterName, setCharacterName] = useState(player?.characterName ?? ""); const [bio, setBio] = useState(""); - const [settings, setSettings] = useState({ - showTotalGold: true, - showTotalClicks: true, - showPrestige: true, - showGuildFounded: true, - }); + const [settings, setSettings] = useState({ ...DEFAULT_PROFILE_SETTINGS }); + const [loadingProfile, setLoadingProfile] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [saved, setSaved] = useState(false); + // Fetch current profile to auto-populate bio and settings + useEffect(() => { + if (!player?.discordId) return; + fetch(`/api/profile/${player.discordId}`) + .then(async (res) => { + if (!res.ok) return; + const data = (await res.json()) as { + bio: string; + profileSettings: ProfileSettings; + characterName: string; + }; + setBio(data.bio ?? ""); + setSettings({ ...DEFAULT_PROFILE_SETTINGS, ...data.profileSettings }); + setCharacterName(data.characterName ?? player.characterName ?? ""); + }) + .catch(() => { + // Fall back to local state if fetch fails — not a blocking error + }) + .finally(() => { + setLoadingProfile(false); + }); + }, [player?.discordId, player?.characterName]); + const handleSave = async (): Promise => { setSaving(true); setError(null); @@ -63,75 +87,79 @@ export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX. -
- - { setCharacterName(e.target.value); }} - /> - {characterName.length} / 32 + {loadingProfile ? ( +

Loading your profile…

+ ) : ( +
+ + { setCharacterName(e.target.value); }} + /> + {characterName.length} / 32 - -