mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
bb04782e11
Bumps both frontend apps (site, admin-ui): - vite 5.4 -> 7.3.6 (pulls esbuild 0.28, patched dev-server CVEs) - @vitejs/plugin-react 4 -> 5 (babel 7.29.7) - postcss 8.4 -> 8.5.25 - react-router-dom 6.27 -> react-router 8.3.0 (package renamed upstream in v8; imports swapped in src). Fixes the open-redirect advisories with no patched 6.x, plus the RSC CSRF advisory (GHSA-qwww-vcr4-c8h2) that only 8.3.0 patches. - react/react-dom 18.3 -> 19.2 (required by react-router 8) npm audit: 0 vulnerabilities in both apps. tsc + vite build clean.
112 lines
4.5 KiB
TypeScript
112 lines
4.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Link, useParams } from 'react-router';
|
|
import { get } from '../lib/api';
|
|
import type { SitePhoto } from '../lib/types';
|
|
import { PlayerAvatar } from '../components/PlayerAvatar';
|
|
import { absoluteTime, num, relativeTime } from '../lib/format';
|
|
|
|
export function PhotoDetail() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const [photo, setPhoto] = useState<SitePhoto | null>(null);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
setErr(null);
|
|
setPhoto(null);
|
|
get<SitePhoto>(`/photos/${id}`).then(setPhoto).catch(e => setErr((e as Error).message));
|
|
}, [id]);
|
|
|
|
if (err) return (
|
|
<div className="card !p-6">
|
|
<h2 className="text-lg font-semibold text-ink-50">Photo not found</h2>
|
|
<p className="text-sm text-ink-400 mt-1">{err}</p>
|
|
<Link to="/feed" className="btn-secondary text-sm mt-4">← Back to feed</Link>
|
|
</div>
|
|
);
|
|
|
|
if (!photo) return <div className="py-10 text-center text-xs text-ink-400">Loading…</div>;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Link to="/feed" className="btn-ghost text-xs">← Back to feed</Link>
|
|
|
|
<div className="card overflow-hidden grid grid-cols-1 lg:grid-cols-[1fr,360px]">
|
|
<div className="bg-ink-950 flex items-center justify-center">
|
|
<img
|
|
src={photo.imageUrl}
|
|
alt={photo.caption || `Photo #${photo.id}`}
|
|
className="w-full h-auto max-h-[80vh] object-contain"
|
|
/>
|
|
</div>
|
|
<aside className="p-5 border-t lg:border-t-0 lg:border-l border-ink-800 flex flex-col gap-4">
|
|
<div>
|
|
<Link
|
|
to={`/players/${photo.uploaderPlayerId}`}
|
|
className="flex items-center gap-3 group"
|
|
>
|
|
<PlayerAvatar
|
|
name={photo.uploaderProfileImageName}
|
|
displayName={photo.uploaderDisplayName}
|
|
size={44}
|
|
/>
|
|
<div className="min-w-0">
|
|
<div className="font-semibold text-ink-50 group-hover:text-brand-200 truncate">
|
|
{photo.uploaderDisplayName}
|
|
</div>
|
|
<div className="text-xs text-ink-400 truncate">@{photo.uploaderUsername}</div>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
|
|
{photo.caption && (
|
|
<p className="text-sm text-ink-200 whitespace-pre-wrap">{photo.caption}</p>
|
|
)}
|
|
|
|
{(photo.taggedPlayers ?? []).length > 0 && (
|
|
<div className="space-y-2">
|
|
<div className="text-[10px] uppercase tracking-widest text-ink-400">Players in photo</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{photo.taggedPlayers.map(p => (
|
|
<Link
|
|
key={p.id}
|
|
to={`/players/${p.id}`}
|
|
className="inline-flex max-w-full items-center gap-2 rounded-md border border-ink-800 bg-ink-900/60 px-2 py-1 text-xs text-ink-200 hover:bg-ink-800/80 hover:text-ink-50"
|
|
>
|
|
<PlayerAvatar name={p.profileImageName} displayName={p.displayName || p.username} size={22} />
|
|
<span className="truncate">{p.displayName || p.username}</span>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
<div className="rounded-md border border-ink-800 bg-ink-900/60 px-3 py-2">
|
|
<div className="text-[10px] uppercase tracking-widest text-ink-400">Cheers</div>
|
|
<div className="text-sm font-semibold text-ink-50 tabular-nums">{num(photo.cheerCount)}</div>
|
|
</div>
|
|
<div className="rounded-md border border-ink-800 bg-ink-900/60 px-3 py-2">
|
|
<div className="text-[10px] uppercase tracking-widest text-ink-400">Views</div>
|
|
<div className="text-sm font-semibold text-ink-50 tabular-nums">{num(photo.viewCount)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-xs text-ink-400 space-y-1">
|
|
{photo.roomName && (
|
|
<div>
|
|
<span className="text-ink-500">In room: </span>
|
|
<span className="font-mono text-ink-200">^{photo.roomName}</span>
|
|
</div>
|
|
)}
|
|
<div title={absoluteTime(photo.createdAt)}>
|
|
<span className="text-ink-500">Posted: </span>
|
|
<span className="text-ink-200">{relativeTime(photo.createdAt)}</span>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|