mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -13,6 +13,7 @@ import {
|
||||
import { useAdvertisements } from "@/contexts/AdminAdvertisementContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
import { resolveAssetSrc } from "@/utils/media.util";
|
||||
import { cn } from "@/lib/utils";
|
||||
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
||||
@@ -453,6 +454,7 @@ function SummaryRow({ label, value }) {
|
||||
}
|
||||
|
||||
function StepReview({ data, selectedAsset, imageUrl }) {
|
||||
const { fmtDateTime } = useDateFormat();
|
||||
const placementMeta = PLACEMENT_MAP[data.placement];
|
||||
const ctas = (data.ctas ?? []).filter((c) => c.label || c.link);
|
||||
const hasLandingPage = !data.redirect_link && (data.landing_page?.title || data.landing_page?.body);
|
||||
@@ -519,8 +521,8 @@ function StepReview({ data, selectedAsset, imageUrl }) {
|
||||
|
||||
<div className="border rounded-lg p-4 space-y-1">
|
||||
<p className="text-sm font-medium mb-2">Scheduling & display</p>
|
||||
<SummaryRow label="Start date" value={data.start_date} />
|
||||
<SummaryRow label="End date" value={data.end_date} />
|
||||
<SummaryRow label="Start date" value={data.start_date ? fmtDateTime(data.start_date) : null} />
|
||||
<SummaryRow label="End date" value={data.end_date ? fmtDateTime(data.end_date) : null} />
|
||||
<SummaryRow label="Order" value={data.order} />
|
||||
<SummaryRow label="Status" value={data.is_active ? "Active" : "Draft"} />
|
||||
</div>
|
||||
@@ -693,7 +695,7 @@ export default function AddAdvertisement() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={stepIndex === 0 ? () => navigate(-1) : handleBack}
|
||||
onClick={stepIndex === 0 ? () => navigate("/admin/advertisements") : handleBack}
|
||||
disabled={loading}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
import { ADVERTISEMENT_TYPES, ADVERTISEMENT_TYPE_MAP, ADVERTISEMENT_STATUSES, ADVERTISEMENT_STATUS_MAP } from "@/data/advertisement.data";
|
||||
import { ADVERTISEMENT_TYPES, ADVERTISEMENT_TYPE_MAP, ADVERTISEMENT_FILTERABLE_STATUSES, ADVERTISEMENT_STATUS_MAP } from "@/data/advertisement.data";
|
||||
import { PLACEMENT_MAP } from "@/data/placement.data";
|
||||
import { TablePagination } from "@/components/generic/Table/TablePagination";
|
||||
|
||||
const PAGE_SIZE = 24;
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
export default function AdvertisementList() {
|
||||
const navigate = useNavigate();
|
||||
@@ -32,6 +32,7 @@ export default function AdvertisementList() {
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const [search, setSearch] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
|
||||
const buildFilters = () => {
|
||||
const filters = [];
|
||||
@@ -45,13 +46,14 @@ export default function AdvertisementList() {
|
||||
// their state update with setPage(1) in the same handler so this only
|
||||
// ever fires once per change (no separate "reset page" effect racing it).
|
||||
useEffect(() => {
|
||||
fetchAdvertisements({ page, limit: PAGE_SIZE, filters: buildFilters() });
|
||||
fetchAdvertisements({ page, limit: pageSize, filters: buildFilters() });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typeFilter, statusFilter, search, page]);
|
||||
}, [typeFilter, statusFilter, search, page, pageSize]);
|
||||
|
||||
const handleTypeFilter = (v) => { setTypeFilter(v); setPage(1); };
|
||||
const handleStatusFilter = (v) => { setStatusFilter(v); setPage(1); };
|
||||
const runSearch = () => { setSearch(searchInput); setPage(1); };
|
||||
const handlePageSizeChange = (size) => { setPageSize(size); setPage(1); };
|
||||
|
||||
const items = [
|
||||
{ label: "Home", icon: <House className="size-4" />, to: "/admin" },
|
||||
@@ -61,7 +63,6 @@ export default function AdvertisementList() {
|
||||
const total = pagination?.totalRecords ?? advertisements.length;
|
||||
const activeCount = advertisements.filter((a) => a.status === "active").length;
|
||||
const scheduledCount = advertisements.filter((a) => a.status === "scheduled").length;
|
||||
const expiredCount = advertisements.filter((a) => a.status === "expired").length;
|
||||
|
||||
async function handleArchive(advertisementId) {
|
||||
await archiveAdvertisement(advertisementId);
|
||||
@@ -95,11 +96,10 @@ export default function AdvertisementList() {
|
||||
</div>
|
||||
|
||||
{/* ── Stat cards ─────────────────────────────────────────────── */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
<StatCard label="Total ads" value={total} />
|
||||
<StatCard label="Active" value={activeCount} tone="success" />
|
||||
<StatCard label="Scheduled" value={scheduledCount} tone="info" />
|
||||
<StatCard label="Expired" value={expiredCount} tone="muted" />
|
||||
</div>
|
||||
|
||||
{/* ── Filters ────────────────────────────────────────────────── */}
|
||||
@@ -122,7 +122,7 @@ export default function AdvertisementList() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All statuses</SelectItem>
|
||||
{ADVERTISEMENT_STATUSES.map((s) => (
|
||||
{ADVERTISEMENT_FILTERABLE_STATUSES.map((s) => (
|
||||
<SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -170,6 +170,7 @@ export default function AdvertisementList() {
|
||||
<TablePagination
|
||||
pagination={pagination}
|
||||
onPageChange={setPage}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
rowCount={advertisements.length}
|
||||
recordLabel="advertisement"
|
||||
/>
|
||||
@@ -203,7 +204,7 @@ function StatCard({ label, value, tone = "default" }) {
|
||||
// ─── Advertisement card ─────────────────────────────────────────────────────
|
||||
|
||||
function AdvertisementCard({ ad, onView, onEdit, onArchive }) {
|
||||
const { fmtDate } = useDateFormat();
|
||||
const { fmtDateTime } = useDateFormat();
|
||||
const typeMeta = ADVERTISEMENT_TYPE_MAP[ad.type] ?? {};
|
||||
const statusMeta = ADVERTISEMENT_STATUS_MAP[ad.status] ?? {};
|
||||
const placementMeta = PLACEMENT_MAP[ad.placement] ?? null;
|
||||
@@ -212,7 +213,7 @@ function AdvertisementCard({ ad, onView, onEdit, onArchive }) {
|
||||
const previewSrc = resolveAssetSrc(ad.image) || ad.image_url || null;
|
||||
const isDimmed = ad.status === "expired" || ad.status === "archived";
|
||||
|
||||
const dateRange = formatDateRange(ad.start_date, ad.end_date, fmtDate);
|
||||
const dateRange = formatDateRange(ad.start_date, ad.end_date, fmtDateTime);
|
||||
|
||||
return (
|
||||
<div className={`bg-background rounded-lg border overflow-hidden flex flex-col ${isDimmed ? "opacity-70" : ""}`}>
|
||||
|
||||
@@ -213,7 +213,7 @@ export default function EditAdvertisement() {
|
||||
}, [advertisementId]);
|
||||
|
||||
const onSubmit = async (values) => {
|
||||
if (!isDirty) { bypassOnce(); return navigate(-1); }
|
||||
if (!isDirty) { bypassOnce(); return navigate("/admin/advertisements"); }
|
||||
|
||||
const payload = {
|
||||
...values,
|
||||
@@ -506,7 +506,7 @@ export default function EditAdvertisement() {
|
||||
</SectionCard>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="outline" onClick={() => navigate(-1)} disabled={loading}>
|
||||
<Button type="button" variant="outline" onClick={() => navigate("/admin/advertisements")} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
|
||||
Reference in New Issue
Block a user