Files
elysium/apps/api/src/routes/profile.ts
T
hikari 5ad2c44399 feat: add number format config, resource cap, and modal scroll fix
- Add user-configurable number format (suffix/scientific/engineering)
  - Suffix: K/M/B/T through Dc (1e33), then letter-based a/b/c... indefinitely
  - Scientific: 1.23e15 style via toExponential
  - Engineering: exponent always a multiple of 3 (1.23E15)
  - Stored in ProfileSettings, fetched from profile API on load
  - Picker UI in EditProfileModal with live examples
- Cap all resource accumulation at 1e300 (RESOURCE_CAP constant)
  - Per-resource FULL badge with tooltip in ResourceBar
  - Amber notice strip when any resource is at cap
  - handleClick also respects the cap
- Make EditProfileModal scrollable with viewport margin
  - Flex column layout with sticky header, scrollable form body
  - Bio textarea preserved as resizable with min-height
- Fix ReferenceError: formatNumber not defined in BossPanel/AchievementPanel
  - Pass formatNumber as prop to BossCard and AchievementCard
  - Pass formatNumber as parameter to conditionDescription
2026-03-06 18:59:43 -08:00

112 lines
4.1 KiB
TypeScript

import type {
GameState,
ProfileSettings,
UpdateProfileRequest,
} from "@elysium/types";
import { DEFAULT_PROFILE_SETTINGS } from "@elysium/types";
import { Hono } from "hono";
import { prisma } from "../db/client.js";
import { authMiddleware } from "../middleware/auth.js";
export const profileRouter = new Hono();
const VALID_NUMBER_FORMATS = new Set(["suffix", "scientific", "engineering"]);
const parseProfileSettings = (raw: unknown): ProfileSettings => {
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
const obj = raw as Record<string, unknown>;
const numberFormat = VALID_NUMBER_FORMATS.has(obj.numberFormat as string)
? (obj.numberFormat as ProfileSettings["numberFormat"])
: "suffix";
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,
numberFormat,
};
}
return { ...DEFAULT_PROFILE_SETTINGS };
};
profileRouter.get("/:discordId", async (context) => {
const { discordId } = context.req.param();
const [player, gameStateRecord] = await Promise.all([
prisma.player.findUnique({ where: { discordId } }),
prisma.gameState.findUnique({ where: { discordId } }),
]);
if (!player) {
return context.json({ error: "Player not found" }, 404);
}
const state = gameStateRecord?.state as unknown as GameState | undefined;
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,
avatar: player.avatar ?? null,
bio: player.bio ?? "",
profileSettings,
prestigeCount,
totalGoldEarned: player.totalGoldEarned,
totalClicks: player.totalClicks,
bossesDefeated,
questsCompleted,
adventurersRecruited,
achievementsUnlocked,
createdAt: player.createdAt,
});
});
profileRouter.put("/", authMiddleware, async (context) => {
const discordId = context.get("discordId") as string;
const body = await context.req.json<UpdateProfileRequest>();
const characterName = (body.characterName ?? "").trim().slice(0, 32);
const bio = (body.bio ?? "").trim().slice(0, 200);
const numberFormat = VALID_NUMBER_FORMATS.has(body.profileSettings?.numberFormat as string)
? (body.profileSettings?.numberFormat as ProfileSettings["numberFormat"])
: "suffix";
const profileSettings: ProfileSettings = {
showTotalGold: body.profileSettings?.showTotalGold !== false,
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,
numberFormat,
};
if (!characterName) {
return context.json({ error: "Character name cannot be empty" }, 400);
}
const updated = await prisma.player.update({
where: { discordId },
data: { characterName, bio, profileSettings: profileSettings as object },
});
return context.json({
characterName: updated.characterName,
bio: updated.bio,
profileSettings,
});
});