This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+112
-102
@@ -1,123 +1,133 @@
|
||||
import { Calendar } from "lucide-react";
|
||||
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;
|
||||
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);
|
||||
if (!ts) return "—";
|
||||
return new Date(ts * 1000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export default async function AdminCalendar() {
|
||||
const t = await getTranslations("pages.admin.calendar");
|
||||
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 = [];
|
||||
}
|
||||
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 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;
|
||||
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(--admin-accent)]/20 to-[var(--admin-accent)]/5 grid place-items-center">
|
||||
<Calendar size={20} className="text-[var(--admin-accent)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="m-0 text-xl font-extrabold text-[var(--admin-text)]">{t("title")}</h1>
|
||||
<p className="m-0 text-xs text-[var(--admin-text-muted)] mt-0.5">
|
||||
{t("subtitle", { count: campaigns.length })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
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(--admin-accent)]/20 to-[var(--admin-accent)]/5 grid place-items-center">
|
||||
<Calendar size={20} className="text-[var(--admin-accent)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="m-0 text-xl font-extrabold text-[var(--admin-text)]">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="m-0 text-xs text-[var(--admin-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>
|
||||
<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 grid-cols-1 gap-4 md: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-[var(--admin-success-subtle)] text-[var(--admin-text)]" : "bg-[var(--admin-error-subtle)] text-[var(--admin-text)]"}`}
|
||||
>
|
||||
{enabled ? t("statusEnabled") : t("statusDisabled")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">
|
||||
{t("campaignMeta", { id: c.id, days: c.totalDays, rewards: count })}
|
||||
</p>
|
||||
<p className="text-xs theme-text-muted dark:theme-text-muted mt-1">
|
||||
{t("startsAt", { date: fmtDate(c.startTimestamp) })}
|
||||
{c.lockExpired === "1" ? ` · ${t("locksExpired")}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
{campaigns.length === 0 ? (
|
||||
<div className="admin-empty">{t("noCampaigns")}</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 md: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-[var(--admin-success-subtle)] text-[var(--admin-text)]" : "bg-[var(--admin-error-subtle)] text-[var(--admin-text)]"}`}
|
||||
>
|
||||
{enabled ? t("statusEnabled") : t("statusDisabled")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">
|
||||
{t("campaignMeta", {
|
||||
id: c.id,
|
||||
days: c.totalDays,
|
||||
rewards: count,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs theme-text-muted dark:theme-text-muted mt-1">
|
||||
{t("startsAt", { date: fmtDate(c.startTimestamp) })}
|
||||
{c.lockExpired === "1" ? ` · ${t("locksExpired")}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user