// modules/admin/pages/advertisements/AdvertisementList.jsx import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { House, Plus, Search, Megaphone, MousePointerClick, Edit, Trash2, Archive } from "lucide-react"; import { useAdvertisements } from "@/contexts/AdminAdvertisementContext"; import { resolveAssetSrc } from "@/utils/media.util"; import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb"; import { useDateFormat } from "@/hooks/useDateFormat"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Spinner } from "@/components/ui/spinner"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; 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 DEFAULT_PAGE_SIZE = 10; export default function AdvertisementList() { const navigate = useNavigate(); const { advertisements, pagination, loading, fetchAdvertisements, archiveAdvertisement } = useAdvertisements(); const [typeFilter, setTypeFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all"); const [searchInput, setSearchInput] = useState(""); const [search, setSearch] = useState(""); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const buildFilters = () => { const filters = []; if (typeFilter !== "all") filters.push({ id: "type", value: typeFilter }); if (statusFilter !== "all") filters.push({ id: "status", value: statusFilter }); if (search.trim()) filters.push({ id: "headline", value: search.trim() }); return filters; }; // Single source of truth for fetching — filter setters below always pair // 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: pageSize, filters: buildFilters() }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [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: , to: "/admin" }, { label: "Advertisements" }, ]; const total = pagination?.totalRecords ?? advertisements.length; const activeCount = advertisements.filter((a) => a.status === "active").length; const scheduledCount = advertisements.filter((a) => a.status === "scheduled").length; async function handleArchive(advertisementId) { await archiveAdvertisement(advertisementId); } return (
{/* ── Header ─────────────────────────────────────────────────── */}

Advertisements

Manage public-facing hero and banner placements

{/* ── Stat cards ─────────────────────────────────────────────── */}
{/* ── Filters ────────────────────────────────────────────────── */}
setSearchInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") runSearch(); }} />
{/* ── Grid ───────────────────────────────────────────────────── */} {loading ? (
) : advertisements.length === 0 ? ( navigate("/admin/advertisements/add")} /> ) : ( <>
{advertisements.map((ad) => ( navigate(`/admin/advertisements/${ad.advertisement_id}/view`)} onEdit={() => navigate(`/admin/advertisements/${ad.advertisement_id}/edit`)} onArchive={() => handleArchive(ad.advertisement_id)} /> ))}
)}
); } // ─── Stat card ────────────────────────────────────────────────────────────── function StatCard({ label, value, tone = "default" }) { const toneClass = { default: "text-foreground", success: "text-green-600 dark:text-green-400", info: "text-blue-600 dark:text-blue-400", muted: "text-muted-foreground", }[tone]; return (

{label}

{value}

); } // ─── Advertisement card ───────────────────────────────────────────────────── function AdvertisementCard({ ad, onView, onEdit, onArchive }) { const { fmtDateTime } = useDateFormat(); const typeMeta = ADVERTISEMENT_TYPE_MAP[ad.type] ?? {}; const statusMeta = ADVERTISEMENT_STATUS_MAP[ad.status] ?? {}; const placementMeta = PLACEMENT_MAP[ad.placement] ?? null; const TypeIcon = typeMeta.icon ?? Megaphone; 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, fmtDateTime); return (
{ad.click_count ?? 0} clicks
Archive this advertisement? "{ad.headline || ad.badge_label || "This advertisement"}" will be moved to archived advertisements. You can restore it later. Cancel Archive
); } // ─── Empty state ──────────────────────────────────────────────────────────── function EmptyState({ onCreate }) { return (

No advertisements yet

Create your first hero or banner placement.

); } // ─── Helpers ──────────────────────────────────────────────────────────────── function formatDateRange(start, end, fmtDate) { if (!start && !end) return null; if (start && end) return `${fmtDate(start)} - ${fmtDate(end)}`; if (start) return `Starts ${fmtDate(start)}`; if (end) return `Ends ${fmtDate(end)}`; return null; }