generated from nhcarrigan/template
feat: replace resource bar profile buttons with avatar dropdown (#103)
Condenses the Donate, Discord, Support, View Profile, and Edit Profile buttons into a single avatar image button. Clicking it opens a dropdown menu with all five actions, significantly decluttering the resource bar.
This commit is contained in:
@@ -135,7 +135,6 @@ const GameLayout = (): JSX.Element => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileUrl = `/profile/${state.player.discordId}`;
|
|
||||||
const codexBadgeCount = pendingCodexEntryIds.length;
|
const codexBadgeCount = pendingCodexEntryIds.length;
|
||||||
const storyBadgeCount = pendingStoryChapterIds.length;
|
const storyBadgeCount = pendingStoryChapterIds.length;
|
||||||
|
|
||||||
@@ -160,7 +159,6 @@ const GameLayout = (): JSX.Element => {
|
|||||||
onEditProfile={handleOpenEditProfile}
|
onEditProfile={handleOpenEditProfile}
|
||||||
onForceSync={forceSync}
|
onForceSync={forceSync}
|
||||||
prestigeCount={state.prestige.count}
|
prestigeCount={state.prestige.count}
|
||||||
profileUrl={profileUrl}
|
|
||||||
resources={state.resources}
|
resources={state.resources}
|
||||||
runestones={state.prestige.runestones}
|
runestones={state.prestige.runestones}
|
||||||
transcendenceCount={state.transcendence?.count ?? 0}
|
transcendenceCount={state.transcendence?.count ?? 0}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
/* eslint-disable max-lines-per-function -- Large header with many resource and action elements */
|
/* 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 */
|
/* 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 { useGame } from "../../context/gameContext.js";
|
||||||
import { RESOURCE_CAP, computeGoldPerSecond } from "../../engine/tick.js";
|
import { RESOURCE_CAP, computeGoldPerSecond } from "../../engine/tick.js";
|
||||||
import type { Resource } from "@elysium/types";
|
import type { Resource } from "@elysium/types";
|
||||||
import type { JSX } from "react";
|
|
||||||
|
|
||||||
interface ResourceBarProperties {
|
interface ResourceBarProperties {
|
||||||
readonly resources: Resource;
|
readonly resources: Resource;
|
||||||
@@ -17,7 +18,6 @@ interface ResourceBarProperties {
|
|||||||
readonly prestigeCount: number;
|
readonly prestigeCount: number;
|
||||||
readonly transcendenceCount: number;
|
readonly transcendenceCount: number;
|
||||||
readonly apotheosisCount: number;
|
readonly apotheosisCount: number;
|
||||||
readonly profileUrl: string;
|
|
||||||
readonly onEditProfile: ()=> void;
|
readonly onEditProfile: ()=> void;
|
||||||
readonly lastSavedAt: number | null;
|
readonly lastSavedAt: number | null;
|
||||||
readonly isSyncing: boolean;
|
readonly isSyncing: boolean;
|
||||||
@@ -58,7 +58,6 @@ const resourceFullTooltip = [
|
|||||||
* @param props.prestigeCount - The number of prestiges completed.
|
* @param props.prestigeCount - The number of prestiges completed.
|
||||||
* @param props.transcendenceCount - The number of transcendences completed.
|
* @param props.transcendenceCount - The number of transcendences completed.
|
||||||
* @param props.apotheosisCount - The number of apotheoses 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.onEditProfile - Callback to open the edit profile modal.
|
||||||
* @param props.lastSavedAt - Timestamp of the last cloud save.
|
* @param props.lastSavedAt - Timestamp of the last cloud save.
|
||||||
* @param props.isSyncing - Whether a sync is currently in progress.
|
* @param props.isSyncing - Whether a sync is currently in progress.
|
||||||
@@ -71,13 +70,14 @@ const ResourceBar = ({
|
|||||||
prestigeCount,
|
prestigeCount,
|
||||||
transcendenceCount,
|
transcendenceCount,
|
||||||
apotheosisCount,
|
apotheosisCount,
|
||||||
profileUrl,
|
|
||||||
onEditProfile,
|
onEditProfile,
|
||||||
lastSavedAt,
|
lastSavedAt,
|
||||||
isSyncing,
|
isSyncing,
|
||||||
onForceSync,
|
onForceSync,
|
||||||
}: ResourceBarProperties): JSX.Element => {
|
}: ResourceBarProperties): JSX.Element => {
|
||||||
const { formatNumber, syncError, state } = useGame();
|
const { formatNumber, syncError, state } = useGame();
|
||||||
|
const [ isProfileOpen, setIsProfileOpen ] = useState(false);
|
||||||
|
|
||||||
const { gold, essence, crystals } = resources;
|
const { gold, essence, crystals } = resources;
|
||||||
let partyCombatPower = 0;
|
let partyCombatPower = 0;
|
||||||
let goldPerSecond = 0;
|
let goldPerSecond = 0;
|
||||||
@@ -88,6 +88,17 @@ const ResourceBar = ({
|
|||||||
}
|
}
|
||||||
goldPerSecond = computeGoldPerSecond(state);
|
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 resourceValues = [ gold, essence, crystals ];
|
||||||
const anyFull = resourceValues.some((v) => {
|
const anyFull = resourceValues.some((v) => {
|
||||||
return v >= RESOURCE_CAP;
|
return v >= RESOURCE_CAP;
|
||||||
@@ -100,6 +111,23 @@ const ResourceBar = ({
|
|||||||
void onForceSync();
|
void onForceSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleToggleProfile(): void {
|
||||||
|
setIsProfileOpen((previous) => {
|
||||||
|
return !previous;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProfileBlur(event: FocusEvent<HTMLDivElement>): void {
|
||||||
|
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||||
|
setIsProfileOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditProfile(): void {
|
||||||
|
setIsProfileOpen(false);
|
||||||
|
onEditProfile();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="resource-bar">
|
<header className="resource-bar">
|
||||||
@@ -174,34 +202,7 @@ const ResourceBar = ({
|
|||||||
{prestigeCount}
|
{prestigeCount}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div className="profile-buttons">
|
<div className="resource-bar-actions">
|
||||||
<a
|
|
||||||
className="profile-link-button"
|
|
||||||
href="https://donate.nhcarrigan.com"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
title="Support the developer"
|
|
||||||
>
|
|
||||||
{"💜"} <span className="btn-label">{"Donate"}</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="profile-link-button"
|
|
||||||
href="https://chat.nhcarrigan.com"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
title="Join our Discord"
|
|
||||||
>
|
|
||||||
{"💬"} <span className="btn-label">{"Discord"}</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="profile-link-button"
|
|
||||||
href="https://support.nhcarrigan.com"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
title="Get support on our forum"
|
|
||||||
>
|
|
||||||
{"🆘"} <span className="btn-label">{"Support"}</span>
|
|
||||||
</a>
|
|
||||||
{syncError === null
|
{syncError === null
|
||||||
? null
|
? null
|
||||||
: <span className="save-status save-error" title={syncError}>
|
: <span className="save-status save-error" title={syncError}>
|
||||||
@@ -228,23 +229,69 @@ const ResourceBar = ({
|
|||||||
? "⏳"
|
? "⏳"
|
||||||
: "💾"}
|
: "💾"}
|
||||||
</button>
|
</button>
|
||||||
|
{avatarUrl === null
|
||||||
|
? null
|
||||||
|
: <div
|
||||||
|
className="profile-menu"
|
||||||
|
onBlur={handleProfileBlur}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="profile-avatar-button"
|
||||||
|
onClick={handleToggleProfile}
|
||||||
|
title="Account"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Profile"
|
||||||
|
className="profile-avatar-img"
|
||||||
|
src={avatarUrl}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{isProfileOpen
|
||||||
|
? <div className="profile-dropdown">
|
||||||
<a
|
<a
|
||||||
className="profile-link-button"
|
className="profile-dropdown-item"
|
||||||
href={profileUrl}
|
href={profileUrl}
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="View your public profile"
|
|
||||||
>
|
>
|
||||||
{"👤"} <span className="btn-label">{"Profile"}</span>
|
{"👤 View Profile"}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
className="profile-edit-button"
|
className="profile-dropdown-item"
|
||||||
onClick={onEditProfile}
|
onClick={handleEditProfile}
|
||||||
title="Edit your profile"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{"✏️"}
|
{"✏️ Edit Profile"}
|
||||||
</button>
|
</button>
|
||||||
|
<hr className="profile-dropdown-divider" />
|
||||||
|
<a
|
||||||
|
className="profile-dropdown-item"
|
||||||
|
href="https://donate.nhcarrigan.com"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{"💜 Donate"}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="profile-dropdown-item"
|
||||||
|
href="https://chat.nhcarrigan.com"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{"💬 Discord"}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="profile-dropdown-item"
|
||||||
|
href="https://support.nhcarrigan.com"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{"🆘 Support"}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{anyFull
|
{anyFull
|
||||||
|
|||||||
+64
-43
@@ -1492,57 +1492,87 @@ body::before {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Profile buttons in ResourceBar ────────────────────────────────────── */
|
/* ── Resource bar actions (save + profile menu) ─────────────────────────── */
|
||||||
|
|
||||||
.profile-buttons {
|
.resource-bar-actions {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-link-button {
|
.profile-menu {
|
||||||
align-items: center;
|
position: relative;
|
||||||
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-link-button:hover {
|
.profile-avatar-button {
|
||||||
background: rgba(147, 51, 234, 0.2);
|
background: none;
|
||||||
border-color: var(--colour-primary);
|
border: 2px solid rgba(147, 51, 234, 0.4);
|
||||||
color: var(--colour-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-edit-button {
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
border: 1px solid rgba(147, 51, 234, 0.4);
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: var(--colour-text-muted);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
display: flex;
|
||||||
font-size: 0.85rem;
|
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
line-height: 1;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
transition: all 0.2s;
|
transition: border-color 0.2s;
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-edit-button:hover {
|
.profile-avatar-button:hover {
|
||||||
background: rgba(147, 51, 234, 0.2);
|
|
||||||
border-color: var(--colour-primary);
|
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);
|
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 {
|
.save-status {
|
||||||
color: var(--colour-text-muted);
|
color: var(--colour-text-muted);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
@@ -3167,10 +3197,10 @@ body::before {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Profile buttons fill their own row, aligned right */
|
/* Resource bar actions fill their own row, aligned right */
|
||||||
.profile-buttons {
|
.resource-bar-actions {
|
||||||
margin-left: 0;
|
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
margin-left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3240,15 +3270,6 @@ body::before {
|
|||||||
|
|
||||||
/* --- Small mobile (≤ 480px) --------------------------- */
|
/* --- Small mobile (≤ 480px) --------------------------- */
|
||||||
@media (max-width: 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 */
|
/* Slightly smaller tab buttons */
|
||||||
.tab-button {
|
.tab-button {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user