furhter bug fixes and updates
This commit is contained in:
@@ -1,9 +1,21 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getMapPopulationSeries } from '@/lib/queries';
|
||||
import { formatDbDateTime } from '@/lib/timezone';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const limitParam = req.nextUrl.searchParams.get('limit');
|
||||
const beforeParam = req.nextUrl.searchParams.get('before'); // ISO string — page further back for pan-to-load-more
|
||||
const limit = Math.min(Math.max(Number(limitParam) || 40, 1), 200);
|
||||
const points = await getMapPopulationSeries(limit);
|
||||
|
||||
let before: string | undefined;
|
||||
if (beforeParam) {
|
||||
const d = new Date(beforeParam);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return NextResponse.json({ error: 'invalid before param' }, { status: 400 });
|
||||
}
|
||||
before = formatDbDateTime(d);
|
||||
}
|
||||
|
||||
const points = await getMapPopulationSeries(limit, before);
|
||||
return NextResponse.json({ points });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { getSteamSummariesBatch, DEFAULT_AVATAR_URL } from '@/lib/steamApi';
|
||||
import { steam2ToSteam64 } from '@/lib/steam';
|
||||
import { parseDbDateTime } from '@/lib/timezone';
|
||||
|
||||
interface SessionRow {
|
||||
steamid: string;
|
||||
current_name: string;
|
||||
session_start_dt: string;
|
||||
session_end_dt: string | null;
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sp = req.nextUrl.searchParams;
|
||||
const date = sp.get('date');
|
||||
const startTime = sp.get('startTime');
|
||||
const endTime = sp.get('endTime');
|
||||
|
||||
if (!date || !startTime || !endTime) {
|
||||
return NextResponse.json({ error: 'date, startTime, endTime are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const windowStartStr = `${date} ${startTime}:00`;
|
||||
const windowEndStr = `${date} ${endTime}:00`;
|
||||
const windowStartMs = parseDbDateTime(windowStartStr).getTime();
|
||||
const windowEndMs = parseDbDateTime(windowEndStr).getTime();
|
||||
const windowMinutes = Math.round((windowEndMs - windowStartMs) / 60000);
|
||||
|
||||
const rows = await query<SessionRow>(
|
||||
`SELECT s.steamid, COALESCE(p.current_name, s.player_name) AS current_name,
|
||||
s.session_start_dt, s.session_end_dt
|
||||
FROM playtime_display_sessions s
|
||||
LEFT JOIN playtime_display_players p ON p.steamid = s.steamid
|
||||
WHERE s.session_start_dt < ?
|
||||
AND (s.session_end_dt IS NULL OR s.session_end_dt > ?)`,
|
||||
[windowEndStr, windowStartStr],
|
||||
);
|
||||
|
||||
// Clip each session row to the window and accumulate per-player segments —
|
||||
// a player can have more than one segment if they disconnected and
|
||||
// reconnected within the window.
|
||||
interface Segment {
|
||||
startMin: number;
|
||||
endMin: number;
|
||||
}
|
||||
const byPlayer = new Map<string, { name: string; segments: Segment[]; totalMinutes: number }>();
|
||||
|
||||
for (const r of rows) {
|
||||
const sStart = parseDbDateTime(r.session_start_dt).getTime();
|
||||
const sEnd = r.session_end_dt ? parseDbDateTime(r.session_end_dt).getTime() : Date.now();
|
||||
const clippedStart = Math.max(sStart, windowStartMs);
|
||||
const clippedEnd = Math.min(sEnd, windowEndMs);
|
||||
if (clippedEnd <= clippedStart) continue;
|
||||
|
||||
const startMin = Math.round((clippedStart - windowStartMs) / 60000);
|
||||
const endMin = Math.round((clippedEnd - windowStartMs) / 60000);
|
||||
|
||||
if (!byPlayer.has(r.steamid)) {
|
||||
byPlayer.set(r.steamid, { name: r.current_name, segments: [], totalMinutes: 0 });
|
||||
}
|
||||
const entry = byPlayer.get(r.steamid)!;
|
||||
entry.segments.push({ startMin, endMin });
|
||||
entry.totalMinutes += endMin - startMin;
|
||||
}
|
||||
|
||||
const steam64ByPlayer = new Map<string, string>();
|
||||
for (const steamid of byPlayer.keys()) {
|
||||
const id64 = steam2ToSteam64(steamid);
|
||||
if (id64) steam64ByPlayer.set(steamid, id64);
|
||||
}
|
||||
const avatars = await getSteamSummariesBatch([...steam64ByPlayer.values()]);
|
||||
|
||||
const players = [...byPlayer.entries()]
|
||||
.map(([steamid, data]) => {
|
||||
const id64 = steam64ByPlayer.get(steamid);
|
||||
const avatarUrl = (id64 && avatars.get(id64)?.avatarUrl) || DEFAULT_AVATAR_URL;
|
||||
return {
|
||||
steamid,
|
||||
name: data.name,
|
||||
avatarUrl,
|
||||
totalMinutes: data.totalMinutes,
|
||||
segments: data.segments,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.totalMinutes - a.totalMinutes);
|
||||
|
||||
return NextResponse.json({ windowMinutes, players });
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
|
||||
function pad(n: number) {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
const MAX_DATES = 120; // safety cap — one query per matching date
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const sp = req.nextUrl.searchParams;
|
||||
const daysParam = sp.get('days'); // comma list, JS convention: 0=Sun..6=Sat
|
||||
const startTime = sp.get('startTime'); // "06:00"
|
||||
const endTime = sp.get('endTime'); // "15:00"
|
||||
const startDate = sp.get('start'); // "2026-07-01"
|
||||
const endDate = sp.get('end'); // "2026-08-22"
|
||||
|
||||
if (!daysParam || !startTime || !endTime || !startDate || !endDate) {
|
||||
return NextResponse.json({ error: 'days, startTime, endTime, start, end are all required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const days = daysParam.split(',').map(Number).filter((d) => d >= 0 && d <= 6);
|
||||
if (days.length === 0) {
|
||||
return NextResponse.json({ error: 'no valid days selected' }, { status: 400 });
|
||||
}
|
||||
if (!/^\d{2}:\d{2}$/.test(startTime) || !/^\d{2}:\d{2}$/.test(endTime) || startTime >= endTime) {
|
||||
return NextResponse.json({ error: 'startTime must be before endTime, both as HH:MM' }, { status: 400 });
|
||||
}
|
||||
|
||||
const [startY, startM, startD] = startDate.split('-').map(Number);
|
||||
const [endY, endM, endD] = endDate.split('-').map(Number);
|
||||
if (!startY || !startM || !startD || !endY || !endM || !endD) {
|
||||
return NextResponse.json({ error: 'invalid start/end date' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Calendar-date arithmetic anchored to UTC purely as a way to enumerate
|
||||
// plain Y-M-D dates and their day-of-week unambiguously — these Date
|
||||
// objects never represent a real instant, just a calendar day.
|
||||
const cursor = new Date(Date.UTC(startY, startM - 1, startD));
|
||||
const endCursor = new Date(Date.UTC(endY, endM - 1, endD));
|
||||
if (cursor > endCursor) {
|
||||
return NextResponse.json({ error: 'start date must be before end date' }, { status: 400 });
|
||||
}
|
||||
|
||||
const matchingDates: string[] = [];
|
||||
while (cursor <= endCursor && matchingDates.length < MAX_DATES) {
|
||||
if (days.includes(cursor.getUTCDay())) {
|
||||
matchingDates.push(`${cursor.getUTCFullYear()}-${pad(cursor.getUTCMonth() + 1)}-${pad(cursor.getUTCDate())}`);
|
||||
}
|
||||
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
||||
}
|
||||
const truncated = matchingDates.length >= MAX_DATES && cursor <= endCursor;
|
||||
|
||||
// Each window boundary is built directly as a "YYYY-MM-DD HH:MM:SS"
|
||||
// string — already in the exact format the DB stores, so no Date
|
||||
// round-trip (and no timezone ambiguity) is needed for these queries.
|
||||
const points = await Promise.all(
|
||||
matchingDates.map(async (dateStr) => {
|
||||
const windowStart = `${dateStr} ${startTime}:00`;
|
||||
const windowEnd = `${dateStr} ${endTime}:00`;
|
||||
const rows = await query<{ steamid: string }>(
|
||||
`SELECT DISTINCT s.steamid
|
||||
FROM playtime_display_sessions s
|
||||
WHERE s.session_start_dt < ?
|
||||
AND (s.session_end_dt IS NULL OR s.session_end_dt > ?)`,
|
||||
[windowEnd, windowStart],
|
||||
);
|
||||
return { date: dateStr, count: rows.length };
|
||||
}),
|
||||
);
|
||||
|
||||
return NextResponse.json({ points, truncated });
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import { getCountriesSummary } from '@/lib/queries';
|
||||
import { countryCodeToFlag, countryCodeToName } from '@/lib/steam';
|
||||
import { countryCodeToFlag, countryCodeToName, formatMinutes } from '@/lib/steam';
|
||||
|
||||
// This page queries the DB directly with no dynamic route segment, so
|
||||
// without this Next.js tries to statically pre-render it at BUILD time
|
||||
@@ -23,22 +23,29 @@ export default async function CountriesPage() {
|
||||
<div className="panel p-6 text-sm text-ink-muted">No countries recorded yet.</div>
|
||||
) : (
|
||||
<div className="panel divide-y divide-base-border">
|
||||
<div className="grid grid-cols-[1fr_auto_auto] gap-6 px-4 py-2 text-xs text-ink-faint uppercase tracking-wide">
|
||||
<div>Country</div>
|
||||
<div className="text-right w-24">Total playtime</div>
|
||||
<div className="text-right w-24">Avg per player</div>
|
||||
</div>
|
||||
{countries.map((c) => (
|
||||
<Link
|
||||
key={c.current_country}
|
||||
href={`/countries/${encodeURIComponent(c.current_country)}`}
|
||||
className="flex items-center justify-between px-4 py-3 hover:bg-base transition-colors"
|
||||
className="grid grid-cols-[1fr_auto_auto] gap-6 items-center px-4 py-3 hover:bg-base transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-lg" aria-hidden>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-lg shrink-0" aria-hidden>
|
||||
{countryCodeToFlag(c.current_country)}
|
||||
</span>
|
||||
<span className="text-ink text-sm">{countryCodeToName(c.current_country)}</span>
|
||||
<span className="text-ink-faint text-xs mono">{c.current_country}</span>
|
||||
</div>
|
||||
<div className="text-sm text-ink-muted mono">
|
||||
{c.player_count} player{c.player_count === 1 ? '' : 's'}
|
||||
<span className="text-ink text-sm truncate">{countryCodeToName(c.current_country)}</span>
|
||||
<span className="text-ink-faint text-xs mono shrink-0">{c.current_country}</span>
|
||||
<span className="text-ink-muted text-xs mono shrink-0">
|
||||
{c.player_count} player{c.player_count === 1 ? '' : 's'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-ink mono text-right w-24">{formatMinutes(c.total_minutes)}</div>
|
||||
<div className="text-sm text-ink-muted mono text-right w-24">{formatMinutes(Math.round(c.avg_minutes))}</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-base text-ink font-sans antialiased;
|
||||
background-image: radial-gradient(ellipse 70% 45% at 50% -10%, rgba(63, 211, 122, 0.07), transparent 70%);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@@ -20,7 +22,11 @@
|
||||
|
||||
@layer components {
|
||||
.panel {
|
||||
@apply bg-base-panel border border-base-border rounded-lg;
|
||||
@apply bg-base-panel border border-base-border rounded-xl;
|
||||
background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.025), rgba(255, 255, 255, 0) 40%);
|
||||
box-shadow:
|
||||
0 1px 0 0 rgba(255, 255, 255, 0.04) inset,
|
||||
0 12px 32px -16px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@@ -30,4 +36,13 @@
|
||||
.mono {
|
||||
@apply font-mono text-sm;
|
||||
}
|
||||
|
||||
/* Subtle lift + accent-tinted background on interactive rows, used
|
||||
across search results, tables, and lists so it's consistent everywhere. */
|
||||
.row-interactive {
|
||||
@apply transition-all duration-150;
|
||||
}
|
||||
.row-interactive:hover {
|
||||
background-color: rgba(63, 211, 122, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<header className="border-b border-base-border">
|
||||
<header className="border-b border-base-border sticky top-0 z-10 bg-base/80 backdrop-blur-md">
|
||||
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-accent shadow-[0_0_8px_theme(colors.accent.DEFAULT)]" />
|
||||
@@ -32,9 +32,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-sm text-ink-muted hover:text-accent transition-colors"
|
||||
className="relative text-sm text-ink-muted hover:text-accent transition-colors group py-1"
|
||||
>
|
||||
{item.label}
|
||||
<span className="absolute left-0 -bottom-0.5 h-px w-0 bg-accent transition-all duration-200 group-hover:w-full" />
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
@@ -17,6 +17,11 @@ export default async function MapPeriodsPage({
|
||||
notFound();
|
||||
}
|
||||
|
||||
const totalMinutes = periods.reduce((sum, p) => {
|
||||
const end = p.map_end_dt ?? new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||
return sum + (diffMinutes(p.map_start_dt, end) ?? 0);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Link href="/maps" className="text-sm text-ink-muted hover:text-accent transition-colors">
|
||||
@@ -26,24 +31,49 @@ export default async function MapPeriodsPage({
|
||||
<div>
|
||||
<h1 className="text-2xl text-ink mb-1">{mapName}</h1>
|
||||
<p className="text-sm text-ink-muted">
|
||||
Played {periods.length} time{periods.length === 1 ? '' : 's'}.
|
||||
Played {periods.length} time{periods.length === 1 ? '' : 's'} · {formatMinutes(totalMinutes)} total
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="panel divide-y divide-base-border">
|
||||
<div className="grid grid-cols-[1fr_auto_auto] gap-4 px-4 py-2 text-xs text-ink-faint uppercase tracking-wide">
|
||||
<div>Session</div>
|
||||
<div className="text-right w-16">Players</div>
|
||||
<div className="text-right w-20">Duration</div>
|
||||
</div>
|
||||
{periods.map((p) => (
|
||||
<Link
|
||||
key={p.map_id}
|
||||
href={`/maps/period/${p.map_id}`}
|
||||
className="flex items-center justify-between px-4 py-3 hover:bg-base transition-colors"
|
||||
className="group grid grid-cols-[1fr_auto_auto] gap-4 items-center px-4 py-3 hover:bg-base transition-colors"
|
||||
>
|
||||
<div>
|
||||
<div className="text-ink text-sm">
|
||||
{new Date(p.map_start_dt.replace(' ', 'T')).toLocaleString()}
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div>
|
||||
<div className="text-ink text-sm">
|
||||
{new Date(p.map_start_dt.replace(' ', 'T')).toLocaleString()}
|
||||
{p.map_end_dt && (
|
||||
<span className="text-ink-faint">
|
||||
{' – '}
|
||||
{new Date(p.map_end_dt.replace(' ', 'T')).toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!p.map_end_dt && <div className="text-xs text-accent">currently running</div>}
|
||||
</div>
|
||||
{!p.map_end_dt && <div className="text-xs text-accent">currently running</div>}
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-ink-faint group-hover:text-accent group-hover:translate-x-0.5 transition-all shrink-0 ml-auto"
|
||||
>
|
||||
<path d="M9 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="mono text-ink-faint text-sm">
|
||||
<div className="mono text-ink-muted text-sm text-right w-16">{p.player_count}</div>
|
||||
<div className="mono text-ink-faint text-sm text-right w-20">
|
||||
{p.map_end_dt ? formatMinutes(diffMinutes(p.map_start_dt, p.map_end_dt)) : '—'}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -98,74 +98,75 @@ export default async function MapPeriodDetailPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Players present */}
|
||||
<div className="panel p-6">
|
||||
<div className="stat-label mb-3">
|
||||
Players present ({players.length})
|
||||
</div>
|
||||
{players.length === 0 ? (
|
||||
<div className="text-sm text-ink-muted">No recorded players during this period.</div>
|
||||
) : (
|
||||
<div className="divide-y divide-base-border">
|
||||
{players.map((p, i) => (
|
||||
<Link
|
||||
key={`${p.steamid}-${i}`}
|
||||
href={`/players/${encodeURIComponent(p.steamid)}`}
|
||||
className="flex items-center justify-between py-2 text-sm hover:bg-base transition-colors px-2 -mx-2 rounded"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span aria-hidden>{countryCodeToFlag(p.current_country)}</span>
|
||||
<span className="text-ink">{p.current_name}</span>
|
||||
</div>
|
||||
<div className="text-ink-faint text-xs mono">
|
||||
{new Date(p.session_start_dt.replace(' ', 'T')).toLocaleTimeString()}
|
||||
{' – '}
|
||||
{p.session_end_dt
|
||||
? new Date(p.session_end_dt.replace(' ', 'T')).toLocaleTimeString()
|
||||
: 'now'}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{/* Players present + Map votes, side by side */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="panel p-6">
|
||||
<div className="stat-label mb-3">
|
||||
Players present ({players.length})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Votes */}
|
||||
<div className="panel p-6">
|
||||
<div className="stat-label mb-3">Map votes</div>
|
||||
{voteEvents.size === 0 ? (
|
||||
<div className="text-sm text-ink-muted">No recorded vote during this period.</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{[...voteEvents.entries()].map(([voteId, event]) => (
|
||||
<div key={voteId}>
|
||||
<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()}
|
||||
{players.length === 0 ? (
|
||||
<div className="text-sm text-ink-muted">No recorded players during this period.</div>
|
||||
) : (
|
||||
<div className="divide-y divide-base-border">
|
||||
{players.map((p, i) => (
|
||||
<Link
|
||||
key={`${p.steamid}-${i}`}
|
||||
href={`/players/${encodeURIComponent(p.steamid)}`}
|
||||
className="flex items-center justify-between py-2 text-sm hover:bg-base transition-colors px-2 -mx-2 rounded"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span aria-hidden>{countryCodeToFlag(p.current_country)}</span>
|
||||
<span className="text-ink">{p.current_name}</span>
|
||||
</div>
|
||||
<div className="text-ink-faint text-xs mono">
|
||||
{new Date(p.session_start_dt.replace(' ', 'T')).toLocaleTimeString()}
|
||||
{' – '}
|
||||
{p.session_end_dt
|
||||
? new Date(p.session_end_dt.replace(' ', 'T')).toLocaleTimeString()
|
||||
: 'now'}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="panel p-6">
|
||||
<div className="stat-label mb-3">Map votes</div>
|
||||
{voteEvents.size === 0 ? (
|
||||
<div className="text-sm text-ink-muted">No recorded vote during this period.</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{[...voteEvents.entries()].map(([voteId, event]) => (
|
||||
<div key={voteId}>
|
||||
<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()}
|
||||
</div>
|
||||
<div className="text-sm text-accent">Result: {event.result ?? '—'}</div>
|
||||
</div>
|
||||
<div className="divide-y divide-base-border">
|
||||
{event.choices.map((c, i) => (
|
||||
<Link
|
||||
key={`${c.steamid}-${i}`}
|
||||
href={`/players/${encodeURIComponent(c.steamid)}`}
|
||||
className="flex items-center justify-between py-2 text-sm hover:bg-base transition-colors px-2 -mx-2 rounded"
|
||||
>
|
||||
<span className="text-ink">{c.current_name}</span>
|
||||
<span className="text-ink-muted">
|
||||
voted <span className="text-ink">{c.vote_choice}</span>
|
||||
{c.vote_weight !== 1 && (
|
||||
<span className="text-ink-faint"> (weight {c.vote_weight})</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm text-accent">Result: {event.result ?? '—'}</div>
|
||||
</div>
|
||||
<div className="divide-y divide-base-border">
|
||||
{event.choices.map((c, i) => (
|
||||
<Link
|
||||
key={`${c.steamid}-${i}`}
|
||||
href={`/players/${encodeURIComponent(c.steamid)}`}
|
||||
className="flex items-center justify-between py-2 text-sm hover:bg-base transition-colors px-2 -mx-2 rounded"
|
||||
>
|
||||
<span className="text-ink">{c.current_name}</span>
|
||||
<span className="text-ink-muted">
|
||||
voted <span className="text-ink">{c.vote_choice}</span>
|
||||
{c.vote_weight !== 1 && (
|
||||
<span className="text-ink-faint"> (weight {c.vote_weight})</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PopulationChart from '@/components/PopulationChart';
|
||||
import MapPopulationChart from '@/components/MapPopulationChart';
|
||||
import RecurringWindowChart from '@/components/RecurringWindowChart';
|
||||
|
||||
export default function OverviewPage() {
|
||||
return (
|
||||
@@ -10,6 +11,7 @@ export default function OverviewPage() {
|
||||
</div>
|
||||
<PopulationChart />
|
||||
<MapPopulationChart />
|
||||
<RecurringWindowChart />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user