quackscore 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +26 -10
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +19 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/leaderboard.d.ts +5 -1
- package/dist/commands/leaderboard.d.ts.map +1 -1
- package/dist/commands/leaderboard.js +57 -29
- package/dist/commands/leaderboard.js.map +1 -1
- package/dist/commands/profile-options.d.ts +24 -0
- package/dist/commands/profile-options.d.ts.map +1 -0
- package/dist/commands/profile-options.js +38 -0
- package/dist/commands/profile-options.js.map +1 -0
- package/dist/commands/remove.d.ts +5 -1
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +14 -5
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/show.d.ts +5 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +12 -3
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/update-summary.d.ts +2 -0
- package/dist/commands/update-summary.d.ts.map +1 -1
- package/dist/commands/update-summary.js +12 -3
- package/dist/commands/update-summary.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +148 -68
- package/dist/commands/update.js.map +1 -1
- package/dist/config/types.d.ts +1 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +14 -0
- package/dist/config/types.js.map +1 -1
- package/dist/github/client.d.ts +5 -5
- package/dist/github/client.d.ts.map +1 -1
- package/dist/github/client.js +163 -186
- package/dist/github/client.js.map +1 -1
- package/dist/index.js +32 -7
- package/dist/index.js.map +1 -1
- package/dist/llm/analyzer.d.ts +3 -3
- package/dist/llm/analyzer.d.ts.map +1 -1
- package/dist/llm/analyzer.js.map +1 -1
- package/dist/llm/character.d.ts +2 -2
- package/dist/llm/character.d.ts.map +1 -1
- package/dist/llm/character.js.map +1 -1
- package/dist/llm/client.d.ts +2 -2
- package/dist/llm/client.d.ts.map +1 -1
- package/dist/llm/client.js +1 -1
- package/dist/llm/client.js.map +1 -1
- package/dist/llm/providers.d.ts +3 -3
- package/dist/llm/providers.d.ts.map +1 -1
- package/dist/llm/providers.js +25 -6
- package/dist/llm/providers.js.map +1 -1
- package/dist/report/generate.d.ts.map +1 -1
- package/dist/report/generate.js +4 -2
- package/dist/report/generate.js.map +1 -1
- package/dist/report/leaderboard.d.ts +5 -1
- package/dist/report/leaderboard.d.ts.map +1 -1
- package/dist/report/leaderboard.js +611 -865
- package/dist/report/leaderboard.js.map +1 -1
- package/dist/shared/profile-scope.d.ts +19 -0
- package/dist/shared/profile-scope.d.ts.map +1 -0
- package/dist/shared/profile-scope.js +101 -0
- package/dist/shared/profile-scope.js.map +1 -0
- package/dist/shared/types.d.ts +28 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/ui.d.ts +1 -0
- package/dist/shared/ui.d.ts.map +1 -1
- package/dist/shared/ui.js +3 -0
- package/dist/shared/ui.js.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/leaderboard.d.ts +3 -2
- package/dist/storage/leaderboard.d.ts.map +1 -1
- package/dist/storage/leaderboard.js +80 -9
- package/dist/storage/leaderboard.js.map +1 -1
- package/dist/storage/paths.d.ts +5 -0
- package/dist/storage/paths.d.ts.map +1 -1
- package/dist/storage/paths.js +13 -0
- package/dist/storage/paths.js.map +1 -1
- package/dist/storage/report.d.ts +4 -2
- package/dist/storage/report.d.ts.map +1 -1
- package/dist/storage/report.js +25 -8
- package/dist/storage/report.js.map +1 -1
- package/dist/storage/user.d.ts +2 -1
- package/dist/storage/user.d.ts.map +1 -1
- package/dist/storage/user.js +27 -10
- package/dist/storage/user.js.map +1 -1
- package/package.json +13 -6
|
@@ -10,9 +10,23 @@ const DISPLAY_LABELS = {
|
|
|
10
10
|
security: 'Security',
|
|
11
11
|
sre: 'SRE',
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
function normalizeTeamKey(value) {
|
|
14
|
+
return String(value ?? '').trim().toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
function collectTeams(profiles) {
|
|
17
|
+
const labels = new Map();
|
|
18
|
+
for (const profile of profiles) {
|
|
19
|
+
for (const team of profile.teams ?? []) {
|
|
20
|
+
const key = normalizeTeamKey(team);
|
|
21
|
+
if (!key || labels.has(key))
|
|
22
|
+
continue;
|
|
23
|
+
labels.set(key, team);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [...labels.values()].sort((left, right) => left.localeCompare(right));
|
|
27
|
+
}
|
|
14
28
|
function escapeHtml(value) {
|
|
15
|
-
return value
|
|
29
|
+
return String(value ?? '')
|
|
16
30
|
.replace(/&/g, '&')
|
|
17
31
|
.replace(/</g, '<')
|
|
18
32
|
.replace(/>/g, '>')
|
|
@@ -24,158 +38,18 @@ function formatLabel(value) {
|
|
|
24
38
|
return 'Unspecified';
|
|
25
39
|
return DISPLAY_LABELS[value] ?? value.split('_').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
|
|
26
40
|
}
|
|
27
|
-
function compactText(value, maxLength = 260) {
|
|
28
|
-
if (!value)
|
|
29
|
-
return 'Summary not available yet.';
|
|
30
|
-
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
31
|
-
if (normalized.length <= maxLength)
|
|
32
|
-
return normalized;
|
|
33
|
-
return `${normalized.slice(0, maxLength - 3).trimEnd()}...`;
|
|
34
|
-
}
|
|
35
|
-
function formatDate(value) {
|
|
36
|
-
if (!value)
|
|
37
|
-
return 'N/A';
|
|
38
|
-
const parsed = Date.parse(value);
|
|
39
|
-
if (Number.isNaN(parsed))
|
|
40
|
-
return 'N/A';
|
|
41
|
-
return new Date(parsed).toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' });
|
|
42
|
-
}
|
|
43
|
-
function calculateLevel(totalPoints) {
|
|
44
|
-
let level = 1;
|
|
45
|
-
for (let i = 1; i < LEVEL_THRESHOLDS.length; i++) {
|
|
46
|
-
if (totalPoints >= LEVEL_THRESHOLDS[i]) {
|
|
47
|
-
level = i + 1;
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
return level;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return MAX_LEVEL;
|
|
54
|
-
}
|
|
55
|
-
function compareEntries(left, right) {
|
|
56
|
-
return right.totalPoints - left.totalPoints
|
|
57
|
-
|| right.analyzedPRs - left.analyzedPRs
|
|
58
|
-
|| Date.parse(right.lastActivityAt ?? right.lastUpdated) - Date.parse(left.lastActivityAt ?? left.lastUpdated)
|
|
59
|
-
|| left.username.localeCompare(right.username);
|
|
60
|
-
}
|
|
61
|
-
function buildEntry(profile) {
|
|
62
|
-
if (profile.contributions.length === 0) {
|
|
63
|
-
return {
|
|
64
|
-
storageKey: profile.storageKey,
|
|
65
|
-
username: profile.username,
|
|
66
|
-
totalPoints: profile.totalPoints,
|
|
67
|
-
level: profile.level,
|
|
68
|
-
lastUpdated: profile.lastUpdated,
|
|
69
|
-
lastActivityAt: profile.lastActivityAt,
|
|
70
|
-
analyzedPRs: profile.analyzedPRs,
|
|
71
|
-
primaryArea: profile.primaryArea,
|
|
72
|
-
title: profile.title,
|
|
73
|
-
summary: profile.summary,
|
|
74
|
-
reportPath: profile.reportPath,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
const areaTotals = profile.contributions.reduce((totals, contribution) => {
|
|
78
|
-
for (const area of contribution.areas) {
|
|
79
|
-
totals[area] = (totals[area] || 0) + contribution.points;
|
|
80
|
-
}
|
|
81
|
-
return totals;
|
|
82
|
-
}, {});
|
|
83
|
-
const totalPoints = profile.contributions.reduce((sum, contribution) => sum + contribution.points, 0);
|
|
84
|
-
const primaryArea = Object.entries(areaTotals).sort(([, left], [, right]) => right - left)[0]?.[0];
|
|
85
|
-
const lastActivityAt = profile.contributions.reduce((latest, contribution) => {
|
|
86
|
-
if (!latest)
|
|
87
|
-
return contribution.mergedAt;
|
|
88
|
-
return Date.parse(contribution.mergedAt) > Date.parse(latest) ? contribution.mergedAt : latest;
|
|
89
|
-
}, undefined);
|
|
90
|
-
return {
|
|
91
|
-
storageKey: profile.storageKey,
|
|
92
|
-
username: profile.username,
|
|
93
|
-
totalPoints,
|
|
94
|
-
level: calculateLevel(totalPoints),
|
|
95
|
-
lastUpdated: profile.lastUpdated,
|
|
96
|
-
lastActivityAt,
|
|
97
|
-
analyzedPRs: profile.contributions.length,
|
|
98
|
-
primaryArea,
|
|
99
|
-
title: profile.title,
|
|
100
|
-
summary: profile.summary,
|
|
101
|
-
reportPath: profile.reportPath,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function renderEntry(entry, index, isOpen) {
|
|
105
|
-
const preview = compactText(entry.summary, 170);
|
|
106
|
-
const fullSummary = compactText(entry.summary, 520);
|
|
107
|
-
const reportAction = entry.reportPath
|
|
108
|
-
? `<a class="action-link" href="file:///${entry.reportPath.replace(/\\/g, '/')}" target="_blank" rel="noopener noreferrer">Open report</a>`
|
|
109
|
-
: '<span class="action-link disabled">Report missing</span>';
|
|
110
|
-
return `<details class="entry"${isOpen ? ' open' : ''}>
|
|
111
|
-
<summary class="entry-summary">
|
|
112
|
-
<div class="rank">${index + 1}</div>
|
|
113
|
-
<div class="entry-main">
|
|
114
|
-
<div class="entry-top">
|
|
115
|
-
<div>
|
|
116
|
-
<h2>${escapeHtml(entry.username)}</h2>
|
|
117
|
-
<div class="subtitle">${escapeHtml(entry.title ?? 'Generated profile')}</div>
|
|
118
|
-
</div>
|
|
119
|
-
<div class="xp-block">
|
|
120
|
-
<strong>${entry.totalPoints.toLocaleString()} XP</strong>
|
|
121
|
-
<span>Level ${entry.level}</span>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
<div class="meta-row">
|
|
125
|
-
<span class="meta-pill">${entry.analyzedPRs} PRs</span>
|
|
126
|
-
<span class="meta-pill">${escapeHtml(formatLabel(entry.primaryArea))}</span>
|
|
127
|
-
<span class="meta-pill">Last activity ${escapeHtml(formatDate(entry.lastActivityAt))}</span>
|
|
128
|
-
</div>
|
|
129
|
-
<p class="summary preview">${escapeHtml(preview)}</p>
|
|
130
|
-
</div>
|
|
131
|
-
<div class="entry-side">
|
|
132
|
-
${reportAction}
|
|
133
|
-
<span class="expand-hint">More</span>
|
|
134
|
-
</div>
|
|
135
|
-
</summary>
|
|
136
|
-
<div class="entry-details">
|
|
137
|
-
<p class="summary full">${escapeHtml(fullSummary)}</p>
|
|
138
|
-
<div class="detail-grid">
|
|
139
|
-
<div class="detail-box">
|
|
140
|
-
<span class="detail-label">Profile</span>
|
|
141
|
-
<strong>${escapeHtml(entry.title ?? 'Generated profile')}</strong>
|
|
142
|
-
</div>
|
|
143
|
-
<div class="detail-box">
|
|
144
|
-
<span class="detail-label">Specialization</span>
|
|
145
|
-
<strong>${escapeHtml(formatLabel(entry.primaryArea))}</strong>
|
|
146
|
-
</div>
|
|
147
|
-
<div class="detail-box">
|
|
148
|
-
<span class="detail-label">Scope</span>
|
|
149
|
-
<strong>${escapeHtml(entry.storageKey)}</strong>
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
</details>`;
|
|
154
|
-
}
|
|
155
|
-
function renderEmptyState(message = 'No generated profiles found in ~/.quackscore yet.') {
|
|
156
|
-
return `<div class="empty">${escapeHtml(message)}</div>`;
|
|
157
|
-
}
|
|
158
|
-
function renderFilterDropdown(id, dataAttribute, selectedLabel, options) {
|
|
159
|
-
return `<div class="filter-dropdown" data-filter-dropdown>
|
|
160
|
-
<input type="hidden" id="${escapeHtml(id)}" value="${escapeHtml(options[0]?.value ?? '')}" ${dataAttribute}>
|
|
161
|
-
<button class="filter-trigger" type="button" aria-haspopup="listbox" aria-expanded="false" aria-controls="${escapeHtml(id)}-menu" data-filter-trigger>
|
|
162
|
-
<span class="filter-trigger-label" data-filter-label>${escapeHtml(selectedLabel)}</span>
|
|
163
|
-
</button>
|
|
164
|
-
<div class="filter-menu" id="${escapeHtml(id)}-menu" role="listbox" tabindex="-1" hidden data-filter-menu>
|
|
165
|
-
${options.map((option, index) => `<button class="filter-option${index === 0 ? ' is-selected' : ''}" type="button" role="option" data-filter-option data-value="${escapeHtml(option.value)}" aria-selected="${index === 0 ? 'true' : 'false'}">${escapeHtml(option.label)}</button>`).join('')}
|
|
166
|
-
</div>
|
|
167
|
-
</div>`;
|
|
168
|
-
}
|
|
169
41
|
function serializeForScript(value) {
|
|
170
42
|
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
171
43
|
}
|
|
172
|
-
export function generateLeaderboardHTML(profiles) {
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const initialPageCount = Math.max(1, Math.ceil(
|
|
178
|
-
const
|
|
44
|
+
export function generateLeaderboardHTML(profiles, options = {}) {
|
|
45
|
+
const teams = collectTeams(profiles);
|
|
46
|
+
const specializations = Array.from(new Set(profiles.flatMap((profile) => (profile.contributions ?? []).flatMap((contribution) => contribution.areas)))).sort((left, right) => formatLabel(left).localeCompare(formatLabel(right)));
|
|
47
|
+
const initialMode = options.initialMode === 'team' ? 'team' : 'profile';
|
|
48
|
+
const initialTeam = teams.find((team) => normalizeTeamKey(team) === normalizeTeamKey(options.initialTeam)) ?? 'all';
|
|
49
|
+
const initialPageCount = Math.max(1, Math.ceil(Math.max(profiles.length, 1) / 40));
|
|
50
|
+
const initialBoard = profiles.length === 0
|
|
51
|
+
? '<div class="empty">No generated profiles found in ~/.quackscore yet.</div>'
|
|
52
|
+
: '<div class="empty">Loading leaderboard…</div>';
|
|
179
53
|
return `<!DOCTYPE html>
|
|
180
54
|
<html lang="en">
|
|
181
55
|
<head>
|
|
@@ -183,678 +57,568 @@ export function generateLeaderboardHTML(profiles) {
|
|
|
183
57
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
184
58
|
<title>Quackscore Leaderboard</title>
|
|
185
59
|
<style>
|
|
186
|
-
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700;800&family=
|
|
60
|
+
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700;800&family=IBM+Plex+Mono:wght@400;500;700&family=Manrope:wght@400;500;700;800&display=swap');
|
|
187
61
|
|
|
188
62
|
:root {
|
|
189
63
|
color-scheme: dark;
|
|
190
|
-
--bg: #
|
|
191
|
-
--panel: rgba(16,
|
|
192
|
-
--panel-
|
|
193
|
-
--
|
|
194
|
-
--
|
|
195
|
-
--
|
|
196
|
-
--
|
|
197
|
-
--
|
|
198
|
-
--
|
|
199
|
-
--
|
|
64
|
+
--bg: #090b10;
|
|
65
|
+
--panel: rgba(16, 20, 28, 0.88);
|
|
66
|
+
--panel-strong: rgba(12, 16, 23, 0.96);
|
|
67
|
+
--text: #eef2ff;
|
|
68
|
+
--muted: #97a0b4;
|
|
69
|
+
--line: rgba(255, 255, 255, 0.1);
|
|
70
|
+
--accent: #f5b331;
|
|
71
|
+
--accent-soft: rgba(245, 179, 49, 0.14);
|
|
72
|
+
--forest: #8ca5ff;
|
|
73
|
+
--gold: #ffd36c;
|
|
74
|
+
--shadow: 0 24px 60px rgba(0, 0, 0, 0.34);
|
|
200
75
|
}
|
|
76
|
+
|
|
201
77
|
* { box-sizing: border-box; }
|
|
78
|
+
|
|
202
79
|
body {
|
|
203
80
|
margin: 0;
|
|
204
81
|
min-height: 100vh;
|
|
205
|
-
font-family: "
|
|
82
|
+
font-family: "Manrope", sans-serif;
|
|
206
83
|
color: var(--text);
|
|
207
84
|
background:
|
|
208
|
-
radial-gradient(circle at top left, rgba(245,179,49,0.
|
|
209
|
-
radial-gradient(circle at
|
|
210
|
-
linear-gradient(180deg, #
|
|
85
|
+
radial-gradient(circle at top left, rgba(245, 179, 49, 0.14), transparent 22%),
|
|
86
|
+
radial-gradient(circle at 84% 14%, rgba(140, 165, 255, 0.14), transparent 20%),
|
|
87
|
+
linear-gradient(180deg, #0c1018 0%, #06080d 100%);
|
|
211
88
|
}
|
|
89
|
+
|
|
212
90
|
.shell {
|
|
213
|
-
width: min(
|
|
91
|
+
width: min(1180px, calc(100vw - 28px));
|
|
214
92
|
margin: 0 auto;
|
|
215
|
-
padding:
|
|
93
|
+
padding: 28px 0 44px;
|
|
216
94
|
}
|
|
95
|
+
|
|
96
|
+
.hero,
|
|
97
|
+
.filters,
|
|
98
|
+
.board,
|
|
99
|
+
.pagination {
|
|
100
|
+
position: relative;
|
|
101
|
+
}
|
|
102
|
+
|
|
217
103
|
.hero {
|
|
218
104
|
display: grid;
|
|
219
105
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
220
|
-
gap:
|
|
221
|
-
margin-bottom:
|
|
106
|
+
gap: 18px;
|
|
107
|
+
margin-bottom: 18px;
|
|
222
108
|
padding: 26px 28px;
|
|
223
109
|
border-radius: 28px;
|
|
224
|
-
border: 1px solid rgba(245,179,49,0.16);
|
|
225
110
|
background:
|
|
226
|
-
|
|
227
|
-
radial-gradient(circle at
|
|
228
|
-
|
|
229
|
-
box-shadow:
|
|
230
|
-
}
|
|
231
|
-
.hero-copy {
|
|
232
|
-
display: grid;
|
|
233
|
-
gap: 10px;
|
|
111
|
+
linear-gradient(135deg, rgba(20,24,33,0.94), rgba(10,12,19,0.97)),
|
|
112
|
+
radial-gradient(circle at right top, rgba(245,179,49,0.12), transparent 32%);
|
|
113
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
114
|
+
box-shadow: var(--shadow);
|
|
234
115
|
}
|
|
116
|
+
|
|
235
117
|
.hero-topline {
|
|
236
|
-
display: flex;
|
|
118
|
+
display: inline-flex;
|
|
237
119
|
align-items: center;
|
|
238
|
-
gap:
|
|
239
|
-
font-family: "
|
|
240
|
-
letter-spacing: 0.18em;
|
|
241
|
-
text-transform: uppercase;
|
|
120
|
+
gap: 10px;
|
|
121
|
+
font-family: "IBM Plex Mono", monospace;
|
|
242
122
|
font-size: 0.78rem;
|
|
123
|
+
letter-spacing: 0.14em;
|
|
124
|
+
text-transform: uppercase;
|
|
243
125
|
color: var(--muted);
|
|
244
126
|
}
|
|
245
|
-
|
|
246
|
-
width: 48px;
|
|
247
|
-
height: 48px;
|
|
248
|
-
display: grid;
|
|
249
|
-
place-items: center;
|
|
250
|
-
border-radius: 16px;
|
|
251
|
-
background: linear-gradient(180deg, rgba(245,179,49,0.22), rgba(245,179,49,0.08));
|
|
252
|
-
border: 1px solid rgba(245,179,49,0.28);
|
|
253
|
-
font-size: 1.6rem;
|
|
254
|
-
box-shadow: 0 12px 28px rgba(0,0,0,0.28);
|
|
255
|
-
}
|
|
127
|
+
|
|
256
128
|
.hero h1 {
|
|
257
|
-
margin:
|
|
258
|
-
font-family: "Cinzel",
|
|
259
|
-
font-size: clamp(
|
|
260
|
-
line-height:
|
|
261
|
-
color: #
|
|
262
|
-
text-shadow: 0 0 18px rgba(245,179,49,0.18);
|
|
129
|
+
margin: 10px 0 8px;
|
|
130
|
+
font-family: "Cinzel", serif;
|
|
131
|
+
font-size: clamp(2.1rem, 4vw, 4rem);
|
|
132
|
+
line-height: 0.95;
|
|
133
|
+
color: #f3c45c;
|
|
263
134
|
}
|
|
135
|
+
|
|
264
136
|
.hero p {
|
|
265
137
|
margin: 0;
|
|
266
138
|
max-width: 760px;
|
|
267
|
-
color: var(--muted);
|
|
268
139
|
line-height: 1.7;
|
|
140
|
+
color: var(--muted);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.hero-stats {
|
|
144
|
+
min-width: 190px;
|
|
145
|
+
display: grid;
|
|
146
|
+
gap: 10px;
|
|
147
|
+
align-content: end;
|
|
269
148
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
padding: 16px 18px;
|
|
149
|
+
|
|
150
|
+
.hero-stat {
|
|
151
|
+
padding: 14px 16px;
|
|
274
152
|
border-radius: 18px;
|
|
275
|
-
border: 1px solid var(--border);
|
|
276
153
|
background: rgba(255,255,255,0.04);
|
|
277
|
-
|
|
154
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.hero-stat span {
|
|
158
|
+
display: block;
|
|
278
159
|
color: var(--muted);
|
|
279
|
-
font-
|
|
160
|
+
font-size: 0.78rem;
|
|
161
|
+
text-transform: uppercase;
|
|
162
|
+
letter-spacing: 0.1em;
|
|
163
|
+
font-family: "IBM Plex Mono", monospace;
|
|
280
164
|
}
|
|
281
|
-
|
|
165
|
+
|
|
166
|
+
.hero-stat strong {
|
|
282
167
|
display: block;
|
|
283
|
-
|
|
168
|
+
margin-top: 8px;
|
|
284
169
|
font-size: 1.6rem;
|
|
285
|
-
|
|
170
|
+
color: #dbe4ff;
|
|
286
171
|
}
|
|
172
|
+
|
|
287
173
|
.filters {
|
|
288
174
|
display: grid;
|
|
289
|
-
grid-template-columns: repeat(
|
|
175
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
290
176
|
gap: 14px;
|
|
291
|
-
margin-bottom:
|
|
292
|
-
padding:
|
|
293
|
-
border-radius:
|
|
294
|
-
|
|
295
|
-
|
|
177
|
+
margin-bottom: 18px;
|
|
178
|
+
padding: 20px 22px;
|
|
179
|
+
border-radius: 24px;
|
|
180
|
+
background: var(--panel);
|
|
181
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
182
|
+
box-shadow: var(--shadow);
|
|
296
183
|
}
|
|
184
|
+
|
|
297
185
|
.filter-group {
|
|
298
186
|
display: grid;
|
|
299
187
|
gap: 8px;
|
|
300
188
|
}
|
|
189
|
+
|
|
190
|
+
.filter-group.wide {
|
|
191
|
+
grid-column: span 2;
|
|
192
|
+
}
|
|
193
|
+
|
|
301
194
|
.filter-label {
|
|
195
|
+
color: var(--muted);
|
|
302
196
|
font-size: 0.76rem;
|
|
303
197
|
letter-spacing: 0.12em;
|
|
304
198
|
text-transform: uppercase;
|
|
305
|
-
|
|
306
|
-
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
199
|
+
font-family: "IBM Plex Mono", monospace;
|
|
307
200
|
}
|
|
308
|
-
|
|
309
|
-
|
|
201
|
+
|
|
202
|
+
.mode-toggle {
|
|
203
|
+
display: inline-flex;
|
|
204
|
+
gap: 8px;
|
|
310
205
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
206
|
+
|
|
207
|
+
.mode-button,
|
|
208
|
+
.select,
|
|
209
|
+
.date-input,
|
|
210
|
+
.page-button,
|
|
211
|
+
.member-link,
|
|
212
|
+
.empty-pill {
|
|
213
|
+
border-radius: 14px;
|
|
214
|
+
border: 1px solid var(--line);
|
|
215
|
+
background: rgba(255,255,255,0.04);
|
|
318
216
|
color: var(--text);
|
|
319
217
|
font: inherit;
|
|
320
|
-
text-align: left;
|
|
321
|
-
cursor: pointer;
|
|
322
|
-
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
|
|
323
|
-
transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
|
|
324
218
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
.filter-trigger:focus-visible,
|
|
330
|
-
.filter-dropdown.is-open .filter-trigger {
|
|
331
|
-
outline: none;
|
|
332
|
-
border-color: rgba(125,162,255,0.42);
|
|
333
|
-
background:
|
|
334
|
-
radial-gradient(circle at right top, rgba(125,162,255,0.14), transparent 42%),
|
|
335
|
-
linear-gradient(180deg, rgba(36,36,36,0.98), rgba(24,24,24,0.98));
|
|
336
|
-
box-shadow:
|
|
337
|
-
0 0 0 1px rgba(125,162,255,0.18),
|
|
338
|
-
0 18px 38px rgba(0,0,0,0.26);
|
|
339
|
-
}
|
|
340
|
-
.filter-trigger::before {
|
|
341
|
-
content: '';
|
|
342
|
-
position: absolute;
|
|
343
|
-
top: 50%;
|
|
344
|
-
right: 50px;
|
|
345
|
-
width: 1px;
|
|
346
|
-
height: 22px;
|
|
347
|
-
border-radius: 999px;
|
|
348
|
-
background: rgba(255,255,255,0.1);
|
|
349
|
-
transform: translateY(-50%);
|
|
350
|
-
pointer-events: none;
|
|
351
|
-
}
|
|
352
|
-
.filter-trigger::after {
|
|
353
|
-
content: '';
|
|
354
|
-
position: absolute;
|
|
355
|
-
top: 50%;
|
|
356
|
-
right: 22px;
|
|
357
|
-
width: 8px;
|
|
358
|
-
height: 8px;
|
|
359
|
-
border-right: 2px solid rgba(255,255,255,0.78);
|
|
360
|
-
border-bottom: 2px solid rgba(255,255,255,0.78);
|
|
361
|
-
transform: translateY(-65%) rotate(45deg);
|
|
362
|
-
pointer-events: none;
|
|
363
|
-
}
|
|
364
|
-
.filter-dropdown.is-open .filter-trigger::after {
|
|
365
|
-
transform: translateY(-20%) rotate(-135deg);
|
|
366
|
-
}
|
|
367
|
-
.filter-trigger-label {
|
|
368
|
-
display: block;
|
|
369
|
-
overflow: hidden;
|
|
370
|
-
text-overflow: ellipsis;
|
|
371
|
-
white-space: nowrap;
|
|
372
|
-
}
|
|
373
|
-
.filter-menu {
|
|
374
|
-
position: absolute;
|
|
375
|
-
top: calc(100% + 10px);
|
|
376
|
-
left: 0;
|
|
377
|
-
right: 0;
|
|
378
|
-
z-index: 20;
|
|
379
|
-
display: grid;
|
|
380
|
-
gap: 4px;
|
|
381
|
-
padding: 8px;
|
|
382
|
-
border-radius: 18px;
|
|
383
|
-
border: 1px solid rgba(125,162,255,0.18);
|
|
384
|
-
background:
|
|
385
|
-
radial-gradient(circle at top right, rgba(125,162,255,0.14), transparent 34%),
|
|
386
|
-
linear-gradient(180deg, rgba(20,20,20,0.98), rgba(12,12,12,0.98));
|
|
387
|
-
box-shadow:
|
|
388
|
-
0 22px 44px rgba(0,0,0,0.46),
|
|
389
|
-
inset 0 1px 0 rgba(255,255,255,0.04);
|
|
390
|
-
}
|
|
391
|
-
.filter-menu[hidden] {
|
|
392
|
-
display: none;
|
|
393
|
-
}
|
|
394
|
-
.filter-option {
|
|
395
|
-
width: 100%;
|
|
396
|
-
padding: 11px 14px;
|
|
397
|
-
border: 0;
|
|
398
|
-
border-radius: 12px;
|
|
399
|
-
background: transparent;
|
|
400
|
-
color: #ececec;
|
|
401
|
-
font: inherit;
|
|
402
|
-
text-align: left;
|
|
219
|
+
|
|
220
|
+
.mode-button {
|
|
221
|
+
padding: 12px 14px;
|
|
403
222
|
cursor: pointer;
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
.filter-option:hover,
|
|
407
|
-
.filter-option:focus-visible {
|
|
408
|
-
outline: none;
|
|
409
|
-
background: rgba(255,255,255,0.07);
|
|
410
|
-
color: #ffffff;
|
|
411
|
-
}
|
|
412
|
-
.filter-option.is-selected {
|
|
413
|
-
background: linear-gradient(180deg, rgba(125,162,255,0.28), rgba(91,122,219,0.24));
|
|
414
|
-
color: #ffffff;
|
|
415
|
-
box-shadow: inset 0 0 0 1px rgba(125,162,255,0.18);
|
|
223
|
+
font-weight: 700;
|
|
416
224
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
.filter-summary strong {
|
|
423
|
-
color: var(--amber);
|
|
225
|
+
|
|
226
|
+
.mode-button.is-active {
|
|
227
|
+
background: linear-gradient(135deg, var(--accent-soft), rgba(255,255,255,0.02));
|
|
228
|
+
border-color: rgba(245, 179, 49, 0.34);
|
|
229
|
+
color: #ffd36c;
|
|
424
230
|
}
|
|
425
|
-
|
|
231
|
+
|
|
232
|
+
.select,
|
|
233
|
+
.date-input {
|
|
426
234
|
width: 100%;
|
|
427
|
-
padding: 14px
|
|
428
|
-
border-radius: 16px;
|
|
429
|
-
border: 1px solid rgba(255,255,255,0.1);
|
|
430
|
-
background: linear-gradient(180deg, rgba(34,34,34,0.96), rgba(25,25,25,0.96));
|
|
431
|
-
color: var(--text);
|
|
432
|
-
font: inherit;
|
|
433
|
-
cursor: pointer;
|
|
434
|
-
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
|
|
435
|
-
transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
|
|
436
|
-
}
|
|
437
|
-
.filter-date-input:hover {
|
|
438
|
-
border-color: rgba(125,162,255,0.22);
|
|
439
|
-
background: linear-gradient(180deg, rgba(38,38,38,0.98), rgba(28,28,28,0.98));
|
|
440
|
-
}
|
|
441
|
-
.filter-date-input:focus {
|
|
442
|
-
outline: none;
|
|
443
|
-
border-color: rgba(125,162,255,0.42);
|
|
444
|
-
box-shadow: 0 0 0 1px rgba(125,162,255,0.18), 0 18px 38px rgba(0,0,0,0.26);
|
|
445
|
-
}
|
|
446
|
-
.filter-date-input::-webkit-calendar-picker-indicator {
|
|
447
|
-
filter: invert(0.8) opacity(0.7);
|
|
448
|
-
cursor: pointer;
|
|
449
|
-
}
|
|
450
|
-
.modal-overlay {
|
|
451
|
-
position: fixed;
|
|
452
|
-
inset: 0;
|
|
453
|
-
z-index: 100;
|
|
454
|
-
display: grid;
|
|
455
|
-
place-items: center;
|
|
456
|
-
background: rgba(0,0,0,0.6);
|
|
457
|
-
backdrop-filter: blur(4px);
|
|
458
|
-
}
|
|
459
|
-
.modal-overlay[hidden] {
|
|
460
|
-
display: none;
|
|
461
|
-
}
|
|
462
|
-
.modal {
|
|
463
|
-
width: min(440px, calc(100vw - 40px));
|
|
464
|
-
padding: 28px;
|
|
465
|
-
border-radius: 24px;
|
|
466
|
-
border: 1px solid rgba(125,162,255,0.22);
|
|
467
|
-
background:
|
|
468
|
-
radial-gradient(circle at top right, rgba(125,162,255,0.1), transparent 34%),
|
|
469
|
-
linear-gradient(180deg, rgba(22,22,22,0.99), rgba(14,14,14,0.99));
|
|
470
|
-
box-shadow: 0 32px 64px rgba(0,0,0,0.56);
|
|
471
|
-
}
|
|
472
|
-
.modal h3 {
|
|
473
|
-
margin: 0 0 20px;
|
|
474
|
-
font-family: "Cinzel", ui-serif, Georgia, serif;
|
|
475
|
-
font-size: 1.1rem;
|
|
476
|
-
color: var(--amber);
|
|
235
|
+
padding: 12px 14px;
|
|
477
236
|
}
|
|
478
|
-
|
|
237
|
+
|
|
238
|
+
.date-range {
|
|
479
239
|
display: grid;
|
|
480
240
|
grid-template-columns: 1fr 1fr;
|
|
481
|
-
gap: 14px;
|
|
482
|
-
margin-bottom: 22px;
|
|
483
|
-
}
|
|
484
|
-
.modal-field {
|
|
485
|
-
display: grid;
|
|
486
|
-
gap: 8px;
|
|
487
|
-
}
|
|
488
|
-
.modal-actions {
|
|
489
|
-
display: flex;
|
|
490
|
-
justify-content: flex-end;
|
|
491
241
|
gap: 10px;
|
|
492
242
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
background: rgba(255,255,255,0.05);
|
|
498
|
-
color: var(--text);
|
|
499
|
-
font: inherit;
|
|
500
|
-
font-size: 0.9rem;
|
|
501
|
-
cursor: pointer;
|
|
502
|
-
transition: background 140ms ease, border-color 140ms ease;
|
|
503
|
-
}
|
|
504
|
-
.modal-btn:hover {
|
|
505
|
-
background: rgba(255,255,255,0.09);
|
|
506
|
-
}
|
|
507
|
-
.modal-btn.primary {
|
|
508
|
-
border-color: rgba(125,162,255,0.36);
|
|
509
|
-
background: linear-gradient(180deg, rgba(125,162,255,0.22), rgba(91,122,219,0.18));
|
|
510
|
-
color: #dbe5ff;
|
|
511
|
-
font-weight: 600;
|
|
243
|
+
|
|
244
|
+
.filter-summary {
|
|
245
|
+
grid-column: 1 / -1;
|
|
246
|
+
color: var(--muted);
|
|
512
247
|
}
|
|
513
|
-
|
|
514
|
-
|
|
248
|
+
|
|
249
|
+
.filter-summary strong {
|
|
250
|
+
color: var(--forest);
|
|
515
251
|
}
|
|
252
|
+
|
|
516
253
|
.board {
|
|
517
254
|
display: grid;
|
|
518
|
-
gap:
|
|
255
|
+
gap: 14px;
|
|
519
256
|
}
|
|
257
|
+
|
|
520
258
|
.entry {
|
|
521
|
-
border-radius:
|
|
522
|
-
|
|
523
|
-
|
|
259
|
+
border-radius: 24px;
|
|
260
|
+
background: var(--panel-strong);
|
|
261
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
262
|
+
box-shadow: var(--shadow);
|
|
524
263
|
overflow: hidden;
|
|
525
264
|
}
|
|
526
|
-
|
|
265
|
+
|
|
266
|
+
.entry summary {
|
|
527
267
|
list-style: none;
|
|
528
268
|
cursor: pointer;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.entry summary::-webkit-details-marker,
|
|
272
|
+
.entry summary::marker {
|
|
273
|
+
display: none;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.entry-head {
|
|
529
277
|
display: grid;
|
|
530
|
-
grid-template-columns:
|
|
278
|
+
grid-template-columns: 66px minmax(0, 1fr) auto;
|
|
531
279
|
gap: 16px;
|
|
532
280
|
align-items: center;
|
|
533
|
-
padding:
|
|
534
|
-
}
|
|
535
|
-
.entry-summary::-webkit-details-marker,
|
|
536
|
-
.entry-summary::marker {
|
|
537
|
-
display: none;
|
|
281
|
+
padding: 18px 20px;
|
|
538
282
|
}
|
|
283
|
+
|
|
539
284
|
.rank {
|
|
285
|
+
width: 52px;
|
|
286
|
+
height: 52px;
|
|
540
287
|
display: grid;
|
|
541
288
|
place-items: center;
|
|
542
|
-
|
|
543
|
-
height: 48px;
|
|
544
|
-
border-radius: 16px;
|
|
545
|
-
background: linear-gradient(180deg, rgba(245,179,49,0.16), rgba(179,128,255,0.10));
|
|
546
|
-
color: var(--amber);
|
|
289
|
+
border-radius: 18px;
|
|
547
290
|
font-weight: 800;
|
|
548
|
-
font-size: 1.
|
|
549
|
-
font-family: "Cinzel",
|
|
291
|
+
font-size: 1.1rem;
|
|
292
|
+
font-family: "Cinzel", serif;
|
|
293
|
+
color: #ffd36c;
|
|
294
|
+
background: linear-gradient(135deg, rgba(245,179,49,0.14), rgba(255,255,255,0.04));
|
|
550
295
|
}
|
|
296
|
+
|
|
551
297
|
.entry-top {
|
|
552
298
|
display: flex;
|
|
553
|
-
align-items: start;
|
|
554
299
|
justify-content: space-between;
|
|
555
300
|
gap: 16px;
|
|
301
|
+
align-items: flex-start;
|
|
556
302
|
}
|
|
303
|
+
|
|
557
304
|
.entry-top h2 {
|
|
558
305
|
margin: 0;
|
|
559
306
|
font-size: 1.18rem;
|
|
560
307
|
}
|
|
308
|
+
|
|
561
309
|
.subtitle {
|
|
562
310
|
margin-top: 4px;
|
|
563
|
-
color: var(--
|
|
564
|
-
font-size: 0.
|
|
311
|
+
color: var(--muted);
|
|
312
|
+
font-size: 0.92rem;
|
|
565
313
|
}
|
|
566
|
-
|
|
314
|
+
|
|
315
|
+
.score {
|
|
567
316
|
text-align: right;
|
|
568
317
|
white-space: nowrap;
|
|
569
|
-
font-family: "
|
|
318
|
+
font-family: "IBM Plex Mono", monospace;
|
|
570
319
|
}
|
|
571
|
-
|
|
320
|
+
|
|
321
|
+
.score strong {
|
|
572
322
|
display: block;
|
|
573
|
-
color: var(--
|
|
323
|
+
color: var(--gold);
|
|
574
324
|
font-size: 1.05rem;
|
|
575
325
|
}
|
|
576
|
-
|
|
577
|
-
.
|
|
578
|
-
.
|
|
579
|
-
.
|
|
580
|
-
.disabled {
|
|
581
|
-
color: var(--muted);
|
|
582
|
-
}
|
|
583
|
-
.meta-row {
|
|
326
|
+
|
|
327
|
+
.meta-row,
|
|
328
|
+
.detail-row,
|
|
329
|
+
.member-list {
|
|
584
330
|
display: flex;
|
|
585
331
|
flex-wrap: wrap;
|
|
586
332
|
gap: 8px;
|
|
587
|
-
margin-top: 10px;
|
|
588
333
|
}
|
|
589
|
-
|
|
334
|
+
|
|
335
|
+
.meta-pill,
|
|
336
|
+
.empty-pill {
|
|
590
337
|
padding: 5px 9px;
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
338
|
+
font-family: "IBM Plex Mono", monospace;
|
|
339
|
+
font-size: 0.76rem;
|
|
340
|
+
color: var(--muted);
|
|
341
|
+
background: rgba(255, 255, 255, 0.05);
|
|
595
342
|
}
|
|
343
|
+
|
|
596
344
|
.summary {
|
|
597
|
-
margin:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
.summary.preview {
|
|
602
|
-
display: -webkit-box;
|
|
603
|
-
-webkit-line-clamp: 2;
|
|
604
|
-
-webkit-box-orient: vertical;
|
|
605
|
-
overflow: hidden;
|
|
345
|
+
margin: 12px 0 0;
|
|
346
|
+
color: var(--muted);
|
|
347
|
+
line-height: 1.7;
|
|
606
348
|
}
|
|
349
|
+
|
|
607
350
|
.entry-side {
|
|
608
351
|
display: flex;
|
|
609
352
|
align-items: center;
|
|
610
353
|
gap: 10px;
|
|
611
354
|
}
|
|
612
|
-
|
|
355
|
+
|
|
356
|
+
.member-link {
|
|
613
357
|
display: inline-flex;
|
|
614
358
|
align-items: center;
|
|
615
359
|
justify-content: center;
|
|
616
|
-
|
|
617
|
-
padding: 10px 13px;
|
|
618
|
-
border-radius: 12px;
|
|
619
|
-
border: 1px solid rgba(125,162,255,0.22);
|
|
620
|
-
background: rgba(125,162,255,0.09);
|
|
621
|
-
color: #dbe5ff;
|
|
360
|
+
padding: 10px 12px;
|
|
622
361
|
text-decoration: none;
|
|
623
|
-
font-weight:
|
|
624
|
-
|
|
362
|
+
font-weight: 700;
|
|
363
|
+
color: #dbe4ff;
|
|
625
364
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
365
|
+
|
|
366
|
+
.member-link.disabled {
|
|
367
|
+
color: var(--muted);
|
|
368
|
+
background: rgba(255, 255, 255, 0.04);
|
|
629
369
|
}
|
|
370
|
+
|
|
630
371
|
.expand-hint {
|
|
631
|
-
min-width: 56px;
|
|
632
|
-
text-align: center;
|
|
633
|
-
padding: 8px 10px;
|
|
634
|
-
border-radius: 10px;
|
|
635
|
-
background: rgba(255,255,255,0.04);
|
|
636
372
|
color: var(--muted);
|
|
637
|
-
font-family: "
|
|
638
|
-
font-size: 0.
|
|
373
|
+
font-family: "IBM Plex Mono", monospace;
|
|
374
|
+
font-size: 0.75rem;
|
|
639
375
|
text-transform: uppercase;
|
|
640
|
-
letter-spacing: 0.08em;
|
|
641
|
-
}
|
|
642
|
-
.entry[open] .expand-hint {
|
|
643
|
-
color: var(--green);
|
|
644
376
|
}
|
|
377
|
+
|
|
645
378
|
.entry-details {
|
|
646
|
-
padding: 0
|
|
647
|
-
border-top: 1px solid rgba(255,255,255,0.06);
|
|
648
|
-
background: rgba(
|
|
379
|
+
padding: 0 20px 20px 102px;
|
|
380
|
+
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
381
|
+
background: rgba(255, 255, 255, 0.02);
|
|
649
382
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
653
|
-
gap: 10px;
|
|
383
|
+
|
|
384
|
+
.detail-row {
|
|
654
385
|
margin-top: 14px;
|
|
655
386
|
}
|
|
656
|
-
|
|
387
|
+
|
|
388
|
+
.detail-card {
|
|
389
|
+
min-width: 180px;
|
|
657
390
|
padding: 12px 14px;
|
|
658
|
-
border-radius:
|
|
659
|
-
border: 1px solid rgba(255,255,255,0.
|
|
391
|
+
border-radius: 16px;
|
|
392
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
660
393
|
background: rgba(255,255,255,0.03);
|
|
661
394
|
}
|
|
662
|
-
|
|
395
|
+
|
|
396
|
+
.detail-card span {
|
|
663
397
|
display: block;
|
|
664
|
-
margin-bottom: 6px;
|
|
665
398
|
color: var(--muted);
|
|
666
399
|
font-size: 0.74rem;
|
|
667
|
-
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
668
400
|
text-transform: uppercase;
|
|
669
401
|
letter-spacing: 0.12em;
|
|
402
|
+
font-family: "IBM Plex Mono", monospace;
|
|
670
403
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
404
|
+
|
|
405
|
+
.detail-card strong {
|
|
406
|
+
display: block;
|
|
407
|
+
margin-top: 8px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.member-list {
|
|
411
|
+
margin-top: 14px;
|
|
674
412
|
}
|
|
413
|
+
|
|
414
|
+
.member-tag {
|
|
415
|
+
display: inline-flex;
|
|
416
|
+
align-items: center;
|
|
417
|
+
gap: 8px;
|
|
418
|
+
padding: 8px 10px;
|
|
419
|
+
border-radius: 999px;
|
|
420
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
421
|
+
background: rgba(255,255,255,0.04);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.member-tag a {
|
|
425
|
+
color: #dbe4ff;
|
|
426
|
+
text-decoration: none;
|
|
427
|
+
font-weight: 700;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.member-tag span {
|
|
431
|
+
color: var(--muted);
|
|
432
|
+
font-size: 0.82rem;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.empty {
|
|
436
|
+
padding: 28px;
|
|
437
|
+
border-radius: 24px;
|
|
438
|
+
background: var(--panel);
|
|
439
|
+
border: 1px dashed rgba(255, 255, 255, 0.14);
|
|
440
|
+
color: var(--muted);
|
|
441
|
+
text-align: center;
|
|
442
|
+
}
|
|
443
|
+
|
|
675
444
|
.pagination {
|
|
676
445
|
display: flex;
|
|
677
446
|
align-items: center;
|
|
678
|
-
justify-content:
|
|
679
|
-
gap:
|
|
447
|
+
justify-content: center;
|
|
448
|
+
gap: 14px;
|
|
680
449
|
margin-top: 18px;
|
|
681
|
-
padding-top: 18px;
|
|
682
|
-
}
|
|
683
|
-
.pagination[hidden] {
|
|
684
|
-
display: none;
|
|
685
450
|
}
|
|
686
|
-
|
|
687
|
-
|
|
451
|
+
|
|
452
|
+
.page-button {
|
|
688
453
|
padding: 10px 14px;
|
|
689
|
-
border-radius: 12px;
|
|
690
|
-
border: 1px solid rgba(255,255,255,0.08);
|
|
691
|
-
background: rgba(255,255,255,0.04);
|
|
692
|
-
color: var(--text);
|
|
693
|
-
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
694
454
|
cursor: pointer;
|
|
695
455
|
}
|
|
696
|
-
|
|
697
|
-
|
|
456
|
+
|
|
457
|
+
.page-button:disabled {
|
|
458
|
+
opacity: 0.4;
|
|
698
459
|
cursor: not-allowed;
|
|
699
460
|
}
|
|
461
|
+
|
|
700
462
|
.page-status {
|
|
701
463
|
color: var(--muted);
|
|
702
|
-
font-family: "
|
|
703
|
-
text-align: center;
|
|
704
|
-
flex: 1 1 auto;
|
|
464
|
+
font-family: "IBM Plex Mono", monospace;
|
|
705
465
|
}
|
|
706
|
-
|
|
707
|
-
|
|
466
|
+
|
|
467
|
+
.hidden {
|
|
468
|
+
display: none !important;
|
|
708
469
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
470
|
+
|
|
471
|
+
@media (max-width: 980px) {
|
|
472
|
+
.filters {
|
|
473
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.filter-group.wide {
|
|
477
|
+
grid-column: span 2;
|
|
478
|
+
}
|
|
715
479
|
}
|
|
716
|
-
|
|
480
|
+
|
|
481
|
+
@media (max-width: 720px) {
|
|
482
|
+
.shell {
|
|
483
|
+
width: min(100vw - 18px, 1180px);
|
|
484
|
+
padding-top: 18px;
|
|
485
|
+
}
|
|
486
|
+
|
|
717
487
|
.hero,
|
|
718
488
|
.filters,
|
|
719
|
-
.entry-
|
|
720
|
-
.detail-grid {
|
|
489
|
+
.entry-head {
|
|
721
490
|
grid-template-columns: 1fr;
|
|
722
491
|
}
|
|
723
|
-
|
|
724
|
-
min-width: 0;
|
|
725
|
-
}
|
|
726
|
-
.entry-side {
|
|
727
|
-
justify-content: start;
|
|
728
|
-
}
|
|
492
|
+
|
|
729
493
|
.entry-details {
|
|
730
|
-
padding-left:
|
|
494
|
+
padding-left: 20px;
|
|
731
495
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
.
|
|
735
|
-
|
|
496
|
+
|
|
497
|
+
.entry-side,
|
|
498
|
+
.score {
|
|
499
|
+
justify-content: flex-start;
|
|
500
|
+
text-align: left;
|
|
736
501
|
}
|
|
737
|
-
|
|
738
|
-
|
|
502
|
+
|
|
503
|
+
.date-range {
|
|
504
|
+
grid-template-columns: 1fr;
|
|
739
505
|
}
|
|
740
506
|
}
|
|
741
507
|
</style>
|
|
742
508
|
</head>
|
|
743
509
|
<body>
|
|
744
510
|
<div class="shell">
|
|
745
|
-
<
|
|
746
|
-
<div
|
|
747
|
-
<div class="hero-topline"
|
|
511
|
+
<section class="hero">
|
|
512
|
+
<div>
|
|
513
|
+
<div class="hero-topline">Guild Hall Rankings</div>
|
|
748
514
|
<h1>Quackscore Leaderboard</h1>
|
|
749
|
-
<p>
|
|
515
|
+
<p>Compare saved Quackscore profiles across scopes, narrow the board to a single team, or switch to team rankings to see which group is shipping the strongest work.</p>
|
|
516
|
+
</div>
|
|
517
|
+
<div class="hero-stats">
|
|
518
|
+
<div class="hero-stat">
|
|
519
|
+
<span>Saved Profiles</span>
|
|
520
|
+
<strong data-profile-count>${profiles.length}</strong>
|
|
521
|
+
</div>
|
|
522
|
+
<div class="hero-stat">
|
|
523
|
+
<span>Tagged Teams</span>
|
|
524
|
+
<strong>${teams.length}</strong>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
</section>
|
|
528
|
+
|
|
529
|
+
<section class="filters">
|
|
530
|
+
<div class="filter-group wide">
|
|
531
|
+
<div class="filter-label">Mode</div>
|
|
532
|
+
<div class="mode-toggle">
|
|
533
|
+
<button class="mode-button${initialMode === 'profile' ? ' is-active' : ''}" type="button" data-mode="profile">Profiles</button>
|
|
534
|
+
<button class="mode-button${initialMode === 'team' ? ' is-active' : ''}" type="button" data-mode="team">Teams</button>
|
|
535
|
+
</div>
|
|
750
536
|
</div>
|
|
751
|
-
|
|
752
|
-
</header>
|
|
753
|
-
<section class="filters" aria-label="Leaderboard filters">
|
|
537
|
+
|
|
754
538
|
<div class="filter-group">
|
|
755
|
-
<label class="filter-label" for="time-filter">Time</label>
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
539
|
+
<label class="filter-label" for="time-filter">Time Window</label>
|
|
540
|
+
<select class="select" id="time-filter" data-time-filter>
|
|
541
|
+
<option value="all">All time</option>
|
|
542
|
+
<option value="last_week">Last week</option>
|
|
543
|
+
<option value="last_month">Last month</option>
|
|
544
|
+
<option value="last_3_months">Last 3 months</option>
|
|
545
|
+
<option value="last_6_months">Last 6 months</option>
|
|
546
|
+
<option value="last_year">Last year</option>
|
|
547
|
+
<option value="custom">Custom range</option>
|
|
548
|
+
</select>
|
|
765
549
|
</div>
|
|
550
|
+
|
|
766
551
|
<div class="filter-group">
|
|
767
552
|
<label class="filter-label" for="specialization-filter">Specialization</label>
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
553
|
+
<select class="select" id="specialization-filter" data-specialization-filter>
|
|
554
|
+
<option value="all">All specializations</option>
|
|
555
|
+
${specializations.map((specialization) => `<option value="${escapeHtml(specialization)}">${escapeHtml(formatLabel(specialization))}</option>`).join('')}
|
|
556
|
+
</select>
|
|
772
557
|
</div>
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
</div>
|
|
781
|
-
</div>
|
|
782
|
-
<div class="modal-overlay" id="date-range-modal" hidden>
|
|
783
|
-
<div class="modal">
|
|
784
|
-
<h3>Custom Date Range</h3>
|
|
785
|
-
<div class="modal-fields">
|
|
786
|
-
<div class="modal-field">
|
|
787
|
-
<label class="filter-label" for="modal-from">From</label>
|
|
788
|
-
<input class="filter-date-input" type="date" id="modal-from" data-modal-from>
|
|
789
|
-
</div>
|
|
790
|
-
<div class="modal-field">
|
|
791
|
-
<label class="filter-label" for="modal-to">To</label>
|
|
792
|
-
<input class="filter-date-input" type="date" id="modal-to" data-modal-to>
|
|
793
|
-
</div>
|
|
558
|
+
|
|
559
|
+
<div class="filter-group">
|
|
560
|
+
<label class="filter-label" for="team-filter">Team Filter</label>
|
|
561
|
+
<select class="select" id="team-filter" data-team-filter>
|
|
562
|
+
<option value="all"${initialTeam === 'all' ? ' selected' : ''}>All teams</option>
|
|
563
|
+
${teams.map((team) => `<option value="${escapeHtml(team)}"${initialTeam === team ? ' selected' : ''}>${escapeHtml(team)}</option>`).join('')}
|
|
564
|
+
</select>
|
|
794
565
|
</div>
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
<
|
|
566
|
+
|
|
567
|
+
<div class="filter-group wide hidden" data-custom-range>
|
|
568
|
+
<div class="filter-label">Custom Range</div>
|
|
569
|
+
<div class="date-range">
|
|
570
|
+
<input class="date-input" type="date" data-from-date>
|
|
571
|
+
<input class="date-input" type="date" data-to-date>
|
|
572
|
+
</div>
|
|
798
573
|
</div>
|
|
574
|
+
|
|
575
|
+
<div class="filter-summary" data-filter-summary><strong>${profiles.length}</strong> saved profiles ready for comparison.</div>
|
|
576
|
+
</section>
|
|
577
|
+
|
|
578
|
+
<section class="board" data-board>
|
|
579
|
+
${initialBoard}
|
|
580
|
+
</section>
|
|
581
|
+
|
|
582
|
+
<div class="pagination" data-pagination hidden>
|
|
583
|
+
<button class="page-button" type="button" data-nav="prev">Previous</button>
|
|
584
|
+
<div class="page-status" data-page-status>Page <strong>1</strong> of ${initialPageCount}</div>
|
|
585
|
+
<button class="page-button" type="button" data-nav="next">Next</button>
|
|
799
586
|
</div>
|
|
800
587
|
</div>
|
|
588
|
+
|
|
801
589
|
<script id="leaderboard-data" type="application/json">${serializeForScript(profiles)}</script>
|
|
590
|
+
<script id="leaderboard-config" type="application/json">${serializeForScript({ initialMode, initialTeam })}</script>
|
|
802
591
|
<script>
|
|
803
|
-
(function() {
|
|
804
|
-
const PROFILES_PER_PAGE =
|
|
592
|
+
(function () {
|
|
593
|
+
const PROFILES_PER_PAGE = 40;
|
|
805
594
|
const DISPLAY_LABELS = ${serializeForScript(DISPLAY_LABELS)};
|
|
806
595
|
const LEVEL_THRESHOLDS = ${serializeForScript(LEVEL_THRESHOLDS)};
|
|
807
596
|
const MAX_LEVEL = ${MAX_LEVEL};
|
|
808
|
-
const TIME_LABELS = {
|
|
809
|
-
all: 'all time',
|
|
810
|
-
last_week: 'the last week',
|
|
811
|
-
last_month: 'the last month',
|
|
812
|
-
last_3_months: 'the last 3 months',
|
|
813
|
-
last_6_months: 'the last 6 months',
|
|
814
|
-
last_year: 'the last year',
|
|
815
|
-
};
|
|
816
597
|
|
|
817
598
|
const dataElement = document.getElementById('leaderboard-data');
|
|
599
|
+
const configElement = document.getElementById('leaderboard-config');
|
|
818
600
|
const board = document.querySelector('[data-board]');
|
|
819
|
-
const
|
|
820
|
-
const specializationFilter = document.querySelector('[data-specialization-filter]');
|
|
821
|
-
const count = document.querySelector('[data-profile-count]');
|
|
822
|
-
const filterSummary = document.querySelector('[data-filter-summary]');
|
|
601
|
+
const summary = document.querySelector('[data-filter-summary]');
|
|
823
602
|
const pagination = document.querySelector('[data-pagination]');
|
|
603
|
+
const pageStatus = document.querySelector('[data-page-status]');
|
|
824
604
|
const prevButton = document.querySelector('[data-nav="prev"]');
|
|
825
605
|
const nextButton = document.querySelector('[data-nav="next"]');
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
const
|
|
830
|
-
const
|
|
831
|
-
const
|
|
832
|
-
const
|
|
606
|
+
const timeFilter = document.querySelector('[data-time-filter]');
|
|
607
|
+
const specializationFilter = document.querySelector('[data-specialization-filter]');
|
|
608
|
+
const teamFilter = document.querySelector('[data-team-filter]');
|
|
609
|
+
const customRange = document.querySelector('[data-custom-range]');
|
|
610
|
+
const fromDate = document.querySelector('[data-from-date]');
|
|
611
|
+
const toDate = document.querySelector('[data-to-date]');
|
|
612
|
+
const modeButtons = Array.from(document.querySelectorAll('[data-mode]'));
|
|
833
613
|
|
|
834
|
-
if (!dataElement || !
|
|
614
|
+
if (!dataElement || !configElement || !board || !summary || !pagination || !pageStatus || !prevButton || !nextButton || !timeFilter || !specializationFilter || !teamFilter || !customRange || !fromDate || !toDate || modeButtons.length === 0) {
|
|
835
615
|
return;
|
|
836
616
|
}
|
|
837
617
|
|
|
838
618
|
const profiles = JSON.parse(dataElement.textContent || '[]');
|
|
619
|
+
const config = JSON.parse(configElement.textContent || '{}');
|
|
620
|
+
let currentMode = config.initialMode || 'profile';
|
|
839
621
|
let currentPage = 1;
|
|
840
|
-
let customFrom = '';
|
|
841
|
-
let customTo = '';
|
|
842
|
-
let previousTimeValue = 'all';
|
|
843
|
-
const timeDropdown = setupDropdown(timeFilter);
|
|
844
|
-
const dropdowns = [timeDropdown, setupDropdown(specializationFilter)].filter(Boolean);
|
|
845
|
-
|
|
846
|
-
document.addEventListener('click', (event) => {
|
|
847
|
-
dropdowns.forEach((instance) => {
|
|
848
|
-
if (!instance.dropdown.contains(event.target)) {
|
|
849
|
-
instance.close();
|
|
850
|
-
}
|
|
851
|
-
});
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
document.addEventListener('keydown', (event) => {
|
|
855
|
-
if (event.key !== 'Escape') return;
|
|
856
|
-
dropdowns.forEach((instance) => instance.close());
|
|
857
|
-
});
|
|
858
622
|
|
|
859
623
|
const escapeHtml = (value) => String(value)
|
|
860
624
|
.replace(/&/g, '&')
|
|
@@ -865,7 +629,7 @@ body {
|
|
|
865
629
|
|
|
866
630
|
const formatLabel = (value) => {
|
|
867
631
|
if (!value) return 'Unspecified';
|
|
868
|
-
return DISPLAY_LABELS[value] || value.split('_').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
|
|
632
|
+
return DISPLAY_LABELS[value] || String(value).split('_').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
|
|
869
633
|
};
|
|
870
634
|
|
|
871
635
|
const compactText = (value, maxLength) => {
|
|
@@ -882,95 +646,35 @@ body {
|
|
|
882
646
|
return new Date(parsed).toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' });
|
|
883
647
|
};
|
|
884
648
|
|
|
885
|
-
function setupDropdown(input) {
|
|
886
|
-
const dropdown = input.closest('[data-filter-dropdown]');
|
|
887
|
-
const trigger = dropdown ? dropdown.querySelector('[data-filter-trigger]') : null;
|
|
888
|
-
const label = dropdown ? dropdown.querySelector('[data-filter-label]') : null;
|
|
889
|
-
const menu = dropdown ? dropdown.querySelector('[data-filter-menu]') : null;
|
|
890
|
-
const options = menu ? Array.from(menu.querySelectorAll('[data-filter-option]')) : [];
|
|
891
|
-
|
|
892
|
-
if (!dropdown || !trigger || !label || !menu || options.length === 0) {
|
|
893
|
-
return null;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const close = () => {
|
|
897
|
-
dropdown.classList.remove('is-open');
|
|
898
|
-
trigger.setAttribute('aria-expanded', 'false');
|
|
899
|
-
menu.hidden = true;
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
const closeOthers = () => {
|
|
903
|
-
document.querySelectorAll('[data-filter-dropdown].is-open').forEach((openDropdown) => {
|
|
904
|
-
if (openDropdown === dropdown) return;
|
|
905
|
-
openDropdown.classList.remove('is-open');
|
|
906
|
-
const openTrigger = openDropdown.querySelector('[data-filter-trigger]');
|
|
907
|
-
const openMenu = openDropdown.querySelector('[data-filter-menu]');
|
|
908
|
-
if (openTrigger) openTrigger.setAttribute('aria-expanded', 'false');
|
|
909
|
-
if (openMenu) openMenu.hidden = true;
|
|
910
|
-
});
|
|
911
|
-
};
|
|
912
|
-
|
|
913
|
-
const open = () => {
|
|
914
|
-
closeOthers();
|
|
915
|
-
dropdown.classList.add('is-open');
|
|
916
|
-
trigger.setAttribute('aria-expanded', 'true');
|
|
917
|
-
menu.hidden = false;
|
|
918
|
-
};
|
|
919
|
-
|
|
920
|
-
const setValue = (value, shouldDispatch) => {
|
|
921
|
-
const selectedOption = options.find((option) => option.getAttribute('data-value') === value) || options[0];
|
|
922
|
-
if (!selectedOption) return;
|
|
923
|
-
|
|
924
|
-
input.value = selectedOption.getAttribute('data-value') || '';
|
|
925
|
-
label.textContent = selectedOption.textContent || '';
|
|
926
|
-
|
|
927
|
-
options.forEach((option) => {
|
|
928
|
-
const isSelected = option === selectedOption;
|
|
929
|
-
option.classList.toggle('is-selected', isSelected);
|
|
930
|
-
option.setAttribute('aria-selected', isSelected ? 'true' : 'false');
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
if (shouldDispatch) {
|
|
934
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
935
|
-
}
|
|
936
|
-
};
|
|
937
|
-
|
|
938
|
-
setValue(input.value, false);
|
|
939
|
-
|
|
940
|
-
trigger.addEventListener('click', () => {
|
|
941
|
-
if (dropdown.classList.contains('is-open')) close();
|
|
942
|
-
else open();
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
options.forEach((option) => {
|
|
946
|
-
option.addEventListener('click', () => {
|
|
947
|
-
setValue(option.getAttribute('data-value') || '', true);
|
|
948
|
-
close();
|
|
949
|
-
trigger.focus();
|
|
950
|
-
});
|
|
951
|
-
});
|
|
952
|
-
|
|
953
|
-
return { dropdown, close, setValue };
|
|
954
|
-
}
|
|
955
|
-
|
|
956
649
|
const calculateLevel = (totalPoints) => {
|
|
957
650
|
let level = 1;
|
|
958
651
|
for (let i = 1; i < LEVEL_THRESHOLDS.length; i += 1) {
|
|
959
|
-
if (totalPoints >= LEVEL_THRESHOLDS[i])
|
|
960
|
-
|
|
961
|
-
} else {
|
|
962
|
-
return level;
|
|
963
|
-
}
|
|
652
|
+
if (totalPoints >= LEVEL_THRESHOLDS[i]) level = i + 1;
|
|
653
|
+
else return level;
|
|
964
654
|
}
|
|
965
655
|
return MAX_LEVEL;
|
|
966
656
|
};
|
|
967
657
|
|
|
968
|
-
const
|
|
658
|
+
const compareProfileEntries = (left, right) =>
|
|
969
659
|
right.totalPoints - left.totalPoints
|
|
970
660
|
|| right.analyzedPRs - left.analyzedPRs
|
|
971
661
|
|| Date.parse(right.lastActivityAt || right.lastUpdated) - Date.parse(left.lastActivityAt || left.lastUpdated)
|
|
972
662
|
|| left.username.localeCompare(right.username);
|
|
973
663
|
|
|
664
|
+
const compareTeamEntries = (left, right) =>
|
|
665
|
+
right.totalPoints - left.totalPoints
|
|
666
|
+
|| right.memberCount - left.memberCount
|
|
667
|
+
|| right.analyzedPRs - left.analyzedPRs
|
|
668
|
+
|| Date.parse(right.lastActivityAt || '') - Date.parse(left.lastActivityAt || '')
|
|
669
|
+
|| left.team.localeCompare(right.team);
|
|
670
|
+
|
|
671
|
+
const normalizeTeamKey = (value) => String(value || '').trim().toLowerCase();
|
|
672
|
+
|
|
673
|
+
const hasTeam = (teams, selectedTeam) => {
|
|
674
|
+
if (selectedTeam === 'all') return true;
|
|
675
|
+
return (teams || []).some((team) => normalizeTeamKey(team) === normalizeTeamKey(selectedTeam));
|
|
676
|
+
};
|
|
677
|
+
|
|
974
678
|
const cutoffFor = (timeValue) => {
|
|
975
679
|
if (timeValue === 'all' || timeValue === 'custom') return null;
|
|
976
680
|
const now = new Date();
|
|
@@ -983,154 +687,239 @@ body {
|
|
|
983
687
|
return cutoff.getTime();
|
|
984
688
|
};
|
|
985
689
|
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
return {
|
|
994
|
-
storageKey: profile.storageKey,
|
|
995
|
-
username: profile.username,
|
|
996
|
-
totalPoints: profile.totalPoints,
|
|
997
|
-
level: profile.level,
|
|
998
|
-
lastUpdated: profile.lastUpdated,
|
|
999
|
-
lastActivityAt: profile.lastActivityAt,
|
|
1000
|
-
analyzedPRs: profile.analyzedPRs,
|
|
1001
|
-
primaryArea: profile.primaryArea,
|
|
1002
|
-
title: profile.title,
|
|
1003
|
-
summary: profile.summary,
|
|
1004
|
-
reportPath: profile.reportPath,
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
690
|
+
const getFilteredContributions = (profile) => {
|
|
691
|
+
const cutoff = cutoffFor(timeFilter.value);
|
|
692
|
+
const fromMs = timeFilter.value === 'custom' && fromDate.value ? Date.parse(fromDate.value) : null;
|
|
693
|
+
const toMs = timeFilter.value === 'custom' && toDate.value ? Date.parse(toDate.value + 'T23:59:59.999') : null;
|
|
694
|
+
const specializationValue = specializationFilter.value;
|
|
1007
695
|
|
|
1008
|
-
|
|
1009
|
-
const customFromMs = timeValue === 'custom' && customFrom ? Date.parse(customFrom) : null;
|
|
1010
|
-
const customToMs = timeValue === 'custom' && customTo ? Date.parse(customTo + 'T23:59:59.999') : null;
|
|
1011
|
-
const filtered = profile.contributions.filter((contribution) => {
|
|
696
|
+
return (profile.contributions || []).filter((contribution) => {
|
|
1012
697
|
const mergedAt = Date.parse(contribution.mergedAt);
|
|
1013
698
|
if (Number.isNaN(mergedAt)) return false;
|
|
1014
699
|
if (cutoff != null && mergedAt < cutoff) return false;
|
|
1015
|
-
if (
|
|
1016
|
-
if (
|
|
700
|
+
if (fromMs != null && mergedAt < fromMs) return false;
|
|
701
|
+
if (toMs != null && mergedAt > toMs) return false;
|
|
1017
702
|
if (specializationValue !== 'all' && !contribution.areas.includes(specializationValue)) return false;
|
|
1018
703
|
return true;
|
|
1019
704
|
});
|
|
705
|
+
};
|
|
1020
706
|
|
|
1021
|
-
|
|
707
|
+
const buildProfileEntries = () => profiles
|
|
708
|
+
.filter((profile) => hasTeam(profile.teams, teamFilter.value))
|
|
709
|
+
.map((profile) => {
|
|
710
|
+
const contributions = getFilteredContributions(profile);
|
|
711
|
+
if (contributions.length === 0) {
|
|
712
|
+
if (timeFilter.value !== 'all' && timeFilter.value !== 'custom') return null;
|
|
713
|
+
if (timeFilter.value === 'custom' && (fromDate.value || toDate.value)) return null;
|
|
714
|
+
if (specializationFilter.value !== 'all') return null;
|
|
715
|
+
if ((profile.totalPoints || 0) <= 0) return null;
|
|
716
|
+
return {
|
|
717
|
+
kind: 'profile',
|
|
718
|
+
storageKey: profile.storageKey,
|
|
719
|
+
username: profile.username,
|
|
720
|
+
scopeLabel: profile.scopeLabel,
|
|
721
|
+
teams: profile.teams || [],
|
|
722
|
+
totalPoints: profile.totalPoints,
|
|
723
|
+
level: profile.level,
|
|
724
|
+
lastUpdated: profile.lastUpdated,
|
|
725
|
+
lastActivityAt: profile.lastActivityAt,
|
|
726
|
+
analyzedPRs: profile.analyzedPRs,
|
|
727
|
+
primaryArea: profile.primaryArea,
|
|
728
|
+
title: profile.title,
|
|
729
|
+
summary: profile.summary,
|
|
730
|
+
reportPath: profile.reportPath,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
1022
733
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
734
|
+
const areaTotals = {};
|
|
735
|
+
let totalPoints = 0;
|
|
736
|
+
let lastActivityAt = contributions[0].mergedAt;
|
|
737
|
+
contributions.forEach((contribution) => {
|
|
738
|
+
totalPoints += contribution.points;
|
|
739
|
+
if (Date.parse(contribution.mergedAt) > Date.parse(lastActivityAt)) lastActivityAt = contribution.mergedAt;
|
|
740
|
+
contribution.areas.forEach((area) => {
|
|
741
|
+
areaTotals[area] = (areaTotals[area] || 0) + contribution.points;
|
|
742
|
+
});
|
|
743
|
+
});
|
|
1026
744
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
745
|
+
return {
|
|
746
|
+
kind: 'profile',
|
|
747
|
+
storageKey: profile.storageKey,
|
|
748
|
+
username: profile.username,
|
|
749
|
+
scopeLabel: profile.scopeLabel,
|
|
750
|
+
teams: profile.teams || [],
|
|
751
|
+
totalPoints,
|
|
752
|
+
level: calculateLevel(totalPoints),
|
|
753
|
+
lastUpdated: profile.lastUpdated,
|
|
754
|
+
lastActivityAt,
|
|
755
|
+
analyzedPRs: contributions.length,
|
|
756
|
+
primaryArea: Object.entries(areaTotals).sort(([, left], [, right]) => right - left)[0]?.[0],
|
|
757
|
+
title: profile.title,
|
|
758
|
+
summary: profile.summary,
|
|
759
|
+
reportPath: profile.reportPath,
|
|
760
|
+
};
|
|
761
|
+
})
|
|
762
|
+
.filter(Boolean)
|
|
763
|
+
.sort(compareProfileEntries);
|
|
764
|
+
|
|
765
|
+
const buildTeamEntries = () => {
|
|
766
|
+
const aggregate = new Map();
|
|
767
|
+
|
|
768
|
+
buildProfileEntries().forEach((profile) => {
|
|
769
|
+
if (!profile.teams || profile.teams.length === 0) return;
|
|
770
|
+
profile.teams.forEach((team) => {
|
|
771
|
+
const teamKey = normalizeTeamKey(team);
|
|
772
|
+
if (!teamKey) return;
|
|
773
|
+
|
|
774
|
+
const bucket = aggregate.get(teamKey) || {
|
|
775
|
+
kind: 'team',
|
|
776
|
+
team,
|
|
777
|
+
totalPoints: 0,
|
|
778
|
+
level: 1,
|
|
779
|
+
memberCount: 0,
|
|
780
|
+
analyzedPRs: 0,
|
|
781
|
+
primaryArea: undefined,
|
|
782
|
+
lastActivityAt: undefined,
|
|
783
|
+
members: [],
|
|
784
|
+
areaTotals: {},
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
bucket.totalPoints += profile.totalPoints;
|
|
788
|
+
bucket.analyzedPRs += profile.analyzedPRs;
|
|
789
|
+
bucket.lastActivityAt = !bucket.lastActivityAt
|
|
790
|
+
? profile.lastActivityAt
|
|
791
|
+
: (Date.parse(profile.lastActivityAt || '') > Date.parse(bucket.lastActivityAt || '') ? profile.lastActivityAt : bucket.lastActivityAt);
|
|
792
|
+
bucket.members.push({
|
|
793
|
+
username: profile.username,
|
|
794
|
+
scopeLabel: profile.scopeLabel,
|
|
795
|
+
reportPath: profile.reportPath,
|
|
796
|
+
});
|
|
797
|
+
if (profile.primaryArea) {
|
|
798
|
+
bucket.areaTotals[profile.primaryArea] = (bucket.areaTotals[profile.primaryArea] || 0) + profile.totalPoints;
|
|
799
|
+
}
|
|
800
|
+
bucket.primaryArea = Object.entries(bucket.areaTotals).sort(([, left], [, right]) => right - left)[0]?.[0];
|
|
801
|
+
aggregate.set(teamKey, bucket);
|
|
1034
802
|
});
|
|
1035
803
|
});
|
|
1036
804
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
lastActivityAt,
|
|
1045
|
-
analyzedPRs: filtered.length,
|
|
1046
|
-
primaryArea,
|
|
1047
|
-
title: profile.title,
|
|
1048
|
-
summary: profile.summary,
|
|
1049
|
-
reportPath: profile.reportPath,
|
|
1050
|
-
};
|
|
805
|
+
return Array.from(aggregate.values())
|
|
806
|
+
.map((entry) => ({
|
|
807
|
+
...entry,
|
|
808
|
+
level: calculateLevel(entry.totalPoints),
|
|
809
|
+
memberCount: entry.members.length,
|
|
810
|
+
}))
|
|
811
|
+
.sort(compareTeamEntries);
|
|
1051
812
|
};
|
|
1052
813
|
|
|
1053
|
-
const
|
|
1054
|
-
const preview = compactText(entry.summary, 170);
|
|
1055
|
-
const fullSummary = compactText(entry.summary, 520);
|
|
814
|
+
const renderProfileEntry = (entry, index) => {
|
|
1056
815
|
const reportAction = entry.reportPath
|
|
1057
|
-
? '<a class="
|
|
1058
|
-
: '<span class="
|
|
816
|
+
? '<a class="member-link" href="file:///' + String(entry.reportPath).replace(/\\\\/g, '/') + '" target="_blank" rel="noopener noreferrer">Open report</a>'
|
|
817
|
+
: '<span class="member-link disabled">Report missing</span>';
|
|
818
|
+
const teams = entry.teams.length > 0
|
|
819
|
+
? entry.teams.map((team) => '<span class="meta-pill">' + escapeHtml(team) + '</span>').join('')
|
|
820
|
+
: '<span class="empty-pill">No teams</span>';
|
|
1059
821
|
|
|
1060
822
|
return '<details class="entry">'
|
|
1061
|
-
+ '<summary class="entry-
|
|
823
|
+
+ '<summary><div class="entry-head">'
|
|
1062
824
|
+ '<div class="rank">' + (index + 1) + '</div>'
|
|
1063
|
-
+ '<div
|
|
1064
|
-
+ '<div class="entry-top">'
|
|
1065
|
-
+ '<div><
|
|
1066
|
-
+ '<div class="xp-block"><strong>' + entry.totalPoints.toLocaleString() + ' XP</strong><span>Level ' + entry.level + '</span></div>'
|
|
1067
|
-
+ '</div>'
|
|
825
|
+
+ '<div>'
|
|
826
|
+
+ '<div class="entry-top"><div><h2>' + escapeHtml(entry.username) + '</h2><div class="subtitle">' + escapeHtml(entry.title || 'Generated profile') + '</div></div>'
|
|
827
|
+
+ '<div class="score"><strong>' + entry.totalPoints.toLocaleString() + ' XP</strong><span>Level ' + entry.level + '</span></div></div>'
|
|
1068
828
|
+ '<div class="meta-row">'
|
|
1069
829
|
+ '<span class="meta-pill">' + entry.analyzedPRs + ' PRs</span>'
|
|
830
|
+
+ '<span class="meta-pill">' + escapeHtml(entry.scopeLabel) + '</span>'
|
|
1070
831
|
+ '<span class="meta-pill">' + escapeHtml(formatLabel(entry.primaryArea)) + '</span>'
|
|
1071
832
|
+ '<span class="meta-pill">Last activity ' + escapeHtml(formatDate(entry.lastActivityAt)) + '</span>'
|
|
1072
833
|
+ '</div>'
|
|
1073
|
-
+ '<p class="summary
|
|
834
|
+
+ '<p class="summary">' + escapeHtml(compactText(entry.summary, 180)) + '</p>'
|
|
1074
835
|
+ '</div>'
|
|
1075
836
|
+ '<div class="entry-side">' + reportAction + '<span class="expand-hint">More</span></div>'
|
|
1076
|
-
+ '</summary>'
|
|
837
|
+
+ '</div></summary>'
|
|
1077
838
|
+ '<div class="entry-details">'
|
|
1078
|
-
+ '<p class="summary
|
|
1079
|
-
+ '<div class="detail-
|
|
1080
|
-
+ '<div class="detail-
|
|
1081
|
-
+ '<div class="detail-
|
|
1082
|
-
+ '<div class="detail-
|
|
839
|
+
+ '<p class="summary">' + escapeHtml(compactText(entry.summary, 520)) + '</p>'
|
|
840
|
+
+ '<div class="detail-row">'
|
|
841
|
+
+ '<div class="detail-card"><span>Scope</span><strong>' + escapeHtml(entry.scopeLabel) + '</strong></div>'
|
|
842
|
+
+ '<div class="detail-card"><span>Teams</span><strong>' + escapeHtml(entry.teams.join(', ') || 'No teams') + '</strong></div>'
|
|
843
|
+
+ '<div class="detail-card"><span>Specialization</span><strong>' + escapeHtml(formatLabel(entry.primaryArea)) + '</strong></div>'
|
|
1083
844
|
+ '</div>'
|
|
845
|
+
+ '<div class="detail-row">' + teams + '</div>'
|
|
846
|
+
+ '</div></details>';
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const renderTeamEntry = (entry, index) => {
|
|
850
|
+
const members = entry.members.map((member) => {
|
|
851
|
+
if (member.reportPath) {
|
|
852
|
+
return '<div class="member-tag"><a href="file:///' + String(member.reportPath).replace(/\\\\/g, '/') + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(member.username) + '</a><span>' + escapeHtml(member.scopeLabel) + '</span></div>';
|
|
853
|
+
}
|
|
854
|
+
return '<div class="member-tag"><strong>' + escapeHtml(member.username) + '</strong><span>' + escapeHtml(member.scopeLabel) + '</span></div>';
|
|
855
|
+
}).join('');
|
|
856
|
+
|
|
857
|
+
return '<details class="entry">'
|
|
858
|
+
+ '<summary><div class="entry-head">'
|
|
859
|
+
+ '<div class="rank">' + (index + 1) + '</div>'
|
|
860
|
+
+ '<div>'
|
|
861
|
+
+ '<div class="entry-top"><div><h2>' + escapeHtml(entry.team) + '</h2><div class="subtitle">Team leaderboard entry</div></div>'
|
|
862
|
+
+ '<div class="score"><strong>' + entry.totalPoints.toLocaleString() + ' XP</strong><span>Level ' + entry.level + '</span></div></div>'
|
|
863
|
+
+ '<div class="meta-row">'
|
|
864
|
+
+ '<span class="meta-pill">' + entry.memberCount + ' members</span>'
|
|
865
|
+
+ '<span class="meta-pill">' + entry.analyzedPRs + ' PRs</span>'
|
|
866
|
+
+ '<span class="meta-pill">' + escapeHtml(formatLabel(entry.primaryArea)) + '</span>'
|
|
867
|
+
+ '<span class="meta-pill">Last activity ' + escapeHtml(formatDate(entry.lastActivityAt)) + '</span>'
|
|
868
|
+
+ '</div>'
|
|
869
|
+
+ '<p class="summary">Combined output from every saved profile tagged with this team.</p>'
|
|
1084
870
|
+ '</div>'
|
|
1085
|
-
+ '</
|
|
871
|
+
+ '<div class="entry-side"><span class="expand-hint">Members</span></div>'
|
|
872
|
+
+ '</div></summary>'
|
|
873
|
+
+ '<div class="entry-details">'
|
|
874
|
+
+ '<div class="detail-row">'
|
|
875
|
+
+ '<div class="detail-card"><span>Members</span><strong>' + entry.memberCount + '</strong></div>'
|
|
876
|
+
+ '<div class="detail-card"><span>Analyzed PRs</span><strong>' + entry.analyzedPRs + '</strong></div>'
|
|
877
|
+
+ '<div class="detail-card"><span>Primary area</span><strong>' + escapeHtml(formatLabel(entry.primaryArea)) + '</strong></div>'
|
|
878
|
+
+ '</div>'
|
|
879
|
+
+ '<div class="member-list">' + members + '</div>'
|
|
880
|
+
+ '</div></details>';
|
|
1086
881
|
};
|
|
1087
882
|
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
let timeLabel;
|
|
1092
|
-
if (timeFilter.value === 'custom') {
|
|
1093
|
-
const parts = [];
|
|
1094
|
-
if (customFrom) parts.push('from ' + formatDate(customFrom));
|
|
1095
|
-
if (customTo) parts.push('to ' + formatDate(customTo));
|
|
1096
|
-
timeLabel = parts.length ? parts.join(' ') : 'custom range';
|
|
1097
|
-
} else {
|
|
1098
|
-
timeLabel = TIME_LABELS[timeFilter.value] || 'all time';
|
|
883
|
+
const renderEmpty = () => {
|
|
884
|
+
if (currentMode === 'team') {
|
|
885
|
+
return '<div class="empty">No teams match the current filters. Generate profiles with --team to populate this view.</div>';
|
|
1099
886
|
}
|
|
1100
|
-
|
|
887
|
+
if (teamFilter.value !== 'all') {
|
|
888
|
+
return '<div class="empty">No profiles match team "' + escapeHtml(teamFilter.value) + '" with the current filters.</div>';
|
|
889
|
+
}
|
|
890
|
+
return '<div class="empty">No saved profiles match the current filters.</div>';
|
|
1101
891
|
};
|
|
1102
892
|
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
.
|
|
893
|
+
const getEntries = () => currentMode === 'team' ? buildTeamEntries() : buildProfileEntries();
|
|
894
|
+
|
|
895
|
+
const updateModeUI = () => {
|
|
896
|
+
modeButtons.forEach((button) => {
|
|
897
|
+
const active = button.getAttribute('data-mode') === currentMode;
|
|
898
|
+
button.classList.toggle('is-active', active);
|
|
899
|
+
});
|
|
900
|
+
teamFilter.disabled = currentMode === 'team';
|
|
901
|
+
};
|
|
1107
902
|
|
|
1108
903
|
const render = () => {
|
|
1109
|
-
|
|
904
|
+
customRange.classList.toggle('hidden', timeFilter.value !== 'custom');
|
|
905
|
+
updateModeUI();
|
|
906
|
+
|
|
907
|
+
const entries = getEntries();
|
|
1110
908
|
const pageCount = Math.max(1, Math.ceil(entries.length / PROFILES_PER_PAGE));
|
|
1111
909
|
if (currentPage > pageCount) currentPage = pageCount;
|
|
1112
910
|
|
|
1113
911
|
const start = (currentPage - 1) * PROFILES_PER_PAGE;
|
|
1114
|
-
const
|
|
912
|
+
const pageEntries = entries.slice(start, start + PROFILES_PER_PAGE);
|
|
1115
913
|
|
|
1116
|
-
board.innerHTML =
|
|
1117
|
-
?
|
|
1118
|
-
:
|
|
914
|
+
board.innerHTML = pageEntries.length
|
|
915
|
+
? pageEntries.map((entry, index) => entry.kind === 'team' ? renderTeamEntry(entry, start + index) : renderProfileEntry(entry, start + index)).join('')
|
|
916
|
+
: renderEmpty();
|
|
1119
917
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if (timeFilter.value === 'custom') {
|
|
1126
|
-
const parts = [];
|
|
1127
|
-
if (customFrom) parts.push('from ' + formatDate(customFrom));
|
|
1128
|
-
if (customTo) parts.push('to ' + formatDate(customTo));
|
|
1129
|
-
timeDescription = parts.length ? parts.join(' ') : 'custom range';
|
|
1130
|
-
} else {
|
|
1131
|
-
timeDescription = TIME_LABELS[timeFilter.value] || 'all time';
|
|
1132
|
-
}
|
|
1133
|
-
filterSummary.innerHTML = '<strong>' + entries.length + '</strong> profiles for ' + escapeHtml(timeDescription) + ' across ' + escapeHtml(specializationLabel) + '.';
|
|
918
|
+
const subject = currentMode === 'team' ? 'teams' : 'profiles';
|
|
919
|
+
const scopeDescription = currentMode === 'team'
|
|
920
|
+
? 'aggregated team rankings'
|
|
921
|
+
: (teamFilter.value === 'all' ? 'all teams' : 'team ' + teamFilter.value);
|
|
922
|
+
summary.innerHTML = '<strong>' + entries.length + '</strong> ' + subject + ' for ' + escapeHtml(scopeDescription) + '.';
|
|
1134
923
|
|
|
1135
924
|
pagination.hidden = pageCount <= 1;
|
|
1136
925
|
prevButton.disabled = currentPage === 1;
|
|
@@ -1138,62 +927,20 @@ body {
|
|
|
1138
927
|
pageStatus.innerHTML = 'Page <strong>' + currentPage + '</strong> of ' + pageCount;
|
|
1139
928
|
};
|
|
1140
929
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
modalFromInput.focus();
|
|
1146
|
-
};
|
|
1147
|
-
|
|
1148
|
-
const closeModal = () => {
|
|
1149
|
-
modalOverlay.hidden = true;
|
|
1150
|
-
};
|
|
1151
|
-
|
|
1152
|
-
timeFilter.addEventListener('change', () => {
|
|
1153
|
-
if (timeFilter.value === 'custom') {
|
|
1154
|
-
openModal();
|
|
1155
|
-
} else {
|
|
1156
|
-
previousTimeValue = timeFilter.value;
|
|
930
|
+
modeButtons.forEach((button) => {
|
|
931
|
+
button.addEventListener('click', () => {
|
|
932
|
+
currentMode = button.getAttribute('data-mode') || 'profile';
|
|
933
|
+
if (currentMode === 'team') teamFilter.value = 'all';
|
|
1157
934
|
currentPage = 1;
|
|
1158
935
|
render();
|
|
1159
|
-
}
|
|
1160
|
-
});
|
|
1161
|
-
|
|
1162
|
-
modalCancelBtn.addEventListener('click', () => {
|
|
1163
|
-
closeModal();
|
|
1164
|
-
if (timeDropdown) timeDropdown.setValue(previousTimeValue, false);
|
|
1165
|
-
});
|
|
1166
|
-
|
|
1167
|
-
modalApplyBtn.addEventListener('click', () => {
|
|
1168
|
-
customFrom = modalFromInput.value;
|
|
1169
|
-
customTo = modalToInput.value;
|
|
1170
|
-
const parts = [];
|
|
1171
|
-
if (customFrom) parts.push(formatDate(customFrom));
|
|
1172
|
-
if (customTo) parts.push(formatDate(customTo));
|
|
1173
|
-
const label = timeFilter.closest('[data-filter-dropdown]')?.querySelector('[data-filter-label]');
|
|
1174
|
-
if (label) label.textContent = parts.length ? parts.join(' \u2013 ') : 'Custom range';
|
|
1175
|
-
closeModal();
|
|
1176
|
-
currentPage = 1;
|
|
1177
|
-
render();
|
|
1178
|
-
});
|
|
1179
|
-
|
|
1180
|
-
modalOverlay.addEventListener('click', (event) => {
|
|
1181
|
-
if (event.target === modalOverlay) {
|
|
1182
|
-
closeModal();
|
|
1183
|
-
if (timeDropdown) timeDropdown.setValue(previousTimeValue, false);
|
|
1184
|
-
}
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
document.addEventListener('keydown', (event) => {
|
|
1188
|
-
if (event.key === 'Escape' && !modalOverlay.hidden) {
|
|
1189
|
-
closeModal();
|
|
1190
|
-
if (timeDropdown) timeDropdown.setValue(previousTimeValue, false);
|
|
1191
|
-
}
|
|
936
|
+
});
|
|
1192
937
|
});
|
|
1193
938
|
|
|
1194
|
-
specializationFilter
|
|
1195
|
-
|
|
1196
|
-
|
|
939
|
+
[timeFilter, specializationFilter, teamFilter, fromDate, toDate].forEach((element) => {
|
|
940
|
+
element.addEventListener('change', () => {
|
|
941
|
+
currentPage = 1;
|
|
942
|
+
render();
|
|
943
|
+
});
|
|
1197
944
|
});
|
|
1198
945
|
|
|
1199
946
|
prevButton.addEventListener('click', () => {
|
|
@@ -1203,8 +950,7 @@ body {
|
|
|
1203
950
|
});
|
|
1204
951
|
|
|
1205
952
|
nextButton.addEventListener('click', () => {
|
|
1206
|
-
const
|
|
1207
|
-
const pageCount = Math.max(1, Math.ceil(entries.length / PROFILES_PER_PAGE));
|
|
953
|
+
const pageCount = Math.max(1, Math.ceil(getEntries().length / PROFILES_PER_PAGE));
|
|
1208
954
|
if (currentPage === pageCount) return;
|
|
1209
955
|
currentPage += 1;
|
|
1210
956
|
render();
|