diff --git a/src/app/(site)/help/tickets/page.tsx b/src/app/(site)/help/tickets/page.tsx index b40b6f01..b8086341 100644 --- a/src/app/(site)/help/tickets/page.tsx +++ b/src/app/(site)/help/tickets/page.tsx @@ -1,4 +1,5 @@ import { redirect } from "next/navigation"; +import { formatDate } from "@/lib/format-date"; import { getTranslations } from "next-intl/server"; import { createTicket } from "@/actions/help-tickets"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; @@ -7,9 +8,6 @@ import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; -function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : ""; -} export default async function HelpTicketsPage() { const session = await auth(); @@ -101,7 +99,7 @@ export default async function HelpTicketsPage() { {t2.open ? t("statusOpen") : t("statusClosed")} - {formatDate(t2.createdAt)} + {formatDate(t2.createdAt, "datetime", "")} ))} diff --git a/src/app/(site)/news/[slug]/page.tsx b/src/app/(site)/news/[slug]/page.tsx index 2c4564f4..8274d8b8 100644 --- a/src/app/(site)/news/[slug]/page.tsx +++ b/src/app/(site)/news/[slug]/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import { formatDate } from "@/lib/format-date"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -45,9 +46,6 @@ const REACTIONS: { key: string; emoji: string }[] = [ { key: "wow", emoji: "๐Ÿ˜ฎ" }, ]; -function formatDate(d: Date | null | undefined): string { - return d ? d.toISOString().slice(0, 10) : ""; -} export default async function ArticlePage({ params, @@ -262,7 +260,7 @@ export default async function ArticlePage({ {t("userNumber", { id: c.userId })} )} {c.createdAt ? ( - ยท {formatDate(c.createdAt)} + ยท {formatDate(c.createdAt, "date", "")} ) : null}

diff --git a/src/app/(site)/news/page.tsx b/src/app/(site)/news/page.tsx index 16dd4260..96c2297f 100644 --- a/src/app/(site)/news/page.tsx +++ b/src/app/(site)/news/page.tsx @@ -1,4 +1,5 @@ import Image from "next/image"; +import { formatDate } from "@/lib/format-date"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } from "@/components/public/ui"; @@ -9,9 +10,6 @@ export const revalidate = 300; export const metadata = { title: "News" }; -function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 10) : ""; -} export default async function NewsPage() { const t = await getTranslations("pages.news"); @@ -99,7 +97,7 @@ export default async function NewsPage() { className="muted" style={{ margin: "0.35rem 0 0.6rem", fontSize: "0.78rem" }} > - {formatDate(a.createdAt)} + {formatDate(a.createdAt, "date", "")}

Period - {contest.startDate ? formatDate(contest.startDate) : "โ€”"} + {contest.startDate ? formatDate(contest.startDate, "datetime-seconds", "") : "โ€”"} {" โ€” "} - {contest.endDate ? formatDate(contest.endDate) : "ongoing"} + {contest.endDate ? formatDate(contest.endDate, "datetime-seconds", "") : "ongoing"} Created - {formatDate(contest.createdAt) || ( + {formatDate(contest.createdAt, "datetime-seconds", "") || ( โ€” )} @@ -79,7 +77,7 @@ export default async function RadioContestDetailPage({ Updated - {formatDate(contest.updatedAt) || ( + {formatDate(contest.updatedAt, "datetime-seconds", "") || ( โ€” )} diff --git a/src/app/(site)/radio/contests/page.tsx b/src/app/(site)/radio/contests/page.tsx index 30fabbf4..0dfde8f7 100644 --- a/src/app/(site)/radio/contests/page.tsx +++ b/src/app/(site)/radio/contests/page.tsx @@ -1,13 +1,11 @@ import Link from "next/link"; +import { formatDate } from "@/lib/format-date"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } 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, 10) : ""; -} export default async function RadioContestsPage() { const t = await getTranslations("pages.radioContests"); @@ -67,7 +65,7 @@ export default async function RadioContestsPage() { style={{ margin: 0, fontSize: "0.85rem" }} > {c.startDate - ? `${formatDate(c.startDate)} โ€” ${c.endDate ? formatDate(c.endDate) : "ongoing"}` + ? `${formatDate(c.startDate, "date", "")} โ€” ${c.endDate ? formatDate(c.endDate, "date", "") : "ongoing"}` : t("dateUnknown")}

diff --git a/src/app/(site)/radio/giveaways/[id]/page.tsx b/src/app/(site)/radio/giveaways/[id]/page.tsx index 5ba7fd92..b990111f 100644 --- a/src/app/(site)/radio/giveaways/[id]/page.tsx +++ b/src/app/(site)/radio/giveaways/[id]/page.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { formatDate } from "@/lib/format-date"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { ContentCard } from "@/components/public/ui"; @@ -6,9 +7,6 @@ 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 RadioGiveawayDetailPage({ params, @@ -68,15 +66,15 @@ export default async function RadioGiveawayDetailPage({ Period - {giveaway.startDate ? formatDate(giveaway.startDate) : "โ€”"} + {giveaway.startDate ? formatDate(giveaway.startDate, "datetime-seconds", "") : "โ€”"} {" โ€” "} - {giveaway.endDate ? formatDate(giveaway.endDate) : "ongoing"} + {giveaway.endDate ? formatDate(giveaway.endDate, "datetime-seconds", "") : "ongoing"} Created - {formatDate(giveaway.createdAt) || ( + {formatDate(giveaway.createdAt, "datetime-seconds", "") || ( โ€” )} @@ -84,7 +82,7 @@ export default async function RadioGiveawayDetailPage({ Updated - {formatDate(giveaway.updatedAt) || ( + {formatDate(giveaway.updatedAt, "datetime-seconds", "") || ( โ€” )} diff --git a/src/app/(site)/radio/giveaways/page.tsx b/src/app/(site)/radio/giveaways/page.tsx index e3a63b2d..eca48e55 100644 --- a/src/app/(site)/radio/giveaways/page.tsx +++ b/src/app/(site)/radio/giveaways/page.tsx @@ -1,13 +1,11 @@ import Link from "next/link"; +import { formatDate } from "@/lib/format-date"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState } 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, 10) : ""; -} export default async function RadioGiveawaysPage() { const t = await getTranslations("pages.radioGiveaways"); @@ -74,7 +72,7 @@ export default async function RadioGiveawaysPage() { style={{ margin: 0, fontSize: "0.85rem" }} > {g.startDate - ? `${formatDate(g.startDate)} โ€” ${g.endDate ? formatDate(g.endDate) : "ongoing"}` + ? `${formatDate(g.startDate, "date", "")} โ€” ${g.endDate ? formatDate(g.endDate, "date", "") : "ongoing"}` : t("dateUnknown")}

diff --git a/src/app/(site)/radio/requests/page.tsx b/src/app/(site)/radio/requests/page.tsx index e875e469..5ece2bc8 100644 --- a/src/app/(site)/radio/requests/page.tsx +++ b/src/app/(site)/radio/requests/page.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { formatDate } from "@/lib/format-date"; import { submitRequest } from "@/actions/radio-requests"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; @@ -6,9 +7,6 @@ import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; -function formatDate(d: Date | null | undefined): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : ""; -} export default async function RadioRequestsPage() { const t = await getTranslations("pages.radioRequests"); @@ -136,7 +134,7 @@ export default async function RadioRequestsPage() { )} - {r.submittedAt ? formatDate(r.submittedAt) : "โ€”"} + {r.submittedAt ? formatDate(r.submittedAt, "datetime", "") : "โ€”"} {status} diff --git a/src/app/(site)/radio/shouts/page.tsx b/src/app/(site)/radio/shouts/page.tsx index d479d641..45f1e303 100644 --- a/src/app/(site)/radio/shouts/page.tsx +++ b/src/app/(site)/radio/shouts/page.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { formatDate } from "@/lib/format-date"; import { postShout } from "@/actions/radio-shouts"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { auth } from "@/lib/auth"; @@ -8,9 +9,6 @@ import { siteSettings } from "@/lib/services/site-settings"; export const dynamic = "force-dynamic"; -function formatDate(d: Date | null | undefined): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : ""; -} export default async function RadioShoutsPage() { const t = await getTranslations("pages.radioShouts"); @@ -124,7 +122,7 @@ export default async function RadioShoutsPage() {

{author?.username ?? `User #${s.userId}`}{" "} - {formatDate(s.createdAt)} + {formatDate(s.createdAt, "datetime", "")}

{s.message}

diff --git a/src/app/(site)/u/[username]/page.tsx b/src/app/(site)/u/[username]/page.tsx index 3b0e03e6..61092450 100644 --- a/src/app/(site)/u/[username]/page.tsx +++ b/src/app/(site)/u/[username]/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import { formatDate } from "@/lib/format-date"; import Link from "next/link"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; @@ -47,9 +48,6 @@ function formatTimestamp(ts: number | null | undefined): string { return new Date(ts * 1000).toISOString().slice(0, 10); } -function formatDate(d: Date | null | undefined): string { - return d ? d.toISOString().slice(0, 10) : ""; -} export default async function ProfilePage({ params, @@ -565,7 +563,7 @@ export default async function ProfilePage({ {author?.username ?? t("unknownUser", { id: g.userId })} {" "} - {formatDate(g.createdAt)} + {formatDate(g.createdAt, "date", "")}

{g.message}

diff --git a/src/app/admin/applications/page.tsx b/src/app/admin/applications/page.tsx index 1040291a..765b5d71 100644 --- a/src/app/admin/applications/page.tsx +++ b/src/app/admin/applications/page.tsx @@ -1,4 +1,5 @@ import { ClipboardList } from "lucide-react"; +import { formatDate } from "@/lib/format-date"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { dismissApplication } from "@/actions/admin-applications"; @@ -17,9 +18,6 @@ type ApplicationRow = { createdAt: Date | null; }; -function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : "โ€”"; -} export default async function AdminApplications() { const { session, permissions } = await getAdminContext(); @@ -105,7 +103,7 @@ export default async function AdminApplications() {

- {formatDate(a.createdAt)} + {formatDate(a.createdAt, "datetime", "โ€”")}

{a.content} diff --git a/src/app/admin/ip/page.tsx b/src/app/admin/ip/page.tsx index 3a9c33db..218c3d4f 100644 --- a/src/app/admin/ip/page.tsx +++ b/src/app/admin/ip/page.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { formatDate } from "@/lib/format-date"; import { addBlacklist, addWhitelist, @@ -19,9 +20,6 @@ type IpRow = { createdAt: Date | null; }; -function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : "โ€”"; -} export default async function AdminIp() { const { session, permissions } = await getAdminContext(); @@ -113,7 +111,7 @@ export default async function AdminIp() { {w.asn ?? "โ€”"} - {formatDate(w.createdAt)} + {formatDate(w.createdAt, "datetime", "โ€”")}

@@ -175,7 +173,7 @@ export default async function AdminIp() { {b.asn ?? "โ€”"} - {formatDate(b.createdAt)} + {formatDate(b.createdAt, "datetime", "โ€”")} diff --git a/src/app/admin/radio/api-keys/page.tsx b/src/app/admin/radio/api-keys/page.tsx index d19eb786..7fa61b65 100644 --- a/src/app/admin/radio/api-keys/page.tsx +++ b/src/app/admin/radio/api-keys/page.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { formatDate } from "@/lib/format-date"; import { createApiKey, deleteApiKey, @@ -23,9 +24,6 @@ type ApiKey = { createdAt: Date | null; }; -function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : "\u2014"; -} function maskKey(key: string): string { return key.length > 8 ? `${key.slice(0, 8)}\u2026` : key; @@ -204,10 +202,10 @@ export default async function AdminRadioApiKeysPage() { {k.rateLimit}/min - {formatDate(k.lastUsedAt)} + {formatDate(k.lastUsedAt, "datetime", "\u2014")} - {formatDate(k.expiresAt)} + {formatDate(k.expiresAt, "datetime", "\u2014")} - {formatDate(track.lastPlayedAt)} + {formatDate(track.lastPlayedAt, "datetime", "\u2014")} #{String(b.id)} {t("banners.byUser", { id: String(b.userId) })} ยท{" "} - {formatDate(b.createdAt)} ยท{" "} + {formatDate(b.createdAt, "datetime", "\u2014")} ยท{" "} diff --git a/src/app/admin/radio/history/page.tsx b/src/app/admin/radio/history/page.tsx index ca6c1861..ffacefb7 100644 --- a/src/app/admin/radio/history/page.tsx +++ b/src/app/admin/radio/history/page.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { formatDate } from "@/lib/format-date"; import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; @@ -14,9 +15,6 @@ type History = { notes: string | null; }; -function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : "\u2014"; -} function formatDuration(start: Date, end: Date | null): string { if (!end) return "\u2014"; @@ -137,12 +135,12 @@ export default async function AdminRadioHistoryPage() { {h.showName ?? "\u2014"} - {formatDate(h.startedAt)} + {formatDate(h.startedAt, "datetime", "\u2014")} {h.endedAt ? ( - {formatDate(h.endedAt)} + {formatDate(h.endedAt, "datetime", "\u2014")} ) : ( diff --git a/src/app/admin/radio/moderation/page.tsx b/src/app/admin/radio/moderation/page.tsx index 29f11479..31c63795 100644 --- a/src/app/admin/radio/moderation/page.tsx +++ b/src/app/admin/radio/moderation/page.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { formatDate } from "@/lib/format-date"; import { deleteShout } from "@/actions/admin-radio-moderation"; import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; @@ -13,9 +14,6 @@ type Shout = { updatedAt: Date | null; }; -function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : "\u2014"; -} export default async function AdminRadioModerationPage() { const t = await getTranslations("pages.admin.radio"); @@ -72,7 +70,7 @@ export default async function AdminRadioModerationPage() { {String(s.userId)} {s.message} - {formatDate(s.createdAt)} + {formatDate(s.createdAt, "datetime", "\u2014")} diff --git a/src/app/admin/radio/page.tsx b/src/app/admin/radio/page.tsx index 83024bc6..75989c56 100644 --- a/src/app/admin/radio/page.tsx +++ b/src/app/admin/radio/page.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { formatDate } from "@/lib/format-date"; import { deleteShout } from "@/actions/admin-radio-moderation"; import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; @@ -33,9 +34,6 @@ type Schedule = { isActive: boolean; }; -function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : "\u2014"; -} function formatTime(d: Date | null): string { return d ? d.toISOString().slice(11, 16) : "\u2014"; @@ -162,7 +160,7 @@ export default async function AdminRadioPage() { {String(s.userId)} {s.message} - {formatDate(s.createdAt)} + {formatDate(s.createdAt, "datetime", "\u2014")} @@ -218,7 +216,7 @@ export default async function AdminRadioPage() { - {formatDate(a.createdAt)} + {formatDate(a.createdAt, "datetime", "\u2014")} ))} diff --git a/src/app/admin/tags/page.tsx b/src/app/admin/tags/page.tsx index b056af83..daddf16b 100644 --- a/src/app/admin/tags/page.tsx +++ b/src/app/admin/tags/page.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { formatDate } from "@/lib/format-date"; import { createTag, deleteTag, updateTag } from "@/actions/admin-tags"; import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; @@ -14,9 +15,6 @@ type Tag = { createdAt: Date | null; }; -function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 10) : "โ€”"; -} export default async function AdminTagsPage() { const { session, permissions } = await getAdminContext(); @@ -170,7 +168,7 @@ export default async function AdminTagsPage() { - {formatDate(tag.createdAt)} + {formatDate(tag.createdAt, "date", "โ€”")} = { + date: "", + datetime: "โ€”", + "datetime-seconds": "โ€”", +}; + +/** + * Format a Date for display. Kept as a single shared helper so the various + * admin/site pages no longer each redefine their own copy. + * + * - "date" โ†’ 2026-07-18 + * - "datetime" โ†’ 2026-07-18 18:30 + * - "datetime-seconds"โ†’ 2026-07-18 18:30:45 + * + * `fallback` overrides the default em-dash / empty string when the value is + * null/undefined. + */ +export function formatDate( + d: Date | null | undefined, + variant: DateFormatVariant = "datetime", + fallback?: string, +): string { + if (!d) return fallback ?? FALLBACKS[variant]; + const iso = d.toISOString(); + switch (variant) { + case "date": + return iso.slice(0, 10); + case "datetime-seconds": + return iso.slice(0, 19).replace("T", " "); + case "datetime": + default: + return iso.slice(0, 16).replace("T", " "); + } +}