125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { StatusCard } from "@/components/admin/dashboard";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { Calendar } from "lucide-react";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type Campaign = {
|
|
id: number;
|
|
name: string;
|
|
image: string;
|
|
startTimestamp: number;
|
|
totalDays: number;
|
|
lockExpired: string;
|
|
enabled: string;
|
|
};
|
|
|
|
function fmtDate(ts: number): string {
|
|
if (!ts) return "—";
|
|
return new Date(ts * 1000).toISOString().slice(0, 10);
|
|
}
|
|
|
|
export default async function AdminCalendar() {
|
|
const t = await getTranslations("pages.admin.calendar");
|
|
|
|
let campaigns: Campaign[] = [];
|
|
try {
|
|
campaigns = await prisma.calendarCampaigns.findMany({
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
image: true,
|
|
startTimestamp: true,
|
|
totalDays: true,
|
|
lockExpired: true,
|
|
enabled: true,
|
|
},
|
|
orderBy: { id: "desc" },
|
|
});
|
|
} catch {
|
|
campaigns = [];
|
|
}
|
|
|
|
const rewardCounts = new Map<number, number>();
|
|
let totalRewards = 0;
|
|
try {
|
|
const rewards = await prisma.calendarRewards.findMany({
|
|
select: { campaignId: true },
|
|
});
|
|
totalRewards = rewards.length;
|
|
for (const r of rewards) {
|
|
rewardCounts.set(r.campaignId, (rewardCounts.get(r.campaignId) ?? 0) + 1);
|
|
}
|
|
} catch {
|
|
}
|
|
|
|
const enabledCount = campaigns.filter((c) => c.enabled === "1").length;
|
|
|
|
return (
|
|
<main>
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[var(--color-primary)]/20 to-[var(--color-primary)]/5 grid place-items-center">
|
|
<Calendar size={20} className="text-[var(--color-primary)]" />
|
|
</div>
|
|
<div>
|
|
<h1 className="m-0 text-xl font-extrabold text-[var(--color-text)]">{t("title")}</h1>
|
|
<p className="m-0 text-xs text-[var(--color-text-muted)] mt-0.5">{t("subtitle", { count: campaigns.length })}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
|
<StatusCard label={t("campaigns")} value={campaigns.length} icon="📅" />
|
|
<StatusCard
|
|
label={t("enabled")}
|
|
value={enabledCount}
|
|
state={enabledCount > 0 ? "ok" : "neutral"}
|
|
icon="✅"
|
|
/>
|
|
<StatusCard label={t("totalRewards")} value={totalRewards} icon="🎁" />
|
|
</div>
|
|
|
|
{campaigns.length === 0 ? (
|
|
<div className="admin-empty">{t("noCampaigns")}</div>
|
|
) : (
|
|
<div className="grid cols-3">
|
|
{campaigns.map((c) => {
|
|
const enabled = c.enabled === "1";
|
|
const count = rewardCounts.get(c.id) ?? 0;
|
|
return (
|
|
<Link
|
|
key={c.id}
|
|
href={`/admin/calendar/${c.id}`}
|
|
className="admin-card hover article"
|
|
>
|
|
{c.image ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img className="article-img" src={c.image} alt={c.name} />
|
|
) : (
|
|
<div className="article-img" />
|
|
)}
|
|
<div className="body">
|
|
<div className="flex items-center justify-between gap-2 mb-1">
|
|
<h3 className="m-0">{c.name || `#${c.id}`}</h3>
|
|
<span className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${enabled ? "bg-green-600/20 text-[var(--color-text)]" : "bg-red-500/22 text-[var(--color-text)]"}`}>
|
|
{enabled ? t("statusEnabled") : t("statusDisabled")}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 m-0">
|
|
{t("campaignMeta", { id: c.id, days: c.totalDays, rewards: count })}
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
{t("startsAt", { date: fmtDate(c.startTimestamp) })}
|
|
{c.lockExpired === "1" ? ` · ${t("locksExpired")}` : ""}
|
|
</p>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|