feat: comprehensive CMS improvements
- Fix DOMPurify SSR crash (use isomorphic-dompurify) - Fix SanitizedHtml to sanitize by default - Add auth guards to studio/catalog maintenance pages - Add update/edit to vouchers CRUD - Add update/edit to rare-values CRUD - Add approve workflow to applications page - Add edit form to guilds detail page - Add SEO metadata to all public pages (21 pages) - Fix mobile nav accessibility (focus trap, aria attributes) - Fix missing labels and table accessibility - Add dynamic imports for heavy client components (6 components) - Fix silent error swallowing (40+ locations) - Add content scheduling for articles (publishAt, status) - Wire up 12 missing webhook notification triggers - Add global search to admin panel - Add bulk actions to admin users table - Fix JSON formatting and a11y issues
This commit is contained in:
1 parent
b810b16672
commit
30c95b1a5c
78 files changed
+2252
-134
No files matched your search
@@ -2,7 +2,11 @@
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { createVoucher, deleteVoucher } from "@/actions/admin-vouchers";
|
||||
import {
|
||||
createVoucher,
|
||||
deleteVoucher,
|
||||
updateVoucher,
|
||||
} from "@/actions/admin-vouchers";
|
||||
import { ConfirmDialog } from "@/components/admin/confirm-dialog";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
||||
@@ -33,10 +37,15 @@ export function VouchersClient({
|
||||
const t = useTranslations("pages.admin.vouchers");
|
||||
const { run, isPending } = useServerAction();
|
||||
const [pendingDelete, setPendingDelete] = useState<VoucherRow | null>(null);
|
||||
const [editingVoucher, setEditingVoucher] = useState<VoucherRow | null>(null);
|
||||
const [code, setCode] = useState("");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [maxUses, setMaxUses] = useState("1");
|
||||
const [expiresAt, setExpiresAt] = useState("");
|
||||
const [editCode, setEditCode] = useState("");
|
||||
const [editAmount, setEditAmount] = useState("");
|
||||
const [editMaxUses, setEditMaxUses] = useState("1");
|
||||
const [editExpiresAt, setEditExpiresAt] = useState("");
|
||||
|
||||
function handleCreate(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
@@ -72,6 +81,36 @@ export function VouchersClient({
|
||||
});
|
||||
}
|
||||
|
||||
function handleStartEdit(v: VoucherRow) {
|
||||
setEditingVoucher(v);
|
||||
setEditCode(v.code);
|
||||
setEditAmount(String(v.amount));
|
||||
setEditMaxUses(String(v.maxUses));
|
||||
setEditExpiresAt(
|
||||
v.expiresAt ? new Date(v.expiresAt).toISOString().slice(0, 16) : "",
|
||||
);
|
||||
}
|
||||
|
||||
function handleUpdate(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!editingVoucher || !canEdit) return;
|
||||
run(
|
||||
() =>
|
||||
updateVoucher({
|
||||
id: editingVoucher.id,
|
||||
code: editCode,
|
||||
amount: Number(editAmount),
|
||||
maxUses: Number(editMaxUses),
|
||||
expiresAt: editExpiresAt || undefined,
|
||||
}),
|
||||
{
|
||||
successMessage: t("updated"),
|
||||
errorMessage: t("updateError"),
|
||||
onSuccess: () => setEditingVoucher(null),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
@@ -158,6 +197,79 @@ export function VouchersClient({
|
||||
</form>
|
||||
)}
|
||||
|
||||
{canEdit && editingVoucher && (
|
||||
<form onSubmit={handleUpdate} className="admin-card mb-6">
|
||||
<h3 style={{ marginTop: 0 }}>
|
||||
{t("edit")} — {editingVoucher.code}
|
||||
</h3>
|
||||
<div className="mb-3 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<label className="flex flex-col gap-1.5">
|
||||
<span className="text-sm theme-text-muted dark:theme-text-muted">
|
||||
{t("form.code")}
|
||||
</span>
|
||||
<input
|
||||
value={editCode}
|
||||
onChange={(e) => setEditCode(e.target.value)}
|
||||
placeholder={t("form.codePlaceholder")}
|
||||
maxLength={255}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1.5">
|
||||
<span className="text-sm theme-text-muted dark:theme-text-muted">
|
||||
{t("form.amount")}
|
||||
</span>
|
||||
<input
|
||||
value={editAmount}
|
||||
onChange={(e) => setEditAmount(e.target.value)}
|
||||
type="number"
|
||||
min={1}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1.5">
|
||||
<span className="text-sm theme-text-muted dark:theme-text-muted">
|
||||
{t("form.maxUses")}
|
||||
</span>
|
||||
<input
|
||||
value={editMaxUses}
|
||||
onChange={(e) => setEditMaxUses(e.target.value)}
|
||||
type="number"
|
||||
min={1}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1.5">
|
||||
<span className="text-sm theme-text-muted dark:theme-text-muted">
|
||||
{t("form.expiresAt")}
|
||||
</span>
|
||||
<input
|
||||
value={editExpiresAt}
|
||||
onChange={(e) => setEditExpiresAt(e.target.value)}
|
||||
type="datetime-local"
|
||||
disabled={isPending}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="submit" variant="default" disabled={isPending}>
|
||||
{isPending ? t("form.saving") : t("form.save")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
disabled={isPending}
|
||||
onClick={() => setEditingVoucher(null)}
|
||||
>
|
||||
{t("form.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">
|
||||
{t("title")} ({vouchers.length})
|
||||
@@ -216,14 +328,24 @@ export function VouchersClient({
|
||||
</td>
|
||||
{canEdit && (
|
||||
<td>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled={isPending}
|
||||
onClick={() => setPendingDelete(v)}
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
disabled={isPending}
|
||||
onClick={() => handleStartEdit(v)}
|
||||
>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled={isPending}
|
||||
onClick={() => setPendingDelete(v)}
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
|
||||
Reference in new issue
Block a user