Files
EpicNext-Cms/src/app/radio/contests/[id]/page.tsx
T
openhands 5c638cd6bc perf: add bans.user_id index, Redis cache layer, rate-limit improvements, radio contest/giveaway columns, and tests
- Add DB index on bans.user_id to speed up per-request ban lookups (migration 0008)
- Replace in-process rate limiter with Redis-backed implementation with in-memory fallback
- Add Redis caching layer for site settings with TTL invalidation (migration 0009)
- Add rate limiting to resetPassword to prevent token brute-force attacks
- Update all rateLimit callers to await the now-async function
- Flesh out RadioContests and RadioGiveaways models with title, description, prize, date, and winner columns
- Update radio contest/giveaway pages to display new fields
- Add tests for rate limiter (4 tests) and password-reset actions (3 tests)
- Add REDIS_URL environment variable (optional, falls back to in-memory)
2026-07-08 12:49:24 +02:00

79 lines
2.2 KiB
TypeScript

import Link from "next/link";
import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { ContentCard } from "@/components/public/ui";
import { prisma } from "@/lib/prisma";
export const revalidate = 300;
function formatDate(d: Date | null | undefined): string {
return d ? d.toISOString().slice(0, 19).replace("T", " ") : "";
}
export default async function RadioContestDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const t = await getTranslations("pages.radioContests");
let contestId: bigint;
try {
contestId = BigInt(id);
} catch {
notFound();
}
const contest = await prisma.radioContests
.findUnique({ where: { id: contestId! } })
.catch(() => null);
if (!contest) notFound();
const active = contest.isActive ? "Active" : "Ended";
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<p className="muted" style={{ margin: 0 }}>
<Link href="/radio/contests">← Back to contests</Link>
</p>
<ContentCard icon="🎉" title={contest.title || `Contest #${contest.id}`} subtitle={active}>
<table style={{ width: "100%" }}>
<tbody>
{contest.description ? (
<tr>
<th style={{ verticalAlign: "top", width: 120 }}>Description</th>
<td>{contest.description}</td>
</tr>
) : null}
{contest.prize ? (
<tr>
<th>Prize</th>
<td>{contest.prize}</td>
</tr>
) : null}
<tr>
<th>Period</th>
<td>
{contest.startDate ? formatDate(contest.startDate) : "—"}
{" — "}
{contest.endDate ? formatDate(contest.endDate) : "ongoing"}
</td>
</tr>
<tr>
<th>Created</th>
<td>{formatDate(contest.createdAt) || <span className="muted">—</span>}</td>
</tr>
<tr>
<th>Updated</th>
<td>{formatDate(contest.updatedAt) || <span className="muted">—</span>}</td>
</tr>
</tbody>
</table>
</ContentCard>
</main>
);
}