From 23bc808ccad9b2dc8d0eef3685569d3c80e680de Mon Sep 17 00:00:00 2001 From: Pabloader Date: Thu, 9 Apr 2026 19:49:44 +0000 Subject: [PATCH] Styling --- .../horde/assets/leaderboard-modal.module.css | 32 +++-- .../assets/manage-workers-modal.module.css | 15 +-- src/games/horde/assets/modal.module.css | 72 ++++++++++ .../horde/assets/options-modal.module.css | 28 +--- src/games/horde/assets/stats-panel.module.css | 17 ++- .../components/modals/leaderboard-modal.tsx | 108 +++++++++------ .../modals/manage-workers-modal.tsx | 127 +++++++++--------- .../horde/components/modals/options-modal.tsx | 66 ++++++--- src/games/horde/components/stats-panel.tsx | 21 +-- src/games/horde/components/workers-panel.tsx | 4 +- src/games/horde/utils/api.ts | 1 + 11 files changed, 303 insertions(+), 188 deletions(-) create mode 100644 src/games/horde/assets/modal.module.css diff --git a/src/games/horde/assets/leaderboard-modal.module.css b/src/games/horde/assets/leaderboard-modal.module.css index a500833..a7e6b35 100644 --- a/src/games/horde/assets/leaderboard-modal.module.css +++ b/src/games/horde/assets/leaderboard-modal.module.css @@ -1,14 +1,5 @@ -.content { - display: flex; - flex-direction: column; - gap: 12px; - min-width: 380px; - max-width: 560px; -} - -.title { - font-size: 18px; - color: var(--text); +.modal { + width: 480px; } .loading, .error { @@ -38,6 +29,10 @@ font-weight: normal; font-size: 11px; text-transform: uppercase; + letter-spacing: 0.5px; + position: sticky; + top: 0; + background: var(--bg); } td { @@ -62,3 +57,18 @@ .below { color: var(--accent-alt) !important; } + +.nextPlace { + font-size: 13px; + color: var(--text-muted); + + strong { + color: var(--yellow); + } +} + +.nextPlaceMeta { + font-size: 12px; + color: var(--text-muted); + opacity: 0.7; +} diff --git a/src/games/horde/assets/manage-workers-modal.module.css b/src/games/horde/assets/manage-workers-modal.module.css index 69db148..22c385f 100644 --- a/src/games/horde/assets/manage-workers-modal.module.css +++ b/src/games/horde/assets/manage-workers-modal.module.css @@ -1,16 +1,5 @@ -.content { - display: flex; - flex-direction: column; - gap: 16px; - min-width: 360px; - max-width: 560px; - max-height: 80vh; - overflow-y: auto; -} - -.title { - font-size: 18px; - color: var(--text); +.modal { + width: 480px; } .loading { diff --git a/src/games/horde/assets/modal.module.css b/src/games/horde/assets/modal.module.css new file mode 100644 index 0000000..a04c66f --- /dev/null +++ b/src/games/horde/assets/modal.module.css @@ -0,0 +1,72 @@ +.overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + max-height: 80vh; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.title { + font-size: 16px; + font-weight: bold; + color: var(--text); +} + +.closeButton { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + color: var(--text-muted); + background: transparent; + border: none; + border-radius: var(--radius); + cursor: pointer; + + &:hover { + color: var(--text); + background: var(--bg-hover); + } +} + +.body { + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 14px; + min-height: 0; +} + +.footer { + padding: 12px 16px; + border-top: 1px solid var(--border); + display: flex; + flex-direction: row; + gap: 8px; + flex-shrink: 0; + align-items: center; +} diff --git a/src/games/horde/assets/options-modal.module.css b/src/games/horde/assets/options-modal.module.css index 6a970ad..70c8578 100644 --- a/src/games/horde/assets/options-modal.module.css +++ b/src/games/horde/assets/options-modal.module.css @@ -1,13 +1,5 @@ -.content { - display: flex; - flex-direction: column; - gap: 16px; - min-width: 320px; -} - -.title { - font-size: 18px; - color: var(--text); +.modal { + width: 380px; } .label { @@ -16,22 +8,12 @@ gap: 6px; font-size: 13px; color: var(--text-muted); -} -.input { - width: 100%; -} - -.actions { - display: flex; - flex-direction: row; - gap: 8px; + input { + width: 100%; + } } .saveBtn { color: var(--accent-alt) !important; - - &:hover { - color: var(--accent-alt) !important; - } } diff --git a/src/games/horde/assets/stats-panel.module.css b/src/games/horde/assets/stats-panel.module.css index f8c855e..d3f369a 100644 --- a/src/games/horde/assets/stats-panel.module.css +++ b/src/games/horde/assets/stats-panel.module.css @@ -1,6 +1,6 @@ .panel { - width: 320px; - min-width: 260px; + width: 420px; + min-width: 320px; flex-shrink: 0; display: flex; flex-direction: column; @@ -73,11 +73,13 @@ .tableWrapper { overflow-y: auto; + overflow-x: hidden; flex: 1; } .table { width: 100%; + table-layout: fixed; border-collapse: collapse; font-size: 12px; @@ -103,10 +105,7 @@ td { color: var(--text-dim); - max-width: 140px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + overflow-wrap: break-word; } tr:hover td { @@ -118,3 +117,9 @@ .ownModel td { color: var(--accent-alt) !important; } + +.colNum { + width: 52px; + text-align: right; + white-space: nowrap; +} diff --git a/src/games/horde/components/modals/leaderboard-modal.tsx b/src/games/horde/components/modals/leaderboard-modal.tsx index aaa2b7c..70a1040 100644 --- a/src/games/horde/components/modals/leaderboard-modal.tsx +++ b/src/games/horde/components/modals/leaderboard-modal.tsx @@ -1,8 +1,9 @@ import { useEffect, useState } from "preact/hooks"; -import { Modal } from "@common/components/modal/Modal"; +import { X } from "lucide-preact"; import { useHordeState } from "../../contexts/state"; import { fetchLeaderboard, type LeaderboardEntry } from "../../utils/api"; -import { formatNumber } from "@common/utils"; +import { formatNumber, formatTime } from "@common/utils"; +import modalStyles from "../../assets/modal.module.css"; import styles from "../../assets/leaderboard-modal.module.css"; interface Props { @@ -14,7 +15,13 @@ const PAGE_SIZE = 25; export const LeaderboardModal = ({ open, onClose }: Props) => { const { state } = useHordeState(); - const { user } = state; + const { user, workers } = state; + + const ownWorkerIds = new Set(user?.worker_ids ?? []); + const ownWorkers = workers.filter(w => ownWorkerIds.has(w.id)); + const totalUptime = ownWorkers.reduce((s, w) => s + w.uptime, 0); + const totalGenerated = ownWorkers.reduce((s, w) => s + (w.kudos_details?.generated ?? 0), 0); + const kudosPerHour = totalUptime > 0 ? (totalGenerated / totalUptime) * 3600 : 0; const [rows, setRows] = useState<(LeaderboardEntry & { rank: number; diff?: number })[]>([]); const [loading, setLoading] = useState(false); @@ -29,13 +36,11 @@ export const LeaderboardModal = ({ open, onClose }: Props) => { setError(null); try { if (!user) { - // Show top 10 const page = await fetchLeaderboard(1); if (!cancelled) { setRows(page.slice(0, 10).map((e, i) => ({ ...e, rank: i + 1 }))); } } else { - // Find user's rank via linear page scan, then show up to 10 above let userRank = -1; let page = 1; outer: while (true) { @@ -48,12 +53,11 @@ export const LeaderboardModal = ({ open, onClose }: Props) => { } } page++; - if (page > 20) break; // safety cap + if (page > 20) break; } if (cancelled) return; - // Collect rows: up to 10 above the user + user row const startRank = Math.max(1, userRank - 10); const startPage = Math.ceil(startRank / PAGE_SIZE); const endPage = Math.ceil(userRank / PAGE_SIZE); @@ -89,39 +93,67 @@ export const LeaderboardModal = ({ open, onClose }: Props) => { return () => { cancelled = true; }; }, [open, user?.username]); + useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [open]); + + if (!open) return null; + return ( - -
-

Leaderboard

- {loading &&

Loading…

} - {error &&

{error}

} - {!loading && !error && rows.length > 0 && ( - - - - - - - {user && } - - - - {rows.map(r => ( - - - - - {user && ( - - )} +
{ if (e.target === e.currentTarget) onClose(); }}> +
+
+ Leaderboard + +
+
+ {loading &&

Loading…

} + {error &&

{error}

} + {!loading && !error && rows.length > 0 && ( +
#UsernameKudosDiff
{r.rank}{r.username}{formatNumber(r.kudos)} 0 ? styles.above : styles.below) : undefined}> - {r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'} -
+ + + + + + {user && } - ))} - -
#UsernameKudosDiff
- )} + + + {rows.map(r => ( + + {r.rank} + {r.username} + {formatNumber(r.kudos)} + {user && ( + 0 ? styles.above : styles.below) : undefined}> + {r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'} + + )} + + ))} + + + )} +
+ {(() => { + if (!user || rows.length < 2 || kudosPerHour <= 0) return null; + const nextRow = rows[rows.length - 2]; + if (!nextRow.diff || nextRow.diff <= 0) return null; + const secs = Math.ceil((nextRow.diff / kudosPerHour) * 3600); + return ( +
+ + Time to #{nextRow.rank}: {formatTime(secs)} + ({formatNumber(Math.round(kudosPerHour))} kudos/h) + +
+ ); + })()} -
+ ); }; diff --git a/src/games/horde/components/modals/manage-workers-modal.tsx b/src/games/horde/components/modals/manage-workers-modal.tsx index a7f57dc..6183ae9 100644 --- a/src/games/horde/components/modals/manage-workers-modal.tsx +++ b/src/games/horde/components/modals/manage-workers-modal.tsx @@ -1,7 +1,8 @@ import { useEffect, useState } from "preact/hooks"; -import { Modal } from "@common/components/modal/Modal"; +import { X } from "lucide-preact"; import { useHordeState } from "../../contexts/state"; import { fetchWorker, updateWorker, deleteWorker, type WorkerData } from "../../utils/api"; +import modalStyles from "../../assets/modal.module.css"; import styles from "../../assets/manage-workers-modal.module.css"; interface Props { @@ -36,11 +37,7 @@ export const ManageWorkersModal = ({ open, onClose }: Props) => { setWorkerDetails(details); const initialEdits: Record = {}; for (const w of details) { - initialEdits[w.id] = { - name: w.name, - info: w.info ?? '', - maintenance_mode: w.maintenance_mode, - }; + initialEdits[w.id] = { name: w.name, info: w.info ?? '', maintenance_mode: w.maintenance_mode }; } setEdits(initialEdits); }) @@ -50,6 +47,15 @@ export const ManageWorkersModal = ({ open, onClose }: Props) => { return () => { cancelled = true; }; }, [open, user?.worker_ids.join(',')]); + useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [open]); + + if (!open || !user) return null; + const setEdit = (id: string, patch: Partial) => { setEdits(prev => ({ ...prev, [id]: { ...prev[id], ...patch } })); }; @@ -68,14 +74,12 @@ export const ManageWorkersModal = ({ open, onClose }: Props) => { }; const confirmDelete = async (id: string) => { - if (!apiKey) return; - if (!confirm('Delete this worker? This cannot be undone.')) return; + if (!apiKey || !confirm('Delete this worker? This cannot be undone.')) return; setDeleting(id); try { await deleteWorker(id, apiKey); setWorkerDetails(prev => prev.filter(w => w.id !== id)); - // Trigger a re-fetch of the user profile on next poll - dispatch({ type: 'SET_USER', user: user ? { ...user, worker_ids: user.worker_ids.filter(i => i !== id) } : null }); + dispatch({ type: 'SET_USER', user: { ...user, worker_ids: user.worker_ids.filter(i => i !== id) } }); } catch (e) { console.error('[horde] delete worker error:', e); } finally { @@ -83,62 +87,57 @@ export const ManageWorkersModal = ({ open, onClose }: Props) => { } }; - if (!user) return null; - return ( - -
-

Manage Workers

- {loading &&

Loading…

} - {!loading && workerDetails.map(w => { - const edit = edits[w.id] ?? { name: w.name, info: w.info ?? '', maintenance_mode: w.maintenance_mode }; - return ( -
-
{w.name}
- -