Add Theme Settings tool + polish remaining radio detail pages

- New /admin/theme: recolour the whole site from housekeeping. 6 atom-
  faithful presets (Atom/Midnight/Ocean/Forest/Sunset/Candy) + per-colour
  pickers for the 12 settings ThemeVars injects + border radius. Writes to
  website_settings, busts the siteSettings cache, and revalidates the
  layout so the new palette applies live with no rebuild. Constants live
  in src/lib/theme-presets.ts (a "use server" file can't export objects).
  Added to the admin sidebar (System).
- radio/contests/[id] + giveaways/[id] wrapped in ContentCard to match
  the public design system.
- Skipped a separate VPN page: /admin/ip already manages the IP
  white/blacklist, so it would only duplicate it.

Verified live (prod, amx_test): applied the Ocean preset → home renders
--color-primary #0ea5e9 site-wide; reverted the test rows. tsc 0,
vitest 49/49, next build 0.
This commit is contained in:
Simo committed 2026-06-28 20:20:38 +02:00
1 parent 6f15e0c8a3
commit 1199176883
6 files changed
+322 -10

No files matched your search

+5 -5
View File
@@ -1,5 +1,6 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { ContentCard } from "@/components/public/ui";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
@@ -33,13 +34,12 @@ export default async function RadioContestDetailPage({
const contestIdStr = contest.id.toString();
return (
<main>
<p className="muted" style={{ margin: "0 0 0.5rem" }}>
<main style={{ display: "grid", gap: "1.5rem" }}>
<p className="muted" style={{ margin: 0 }}>
<Link href="/radio/contests">← Back to contests</Link>
</p>
<div className="card">
<h1 style={{ margin: "0 0 0.5rem" }}>Contest #{contestIdStr}</h1>
<ContentCard icon="🎉" title={`Contest #${contestIdStr}`} subtitle="Radio contest details">
<table>
<tbody>
<tr>
@@ -52,7 +52,7 @@ export default async function RadioContestDetailPage({
</tr>
</tbody>
</table>
</div>
</ContentCard>
</main>
);
}
+5 -5
View File
@@ -1,5 +1,6 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { ContentCard } from "@/components/public/ui";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
@@ -33,13 +34,12 @@ export default async function RadioGiveawayDetailPage({
const giveawayIdStr = giveaway.id.toString();
return (
<main>
<p className="muted" style={{ margin: "0 0 0.5rem" }}>
<main style={{ display: "grid", gap: "1.5rem" }}>
<p className="muted" style={{ margin: 0 }}>
<Link href="/radio/giveaways">← Back to giveaways</Link>
</p>
<div className="card">
<h1 style={{ margin: "0 0 0.5rem" }}>Giveaway #{giveawayIdStr}</h1>
<ContentCard icon="🎁" title={`Giveaway #${giveawayIdStr}`} subtitle="Radio giveaway details">
<table>
<tbody>
<tr>
@@ -52,7 +52,7 @@ export default async function RadioGiveawayDetailPage({
</tr>
</tbody>
</table>
</div>
</ContentCard>
</main>
);
}