added infinite scrolling to frontend and displaying vote information
This commit is contained in:
@@ -35,14 +35,49 @@ export default async function MapPeriodDetailPage({
|
||||
const [players, votes] = await Promise.all([attachAvatars(playersRaw), attachAvatars(votesRaw)]);
|
||||
|
||||
// Group vote rows by their vote event (a map can theoretically have more
|
||||
// than one vote called during its runtime).
|
||||
const voteEvents = new Map<number, { vote_time: string; result: string | null; choices: typeof votes }>();
|
||||
// than one vote called during its runtime), and tally each map's total
|
||||
// (weighted) votes within that event, descending. "Total votes cast" is
|
||||
// computed from the same weighted sum as the tally below it, so the two
|
||||
// numbers always agree — counting a weighted vote as 1 instead of its
|
||||
// actual weight is exactly what caused them to disagree before.
|
||||
interface VoteTallyRow {
|
||||
map: string;
|
||||
weightedVotes: number;
|
||||
voterCount: number;
|
||||
}
|
||||
const voteEvents = new Map<
|
||||
number,
|
||||
{
|
||||
vote_time: string;
|
||||
result: string | null;
|
||||
choices: typeof votes;
|
||||
tally: VoteTallyRow[];
|
||||
totalWeightedVotes: number;
|
||||
}
|
||||
>();
|
||||
for (const v of votes) {
|
||||
if (!voteEvents.has(v.vote_id)) {
|
||||
voteEvents.set(v.vote_id, { vote_time: v.vote_time, result: v.result, choices: [] });
|
||||
voteEvents.set(v.vote_id, {
|
||||
vote_time: v.vote_time,
|
||||
result: v.result,
|
||||
choices: [],
|
||||
tally: [],
|
||||
totalWeightedVotes: 0,
|
||||
});
|
||||
}
|
||||
voteEvents.get(v.vote_id)!.choices.push(v);
|
||||
}
|
||||
for (const event of voteEvents.values()) {
|
||||
const byMap = new Map<string, VoteTallyRow>();
|
||||
for (const c of event.choices) {
|
||||
const row = byMap.get(c.vote_choice) ?? { map: c.vote_choice, weightedVotes: 0, voterCount: 0 };
|
||||
row.weightedVotes += c.vote_weight;
|
||||
row.voterCount += 1;
|
||||
byMap.set(c.vote_choice, row);
|
||||
}
|
||||
event.tally = [...byMap.values()].sort((a, b) => b.weightedVotes - a.weightedVotes);
|
||||
event.totalWeightedVotes = event.tally.reduce((sum, row) => sum + row.weightedVotes, 0);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -104,7 +139,7 @@ export default async function MapPeriodDetailPage({
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="panel p-6">
|
||||
<div className="stat-label mb-3">
|
||||
Players present ({players.length})
|
||||
Connected during this session ({players.length})
|
||||
</div>
|
||||
{players.length === 0 ? (
|
||||
<div className="text-sm text-ink-muted">No recorded players during this period.</div>
|
||||
@@ -150,9 +185,39 @@ export default async function MapPeriodDetailPage({
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs text-ink-faint mono">
|
||||
{new Date(event.vote_time.replace(' ', 'T')).toLocaleString()}
|
||||
{' · '}
|
||||
{event.totalWeightedVotes} vote{event.totalWeightedVotes === 1 ? '' : 's'} cast
|
||||
{event.totalWeightedVotes !== event.choices.length && (
|
||||
<span className="text-ink-faint"> ({event.choices.length} voter{event.choices.length === 1 ? '' : 's'})</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-accent">Result: {event.result ?? '—'}</div>
|
||||
</div>
|
||||
|
||||
{/* Per-map tally, highest first */}
|
||||
<div className="space-y-1 mb-4">
|
||||
{event.tally.map((t) => {
|
||||
const pct = event.totalWeightedVotes > 0 ? (t.weightedVotes / event.tally[0].weightedVotes) * 100 : 0;
|
||||
return (
|
||||
<div key={t.map} className="flex items-center gap-2 text-xs">
|
||||
<span className="text-ink w-40 truncate shrink-0">{t.map}</span>
|
||||
<div className="flex-1 h-3 bg-base rounded overflow-hidden border border-base-border">
|
||||
<div
|
||||
className="h-full rounded-sm"
|
||||
style={{
|
||||
width: `${Math.max(pct, 3)}%`,
|
||||
background: 'linear-gradient(90deg, #2A9C5C, #5EE596)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-ink-muted mono w-20 text-right shrink-0">
|
||||
{t.weightedVotes} vote{t.weightedVotes === 1 ? '' : 's'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="row-divide">
|
||||
{event.choices.map((c, i) => (
|
||||
<Link
|
||||
|
||||
Reference in New Issue
Block a user