From 7e04daa07356fc530e4839f30c78c5f6defedd7d Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 6 Mar 2026 13:56:12 -0800 Subject: [PATCH] feat: add profile editing (bio, display name, stat visibility) --- apps/api/prisma/schema.prisma | 2 + apps/api/src/routes/profile.ts | 57 ++++- apps/web/src/api/client.ts | 10 + .../src/components/game/EditProfileModal.tsx | 138 +++++++++++ apps/web/src/components/game/GameLayout.tsx | 6 + apps/web/src/components/game/ProfilePage.tsx | 57 +++-- apps/web/src/components/ui/ResourceBar.tsx | 36 ++- apps/web/src/styles.css | 214 +++++++++++++++++- packages/types/src/index.ts | 4 + packages/types/src/interfaces/Api.ts | 18 ++ .../types/src/interfaces/ProfileSettings.ts | 13 ++ 11 files changed, 525 insertions(+), 30 deletions(-) create mode 100644 apps/web/src/components/game/EditProfileModal.tsx create mode 100644 packages/types/src/interfaces/ProfileSettings.ts diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index c68de71..d739288 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -14,6 +14,8 @@ model Player { discriminator String avatar String? characterName String @default("") + bio String @default("") + profileSettings Json? createdAt Float lastSavedAt Float totalGoldEarned Float @default(0) diff --git a/apps/api/src/routes/profile.ts b/apps/api/src/routes/profile.ts index 0734a6e..bb73944 100644 --- a/apps/api/src/routes/profile.ts +++ b/apps/api/src/routes/profile.ts @@ -1,9 +1,32 @@ -import type { GameState } from "@elysium/types"; +import type { + GameState, + ProfileSettings, + UpdateProfileRequest, +} 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) + ) { + const obj = raw as Record; + return { + showTotalGold: obj.showTotalGold !== false, + showTotalClicks: obj.showTotalClicks !== false, + showPrestige: obj.showPrestige !== false, + showGuildFounded: obj.showGuildFounded !== false, + }; + } + return { ...DEFAULT_PROFILE_SETTINGS }; +}; + profileRouter.get("/:discordId", async (context) => { const { discordId } = context.req.param(); @@ -18,14 +41,46 @@ profileRouter.get("/:discordId", async (context) => { const state = gameStateRecord?.state as unknown as GameState | undefined; const prestigeCount = state?.prestige.count ?? 0; + const profileSettings = parseProfileSettings(player.profileSettings); return context.json({ characterName: player.characterName, username: player.username, avatar: player.avatar ?? null, + bio: player.bio ?? "", + profileSettings, prestigeCount, totalGoldEarned: player.totalGoldEarned, totalClicks: player.totalClicks, createdAt: player.createdAt, }); }); + +profileRouter.put("/", authMiddleware, async (context) => { + const discordId = context.get("discordId") as string; + const body = await context.req.json(); + + const characterName = (body.characterName ?? "").trim().slice(0, 32); + const bio = (body.bio ?? "").trim().slice(0, 200); + const profileSettings: ProfileSettings = { + showTotalGold: body.profileSettings?.showTotalGold !== false, + showTotalClicks: body.profileSettings?.showTotalClicks !== false, + showPrestige: body.profileSettings?.showPrestige !== false, + showGuildFounded: body.profileSettings?.showGuildFounded !== false, + }; + + if (!characterName) { + return context.json({ error: "Character name cannot be empty" }, 400); + } + + const updated = await prisma.player.update({ + where: { discordId }, + data: { characterName, bio, profileSettings }, + }); + + return context.json({ + characterName: updated.characterName, + bio: updated.bio, + profileSettings, + }); +}); diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 9676d26..0ab8cee 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -8,6 +8,8 @@ import type { PublicProfileResponse, SaveRequest, SaveResponse, + UpdateProfileRequest, + UpdateProfileResponse, } from "@elysium/types"; const BASE_URL = "/api"; @@ -79,3 +81,11 @@ export const getPublicProfile = async ( discordId: string, ): Promise => request(`/profile/${discordId}`); + +export const updateProfile = async ( + body: UpdateProfileRequest, +): Promise => + request("/profile", { + method: "PUT", + body: JSON.stringify(body), + }); diff --git a/apps/web/src/components/game/EditProfileModal.tsx b/apps/web/src/components/game/EditProfileModal.tsx new file mode 100644 index 0000000..5c61d21 --- /dev/null +++ b/apps/web/src/components/game/EditProfileModal.tsx @@ -0,0 +1,138 @@ +import type { ProfileSettings } from "@elysium/types"; +import { useState } from "react"; +import { updateProfile } from "../../api/client.js"; +import { useGame } from "../../context/GameContext.js"; + +interface EditProfileModalProps { + onClose: () => void; +} + +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: "showGuildFounded", label: "Guild Founded Date", icon: "πŸ“…" }, +]; + +export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.Element => { + const { state } = useGame(); + const player = state?.player; + + const [characterName, setCharacterName] = useState(player?.characterName ?? ""); + const [bio, setBio] = useState(""); + const [settings, setSettings] = useState({ + showTotalGold: true, + showTotalClicks: true, + showPrestige: true, + showGuildFounded: true, + }); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [saved, setSaved] = useState(false); + + const handleSave = async (): Promise => { + setSaving(true); + setError(null); + try { + await updateProfile({ characterName, bio, profileSettings: settings }); + setSaved(true); + setTimeout(onClose, 900); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to save"); + } finally { + setSaving(false); + } + }; + + const toggleSetting = (key: keyof ProfileSettings): void => { + setSettings((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + return ( +
+
+
+

Edit Profile

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