added number sorting to graphs and updated the graph for repeated days showcase
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import PlayerTimelineChart from './PlayerTimelineChart';
|
||||
import { formatMinutes, countryCodeToFlag } from '@/lib/steam';
|
||||
|
||||
interface RecurringPoint {
|
||||
date: string; // "2026-07-01"
|
||||
@@ -17,10 +19,20 @@ interface TimelinePlayer {
|
||||
steamid: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
country?: string | null;
|
||||
totalMinutes: number;
|
||||
segments: Segment[];
|
||||
}
|
||||
|
||||
interface AggregatePlayer {
|
||||
steamid: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
country?: string | null;
|
||||
totalMinutes: number;
|
||||
daysPresent: number;
|
||||
}
|
||||
|
||||
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const HEIGHT = 220;
|
||||
const PAD_LEFT = 32;
|
||||
@@ -56,6 +68,11 @@ export default function RecurringWindowChart() {
|
||||
const [windowMinutes, setWindowMinutes] = useState(0);
|
||||
const [loadingPlayers, setLoadingPlayers] = useState(false);
|
||||
|
||||
const [aggregatePlayers, setAggregatePlayers] = useState<AggregatePlayer[]>([]);
|
||||
const [aggregateDates, setAggregateDates] = useState(0);
|
||||
const [loadingAggregate, setLoadingAggregate] = useState(false);
|
||||
const [aggregateTruncated, setAggregateTruncated] = useState(false);
|
||||
|
||||
function toggleDay(d: number) {
|
||||
setSelectedDays((prev) => (prev.includes(d) ? prev.filter((x) => x !== d) : [...prev, d].sort()));
|
||||
}
|
||||
@@ -66,28 +83,49 @@ export default function RecurringWindowChart() {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setLoadingAggregate(true);
|
||||
setError(null);
|
||||
setSelected(null);
|
||||
setHasSearched(true);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
days: selectedDays.join(','),
|
||||
startTime,
|
||||
endTime,
|
||||
start: rangeStart,
|
||||
end: rangeEnd,
|
||||
});
|
||||
const res = await fetch(`/api/population/recurring?${params.toString()}`);
|
||||
if (!res.ok) throw new Error((await res.json()).error ?? 'failed to load');
|
||||
const data = await res.json();
|
||||
setPoints(data.points ?? []);
|
||||
setTruncated(!!data.truncated);
|
||||
} catch (e: any) {
|
||||
setError(e.message ?? 'something went wrong');
|
||||
setPoints([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
days: selectedDays.join(','),
|
||||
startTime,
|
||||
endTime,
|
||||
start: rangeStart,
|
||||
end: rangeEnd,
|
||||
});
|
||||
|
||||
const summaryPromise = (async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/population/recurring?${params.toString()}`);
|
||||
if (!res.ok) throw new Error((await res.json()).error ?? 'failed to load');
|
||||
const data = await res.json();
|
||||
setPoints(data.points ?? []);
|
||||
setTruncated(!!data.truncated);
|
||||
} catch (e: any) {
|
||||
setError(e.message ?? 'something went wrong');
|
||||
setPoints([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
const aggregatePromise = (async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/population/recurring/aggregate?${params.toString()}`);
|
||||
const data = await res.json();
|
||||
setAggregatePlayers(data.players ?? []);
|
||||
setAggregateDates(data.totalMatchingDates ?? 0);
|
||||
setAggregateTruncated(!!data.truncatedPlayers);
|
||||
} catch {
|
||||
setAggregatePlayers([]);
|
||||
setAggregateDates(0);
|
||||
} finally {
|
||||
setLoadingAggregate(false);
|
||||
}
|
||||
})();
|
||||
|
||||
await Promise.all([summaryPromise, aggregatePromise]);
|
||||
}
|
||||
|
||||
async function loadPlayersFor(point: RecurringPoint) {
|
||||
@@ -278,19 +316,91 @@ export default function RecurringWindowChart() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{selected && (
|
||||
<div className="mt-4 pt-4 border-t-2 border-base-divider">
|
||||
<div className="stat-label mb-3">
|
||||
{new Date(`${selected.date}T00:00:00Z`).toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric' })}, {startTime}–{endTime} — {selectedPlayers.length} player
|
||||
{selectedPlayers.length === 1 ? '' : 's'}, sorted by time spent in the window
|
||||
{hasSearched && !loading && points.length > 0 && (
|
||||
<div className="mt-6 pt-6 border-t-2 border-base-divider grid md:grid-cols-2 gap-6">
|
||||
{/* Overall — combined across every matching day */}
|
||||
<div>
|
||||
<div className="stat-label mb-1">
|
||||
Overall — combined across all {aggregateDates} matching day{aggregateDates === 1 ? '' : 's'}, {startTime}–{endTime}
|
||||
</div>
|
||||
<p className="text-xs text-ink-muted mb-3">
|
||||
Each player's time spent inside the window, summed across every matching date.
|
||||
</p>
|
||||
|
||||
{loadingAggregate ? (
|
||||
<div className="text-sm text-ink-faint font-mono py-4">loading…</div>
|
||||
) : aggregatePlayers.length === 0 ? (
|
||||
<div className="text-sm text-ink-muted py-4">No players recorded across these dates.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="row-divide">
|
||||
{aggregatePlayers.map((p, i) => (
|
||||
<Link
|
||||
key={p.steamid}
|
||||
href={`/players/${encodeURIComponent(p.steamid)}`}
|
||||
className="flex items-center justify-between py-2.5 text-sm hover:bg-base transition-colors px-2 -mx-2 rounded"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-ink-faint text-xs w-5 text-right shrink-0">{i + 1}</span>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={p.avatarUrl}
|
||||
alt=""
|
||||
className="w-7 h-7 rounded-full object-cover ring-1 ring-base-border shrink-0"
|
||||
/>
|
||||
{p.country !== undefined && (
|
||||
<span className="text-xs shrink-0" aria-hidden>
|
||||
{countryCodeToFlag(p.country)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-ink truncate">{p.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 shrink-0">
|
||||
<span className="text-ink-faint text-xs mono">
|
||||
{p.daysPresent} of {aggregateDates} day{aggregateDates === 1 ? '' : 's'}
|
||||
</span>
|
||||
<span className="text-ink mono text-xs w-16 text-right">{formatMinutes(p.totalMinutes)}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{aggregateTruncated && (
|
||||
<div className="text-xs text-ink-faint mt-2">
|
||||
Showing the top {aggregatePlayers.length} players by time — narrow the search for a complete list.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* One specific day — populated once a bar is clicked, visible (empty) before that */}
|
||||
<div>
|
||||
{selected ? (
|
||||
<>
|
||||
<div className="stat-label mb-3">
|
||||
{new Date(`${selected.date}T00:00:00Z`).toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric' })}, {startTime}–{endTime} — {selectedPlayers.length} player
|
||||
{selectedPlayers.length === 1 ? '' : 's'}, sorted by time spent in the window
|
||||
</div>
|
||||
<PlayerTimelineChart
|
||||
players={selectedPlayers}
|
||||
windowMinutes={windowMinutes || 1}
|
||||
windowStartLabel={startTime}
|
||||
windowEndLabel={endTime}
|
||||
loading={loadingPlayers}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="min-h-[200px] border border-dashed border-base-border rounded-lg p-4">
|
||||
<div className="flex items-center gap-1.5 text-sm text-accent">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" className="shrink-0">
|
||||
<path d="M9 9l6 6M9 15l6-6" strokeLinecap="round" />
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
</svg>
|
||||
<span>Click any green bar above to see who was online — and exactly when — on that specific day.</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PlayerTimelineChart
|
||||
players={selectedPlayers}
|
||||
windowMinutes={windowMinutes || 1}
|
||||
windowStartLabel={startTime}
|
||||
windowEndLabel={endTime}
|
||||
loading={loadingPlayers}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user