diff --git a/apps/web/src/components/game/gameLayout.tsx b/apps/web/src/components/game/gameLayout.tsx index 23819e6..37162da 100644 --- a/apps/web/src/components/game/gameLayout.tsx +++ b/apps/web/src/components/game/gameLayout.tsx @@ -135,7 +135,6 @@ const GameLayout = (): JSX.Element => { ); } - const profileUrl = `/profile/${state.player.discordId}`; const codexBadgeCount = pendingCodexEntryIds.length; const storyBadgeCount = pendingStoryChapterIds.length; @@ -160,7 +159,6 @@ const GameLayout = (): JSX.Element => { onEditProfile={handleOpenEditProfile} onForceSync={forceSync} prestigeCount={state.prestige.count} - profileUrl={profileUrl} resources={state.resources} runestones={state.prestige.runestones} transcendenceCount={state.transcendence?.count ?? 0} diff --git a/apps/web/src/components/ui/resourceBar.tsx b/apps/web/src/components/ui/resourceBar.tsx index a7599cd..351d690 100644 --- a/apps/web/src/components/ui/resourceBar.tsx +++ b/apps/web/src/components/ui/resourceBar.tsx @@ -5,11 +5,12 @@ * @author Naomi Carrigan */ /* eslint-disable max-lines-per-function -- Large header with many resource and action elements */ +/* eslint-disable max-statements -- Resource bar requires many local computations and handlers */ /* eslint-disable complexity -- Many conditional resource and badge render paths */ +import { useState, type FocusEvent, type JSX } from "react"; import { useGame } from "../../context/gameContext.js"; import { RESOURCE_CAP, computeGoldPerSecond } from "../../engine/tick.js"; import type { Resource } from "@elysium/types"; -import type { JSX } from "react"; interface ResourceBarProperties { readonly resources: Resource; @@ -17,7 +18,6 @@ interface ResourceBarProperties { readonly prestigeCount: number; readonly transcendenceCount: number; readonly apotheosisCount: number; - readonly profileUrl: string; readonly onEditProfile: ()=> void; readonly lastSavedAt: number | null; readonly isSyncing: boolean; @@ -58,7 +58,6 @@ const resourceFullTooltip = [ * @param props.prestigeCount - The number of prestiges completed. * @param props.transcendenceCount - The number of transcendences completed. * @param props.apotheosisCount - The number of apotheoses completed. - * @param props.profileUrl - The URL of the player's public profile. * @param props.onEditProfile - Callback to open the edit profile modal. * @param props.lastSavedAt - Timestamp of the last cloud save. * @param props.isSyncing - Whether a sync is currently in progress. @@ -71,13 +70,14 @@ const ResourceBar = ({ prestigeCount, transcendenceCount, apotheosisCount, - profileUrl, onEditProfile, lastSavedAt, isSyncing, onForceSync, }: ResourceBarProperties): JSX.Element => { const { formatNumber, syncError, state } = useGame(); + const [ isProfileOpen, setIsProfileOpen ] = useState(false); + const { gold, essence, crystals } = resources; let partyCombatPower = 0; let goldPerSecond = 0; @@ -88,6 +88,17 @@ const ResourceBar = ({ } goldPerSecond = computeGoldPerSecond(state); } + + let avatarUrl: string | null = null; + if (state !== null) { + avatarUrl = state.player.avatar === null + ? `https://cdn.discordapp.com/embed/avatars/${String(Number.parseInt(state.player.discordId, 10) % 5)}.png` + : `https://cdn.discordapp.com/avatars/${state.player.discordId}/${state.player.avatar}.png?size=64`; + } + const profileUrl = state === null + ? "#" + : `/profile/${state.player.discordId}`; + const resourceValues = [ gold, essence, crystals ]; const anyFull = resourceValues.some((v) => { return v >= RESOURCE_CAP; @@ -100,6 +111,23 @@ const ResourceBar = ({ void onForceSync(); } + function handleToggleProfile(): void { + setIsProfileOpen((previous) => { + return !previous; + }); + } + + function handleProfileBlur(event: FocusEvent): void { + if (!event.currentTarget.contains(event.relatedTarget)) { + setIsProfileOpen(false); + } + } + + function handleEditProfile(): void { + setIsProfileOpen(false); + onEditProfile(); + } + return ( <>
@@ -174,34 +202,7 @@ const ResourceBar = ({ {prestigeCount} } -
- - {"💜"} {"Donate"} - - - {"💬"} {"Discord"} - - - {"🆘"} {"Support"} - +
{syncError === null ? null : @@ -228,23 +229,69 @@ const ResourceBar = ({ ? "⏳" : "💾"} - - {"👤"} {"Profile"} - - + {avatarUrl === null + ? null + :
+ + {isProfileOpen + ? + : null} +
}
{anyFull diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 4d0ed2c..fd46462 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -1492,57 +1492,87 @@ body::before { font-size: 0.85rem; } -/* ── Profile buttons in ResourceBar ────────────────────────────────────── */ +/* ── Resource bar actions (save + profile menu) ─────────────────────────── */ -.profile-buttons { +.resource-bar-actions { align-items: center; display: flex; gap: 0.35rem; margin-left: auto; } -.profile-link-button { - align-items: center; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(147, 51, 234, 0.4); - border-radius: 1rem; - color: var(--colour-text-muted); - display: flex; - font-size: 0.8rem; - gap: 0.3rem; - padding: 0.3rem 0.8rem; - text-decoration: none; - transition: all 0.2s; - white-space: nowrap; +.profile-menu { + position: relative; } -.profile-link-button:hover { - background: rgba(147, 51, 234, 0.2); - border-color: var(--colour-primary); - color: var(--colour-text); -} - -.profile-edit-button { - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(147, 51, 234, 0.4); +.profile-avatar-button { + background: none; + border: 2px solid rgba(147, 51, 234, 0.4); border-radius: 50%; - color: var(--colour-text-muted); cursor: pointer; - font-family: inherit; - font-size: 0.85rem; + display: flex; height: 2rem; - line-height: 1; + overflow: hidden; padding: 0; - transition: all 0.2s; + transition: border-color 0.2s; width: 2rem; } -.profile-edit-button:hover { - background: rgba(147, 51, 234, 0.2); +.profile-avatar-button:hover { border-color: var(--colour-primary); +} + +.profile-avatar-img { + height: 100%; + object-fit: cover; + width: 100%; +} + +.profile-dropdown { + background: var(--colour-surface); + border: 1px solid rgba(147, 51, 234, 0.4); + border-radius: 0.5rem; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + display: flex; + flex-direction: column; + min-width: 10rem; + padding: 0.25rem; + position: absolute; + right: 0; + top: calc(100% + 0.4rem); + z-index: 100; +} + +.profile-dropdown-item { + align-items: center; + background: none; + border: none; + border-radius: 0.35rem; + color: var(--colour-text-muted); + cursor: pointer; + display: flex; + font-family: inherit; + font-size: 0.85rem; + gap: 0.4rem; + padding: 0.45rem 0.75rem; + text-align: left; + text-decoration: none; + transition: background 0.15s, color 0.15s; + white-space: nowrap; + width: 100%; +} + +.profile-dropdown-item:hover { + background: rgba(147, 51, 234, 0.15); color: var(--colour-text); } +.profile-dropdown-divider { + border: none; + border-top: 1px solid rgba(147, 51, 234, 0.2); + margin: 0.25rem 0; +} + .save-status { color: var(--colour-text-muted); font-size: 0.75rem; @@ -3167,10 +3197,10 @@ body::before { display: none; } - /* Profile buttons fill their own row, aligned right */ - .profile-buttons { - margin-left: 0; + /* Resource bar actions fill their own row, aligned right */ + .resource-bar-actions { justify-content: flex-end; + margin-left: 0; width: 100%; } @@ -3240,15 +3270,6 @@ body::before { /* --- Small mobile (≤ 480px) --------------------------- */ @media (max-width: 480px) { - /* Icon-only profile link buttons to save horizontal space */ - .btn-label { - display: none; - } - - .profile-link-button { - padding: 0.3rem 0.5rem; - } - /* Slightly smaller tab buttons */ .tab-button { font-size: 0.8rem;