some more styling to the frontend
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getPlayersPresentDuringMap } from '@/lib/queries';
|
||||
import { getSteamSummariesBatch, DEFAULT_AVATAR_URL } from '@/lib/steamApi';
|
||||
import { steam2ToSteam64 } from '@/lib/steam';
|
||||
import { attachAvatars } from '@/lib/steamApi';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const mapIdParam = req.nextUrl.searchParams.get('mapId');
|
||||
@@ -10,21 +9,14 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'mapId query param is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const players = await getPlayersPresentDuringMap(mapId);
|
||||
const rows = await getPlayersPresentDuringMap(mapId);
|
||||
const enriched = await attachAvatars(rows);
|
||||
const players = enriched.map((p) => ({
|
||||
steamid: p.steamid,
|
||||
name: p.current_name,
|
||||
country: p.current_country,
|
||||
avatarUrl: p.avatarUrl,
|
||||
}));
|
||||
|
||||
const steam64ByPlayer = new Map<string, string>();
|
||||
for (const p of players) {
|
||||
const id64 = steam2ToSteam64(p.steamid);
|
||||
if (id64) steam64ByPlayer.set(p.steamid, id64);
|
||||
}
|
||||
|
||||
const avatars = await getSteamSummariesBatch([...steam64ByPlayer.values()]);
|
||||
|
||||
const result = players.map((p) => {
|
||||
const id64 = steam64ByPlayer.get(p.steamid);
|
||||
const avatarUrl = (id64 && avatars.get(id64)?.avatarUrl) || DEFAULT_AVATAR_URL;
|
||||
return { steamid: p.steamid, name: p.current_name, avatarUrl };
|
||||
});
|
||||
|
||||
return NextResponse.json({ players: result });
|
||||
return NextResponse.json({ players });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db';
|
||||
import { attachAvatars } from '@/lib/steamApi';
|
||||
|
||||
interface PlayerRow {
|
||||
steamid: string;
|
||||
@@ -7,6 +8,7 @@ interface PlayerRow {
|
||||
current_country: string | null;
|
||||
matched_name: string | null;
|
||||
total_minutes: number | null;
|
||||
last_connected: string | null;
|
||||
}
|
||||
|
||||
const TOTAL_MINUTES_SUBQUERY = `
|
||||
@@ -17,6 +19,12 @@ const TOTAL_MINUTES_SUBQUERY = `
|
||||
LIMIT 1) AS total_minutes
|
||||
`;
|
||||
|
||||
const LAST_CONNECTED_SUBQUERY = `
|
||||
(SELECT MAX(s.session_end_dt)
|
||||
FROM playtime_display_sessions s
|
||||
WHERE s.steamid = p.steamid) AS last_connected
|
||||
`;
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const q = req.nextUrl.searchParams.get('q')?.trim() ?? '';
|
||||
const sort = req.nextUrl.searchParams.get('sort') ?? 'recent';
|
||||
@@ -28,37 +36,41 @@ export async function GET(req: NextRequest) {
|
||||
? 'p.current_name'
|
||||
: 'p.last_seen DESC';
|
||||
|
||||
// No search term: just list players, ordered per the chosen sort.
|
||||
let rows: PlayerRow[];
|
||||
|
||||
if (!q) {
|
||||
const rows = await query<PlayerRow>(
|
||||
// No search term: just list players, ordered per the chosen sort.
|
||||
rows = await query<PlayerRow>(
|
||||
`SELECT p.steamid, p.current_name, p.current_country, NULL AS matched_name,
|
||||
${TOTAL_MINUTES_SUBQUERY}
|
||||
${TOTAL_MINUTES_SUBQUERY},
|
||||
${LAST_CONNECTED_SUBQUERY}
|
||||
FROM playtime_display_players p
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT 50`,
|
||||
);
|
||||
return NextResponse.json({ players: rows });
|
||||
} else {
|
||||
// Search matches steamid directly, or current/previous names.
|
||||
const like = `%${q}%`;
|
||||
rows = await query<PlayerRow>(
|
||||
`SELECT p.steamid, p.current_name, p.current_country, h.player_name AS matched_name,
|
||||
${TOTAL_MINUTES_SUBQUERY},
|
||||
${LAST_CONNECTED_SUBQUERY}
|
||||
FROM playtime_display_players p
|
||||
LEFT JOIN playtime_display_name_history h
|
||||
ON h.steamid = p.steamid AND h.player_name LIKE ?
|
||||
WHERE p.steamid LIKE ?
|
||||
OR p.current_name LIKE ?
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM playtime_display_name_history h2
|
||||
WHERE h2.steamid = p.steamid AND h2.player_name LIKE ?
|
||||
)
|
||||
GROUP BY p.steamid
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT 50`,
|
||||
[like, like, like, like],
|
||||
);
|
||||
}
|
||||
|
||||
// Search matches steamid directly, or current/previous names.
|
||||
const like = `%${q}%`;
|
||||
const rows = await query<PlayerRow>(
|
||||
`SELECT p.steamid, p.current_name, p.current_country, h.player_name AS matched_name,
|
||||
${TOTAL_MINUTES_SUBQUERY}
|
||||
FROM playtime_display_players p
|
||||
LEFT JOIN playtime_display_name_history h
|
||||
ON h.steamid = p.steamid AND h.player_name LIKE ?
|
||||
WHERE p.steamid LIKE ?
|
||||
OR p.current_name LIKE ?
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM playtime_display_name_history h2
|
||||
WHERE h2.steamid = p.steamid AND h2.player_name LIKE ?
|
||||
)
|
||||
GROUP BY p.steamid
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT 50`,
|
||||
[like, like, like, like],
|
||||
);
|
||||
|
||||
return NextResponse.json({ players: rows });
|
||||
const players = await attachAvatars(rows);
|
||||
return NextResponse.json({ players });
|
||||
}
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getCurrentPlayers } from '@/lib/queries';
|
||||
import { getSteamSummariesBatch, DEFAULT_AVATAR_URL } from '@/lib/steamApi';
|
||||
import { steam2ToSteam64 } from '@/lib/steam';
|
||||
import { attachAvatars } from '@/lib/steamApi';
|
||||
|
||||
export async function GET() {
|
||||
const players = await getCurrentPlayers(3);
|
||||
const rows = await getCurrentPlayers(3);
|
||||
const enriched = await attachAvatars(rows);
|
||||
const players = enriched.map((p) => ({
|
||||
steamid: p.steamid,
|
||||
name: p.current_name,
|
||||
country: p.current_country,
|
||||
avatarUrl: p.avatarUrl,
|
||||
}));
|
||||
|
||||
const steam64ByPlayer = new Map<string, string>();
|
||||
for (const p of players) {
|
||||
const id64 = steam2ToSteam64(p.steamid);
|
||||
if (id64) steam64ByPlayer.set(p.steamid, id64);
|
||||
}
|
||||
|
||||
const avatars = await getSteamSummariesBatch([...steam64ByPlayer.values()]);
|
||||
|
||||
const result = players.map((p) => {
|
||||
const id64 = steam64ByPlayer.get(p.steamid);
|
||||
const avatarUrl = (id64 && avatars.get(id64)?.avatarUrl) || DEFAULT_AVATAR_URL;
|
||||
return { steamid: p.steamid, name: p.current_name, avatarUrl };
|
||||
});
|
||||
|
||||
return NextResponse.json({ players: result, count: result.length });
|
||||
return NextResponse.json({ players, count: players.length });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getPlayersAtTime } from '@/lib/queries';
|
||||
import { getSteamSummariesBatch, DEFAULT_AVATAR_URL } from '@/lib/steamApi';
|
||||
import { steam2ToSteam64 } from '@/lib/steam';
|
||||
import { attachAvatars } from '@/lib/steamApi';
|
||||
import { formatDbDateTime } from '@/lib/timezone';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
@@ -15,21 +14,14 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'invalid at timestamp' }, { status: 400 });
|
||||
}
|
||||
|
||||
const players = await getPlayersAtTime(formatDbDateTime(atDate));
|
||||
const rows = await getPlayersAtTime(formatDbDateTime(atDate));
|
||||
const enriched = await attachAvatars(rows);
|
||||
const players = enriched.map((p) => ({
|
||||
steamid: p.steamid,
|
||||
name: p.current_name,
|
||||
country: p.current_country,
|
||||
avatarUrl: p.avatarUrl,
|
||||
}));
|
||||
|
||||
const steam64ByPlayer = new Map<string, string>();
|
||||
for (const p of players) {
|
||||
const id64 = steam2ToSteam64(p.steamid);
|
||||
if (id64) steam64ByPlayer.set(p.steamid, id64);
|
||||
}
|
||||
|
||||
const avatars = await getSteamSummariesBatch([...steam64ByPlayer.values()]);
|
||||
|
||||
const result = players.map((p) => {
|
||||
const id64 = steam64ByPlayer.get(p.steamid);
|
||||
const avatarUrl = (id64 && avatars.get(id64)?.avatarUrl) || DEFAULT_AVATAR_URL;
|
||||
return { steamid: p.steamid, name: p.current_name, avatarUrl };
|
||||
});
|
||||
|
||||
return NextResponse.json({ players: result });
|
||||
return NextResponse.json({ players });
|
||||
}
|
||||
|
||||
+16
-24
@@ -1,12 +1,12 @@
|
||||
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 { attachAvatars } from '@/lib/steamApi';
|
||||
import { parseDbDateTime } from '@/lib/timezone';
|
||||
|
||||
interface SessionRow {
|
||||
steamid: string;
|
||||
current_name: string;
|
||||
current_country: string | null;
|
||||
session_start_dt: string;
|
||||
session_end_dt: string | null;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export async function GET(req: NextRequest) {
|
||||
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,
|
||||
`SELECT s.steamid, COALESCE(p.current_name, s.player_name) AS current_name, p.current_country,
|
||||
s.session_start_dt, s.session_end_dt
|
||||
FROM playtime_display_sessions s
|
||||
LEFT JOIN playtime_display_players p ON p.steamid = s.steamid
|
||||
@@ -44,7 +44,10 @@ export async function GET(req: NextRequest) {
|
||||
startMin: number;
|
||||
endMin: number;
|
||||
}
|
||||
const byPlayer = new Map<string, { name: string; segments: Segment[]; totalMinutes: number }>();
|
||||
const byPlayer = new Map<
|
||||
string,
|
||||
{ name: string; country: string | null; segments: Segment[]; totalMinutes: number }
|
||||
>();
|
||||
|
||||
for (const r of rows) {
|
||||
const sStart = parseDbDateTime(r.session_start_dt).getTime();
|
||||
@@ -57,33 +60,22 @@ export async function GET(req: NextRequest) {
|
||||
const endMin = Math.round((clippedEnd - windowStartMs) / 60000);
|
||||
|
||||
if (!byPlayer.has(r.steamid)) {
|
||||
byPlayer.set(r.steamid, { name: r.current_name, segments: [], totalMinutes: 0 });
|
||||
byPlayer.set(r.steamid, { name: r.current_name, country: r.current_country, 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 unenriched = [...byPlayer.entries()].map(([steamid, data]) => ({
|
||||
steamid,
|
||||
name: data.name,
|
||||
country: data.country,
|
||||
totalMinutes: data.totalMinutes,
|
||||
segments: data.segments,
|
||||
}));
|
||||
|
||||
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);
|
||||
const players = (await attachAvatars(unenriched)).sort((a, b) => b.totalMinutes - a.totalMinutes);
|
||||
|
||||
return NextResponse.json({ windowMinutes, players });
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { getPlayersByCountry } from '@/lib/queries';
|
||||
import { attachAvatars } from '@/lib/steamApi';
|
||||
import { countryCodeToFlag, countryCodeToName, formatMinutes } from '@/lib/steam';
|
||||
|
||||
// Direct DB query, no dynamic route data needed at build time — same
|
||||
// reasoning as the /countries listing page.
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function CountryPlayersPage({
|
||||
params,
|
||||
searchParams,
|
||||
@@ -11,15 +16,20 @@ export default async function CountryPlayersPage({
|
||||
searchParams: Promise<{ sort?: string }>;
|
||||
}) {
|
||||
const { code: rawCode } = await params;
|
||||
const code = decodeURIComponent(rawCode).toUpperCase();
|
||||
const decoded = decodeURIComponent(rawCode);
|
||||
const isUnknown = decoded.toLowerCase() === 'unknown';
|
||||
const code = isUnknown ? null : decoded.toUpperCase();
|
||||
|
||||
const { sort: rawSort } = await searchParams;
|
||||
const sort = rawSort === 'playtime' ? 'playtime' : 'recent';
|
||||
|
||||
const players = await getPlayersByCountry(code, sort);
|
||||
if (players.length === 0) {
|
||||
const rows = await getPlayersByCountry(code, sort);
|
||||
if (rows.length === 0) {
|
||||
notFound();
|
||||
}
|
||||
const players = await attachAvatars(rows);
|
||||
|
||||
const linkBase = isUnknown ? '/countries/unknown' : `/countries/${encodeURIComponent(code!)}`;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -40,7 +50,7 @@ export default async function CountryPlayersPage({
|
||||
|
||||
<div className="flex gap-2 text-sm">
|
||||
<Link
|
||||
href={`/countries/${encodeURIComponent(code)}?sort=recent`}
|
||||
href={`${linkBase}?sort=recent`}
|
||||
className={`px-3 py-1.5 rounded border transition-colors ${
|
||||
sort === 'recent'
|
||||
? 'border-accent/50 text-accent bg-accent-dim/20'
|
||||
@@ -50,7 +60,7 @@ export default async function CountryPlayersPage({
|
||||
Most recently played
|
||||
</Link>
|
||||
<Link
|
||||
href={`/countries/${encodeURIComponent(code)}?sort=playtime`}
|
||||
href={`${linkBase}?sort=playtime`}
|
||||
className={`px-3 py-1.5 rounded border transition-colors ${
|
||||
sort === 'playtime'
|
||||
? 'border-accent/50 text-accent bg-accent-dim/20'
|
||||
@@ -69,7 +79,15 @@ export default async function CountryPlayersPage({
|
||||
href={`/players/${encodeURIComponent(p.steamid)}`}
|
||||
className="flex items-center justify-between px-4 py-3 hover:bg-base transition-colors"
|
||||
>
|
||||
<div className="text-ink text-sm">{p.current_name}</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={p.avatarUrl}
|
||||
alt=""
|
||||
className="w-9 h-9 rounded-full object-cover ring-1 ring-base-border shrink-0"
|
||||
/>
|
||||
<div className="text-ink text-sm">{p.current_name}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs">
|
||||
<span className="text-ink-faint mono">{p.steamid}</span>
|
||||
<span className="text-ink-muted mono w-16 text-right">
|
||||
|
||||
@@ -30,8 +30,8 @@ export default async function CountriesPage() {
|
||||
</div>
|
||||
{countries.map((c) => (
|
||||
<Link
|
||||
key={c.current_country}
|
||||
href={`/countries/${encodeURIComponent(c.current_country)}`}
|
||||
key={c.current_country ?? 'unknown'}
|
||||
href={`/countries/${c.current_country ? encodeURIComponent(c.current_country) : 'unknown'}`}
|
||||
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 min-w-0">
|
||||
@@ -39,7 +39,9 @@ export default async function CountriesPage() {
|
||||
{countryCodeToFlag(c.current_country)}
|
||||
</span>
|
||||
<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>
|
||||
{c.current_country && (
|
||||
<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>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getMapVotes,
|
||||
getAdjacentMaps,
|
||||
} from '@/lib/queries';
|
||||
import { attachAvatars } from '@/lib/steamApi';
|
||||
import { diffMinutes } from '@/lib/dates';
|
||||
import { formatMinutes, countryCodeToFlag } from '@/lib/steam';
|
||||
|
||||
@@ -26,11 +27,12 @@ export default async function MapPeriodDetailPage({
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [players, votes, adjacent] = await Promise.all([
|
||||
const [playersRaw, votesRaw, adjacent] = await Promise.all([
|
||||
getPlayersPresentDuringMap(mapId),
|
||||
getMapVotes(mapId),
|
||||
getAdjacentMaps(period.map_start_dt),
|
||||
]);
|
||||
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).
|
||||
@@ -115,6 +117,12 @@ export default async function MapPeriodDetailPage({
|
||||
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">
|
||||
{/* 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"
|
||||
/>
|
||||
<span aria-hidden>{countryCodeToFlag(p.current_country)}</span>
|
||||
<span className="text-ink">{p.current_name}</span>
|
||||
</div>
|
||||
@@ -152,7 +160,16 @@ export default async function MapPeriodDetailPage({
|
||||
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>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={c.avatarUrl}
|
||||
alt=""
|
||||
className="w-7 h-7 rounded-full object-cover ring-1 ring-base-border shrink-0"
|
||||
/>
|
||||
<span aria-hidden>{countryCodeToFlag(c.current_country)}</span>
|
||||
<span className="text-ink">{c.current_name}</span>
|
||||
</div>
|
||||
<span className="text-ink-muted">
|
||||
voted <span className="text-ink">{c.vote_choice}</span>
|
||||
{c.vote_weight !== 1 && (
|
||||
|
||||
Reference in New Issue
Block a user