From b9c6001f08e9c4aaf7fb8b9c51c05b8ea8d135fd Mon Sep 17 00:00:00 2001
From: openhands
Date: Sat, 18 Jul 2026 19:17:17 +0200
Subject: [PATCH] refactor: centralize duplicated formatDate helper
21 files defined their own local formatDate (with three different output
formats: date, datetime, datetime-seconds, and varying null fallbacks).
Replace them with a single shared helper at src/lib/format-date.ts that
takes a variant + optional fallback, preserving the exact previous output
per call site (verified identical string results).
Removes ~21 copies of the same logic. photos.tsx and media-grid.tsx keep
their epoch-ms based formatters since those are a different input shape.
Verified: tsc --noEmit clean, full test suite green (301/301).
---
src/app/(site)/help/tickets/page.tsx | 6 ++--
src/app/(site)/news/[slug]/page.tsx | 6 ++--
src/app/(site)/news/page.tsx | 6 ++--
src/app/(site)/radio/contests/[id]/page.tsx | 12 +++----
src/app/(site)/radio/contests/page.tsx | 6 ++--
src/app/(site)/radio/giveaways/[id]/page.tsx | 12 +++----
src/app/(site)/radio/giveaways/page.tsx | 6 ++--
src/app/(site)/radio/requests/page.tsx | 6 ++--
src/app/(site)/radio/shouts/page.tsx | 6 ++--
src/app/(site)/u/[username]/page.tsx | 6 ++--
src/app/admin/applications/page.tsx | 6 ++--
src/app/admin/ip/page.tsx | 8 ++---
src/app/admin/radio/api-keys/page.tsx | 8 ++---
src/app/admin/radio/autodj/page.tsx | 6 ++--
src/app/admin/radio/banners/page.tsx | 6 ++--
src/app/admin/radio/history/page.tsx | 8 ++---
src/app/admin/radio/moderation/page.tsx | 6 ++--
src/app/admin/radio/page.tsx | 8 ++---
src/app/admin/tags/page.tsx | 6 ++--
src/lib/format-date.ts | 36 ++++++++++++++++++++
20 files changed, 84 insertions(+), 86 deletions(-)
create mode 100644 src/lib/format-date.ts
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", "โ")}
|
|
- {formatDate(b.createdAt)}
+ {formatDate(b.createdAt, "datetime", "โ")}
|
|
- {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")}
|
|
- {formatDate(tag.createdAt)}
+ {formatDate(tag.createdAt, "date", "โ")}
|
|