Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-11 12:12:40 +08:00
parent 01a2c63b06
commit 71f758fe0b
66 changed files with 3055 additions and 939 deletions
@@ -27,8 +27,18 @@ export default function AdvertisementList() {
const [typeFilter, setTypeFilter] = useState("all");
const [placementFilter, setPlacementFilter] = useState("all");
const [statusFilter, setStatusFilter] = useState("all");
const [searchInput, setSearchInput] = useState("");
const [search, setSearch] = useState("");
// TODO(ads-3): Filters are not actually filtering — the "status" filter in
// particular compares against the stored `status` column, but status is only
// recomputed on read (see deriveStatus() in
// controllers/admin/advertisements.controller.js) and never persisted back
// to the DB. An ad that lapsed to "expired" still has status="active" in
// the row, so filtering by status here misses/matches the wrong rows.
// Needs either persisting the derived status on write/read, or filtering
// server-side using the same derivation logic. Also verify type/placement
// filters actually round-trip once ads-1/ads-2 land.
useEffect(() => {
const filters = [];
if (typeFilter !== "all") filters.push({ field: "type", value: typeFilter });
@@ -103,6 +113,7 @@ export default function AdvertisementList() {
</SelectContent>
</Select>
{/* TODO(ads-2): Remove this "All placements" dropdown entirely. */}
<Select value={placementFilter} onValueChange={setPlacementFilter}>
<SelectTrigger className="w-[220px] bg-background">
<SelectValue placeholder="All placements" />
@@ -127,17 +138,31 @@ export default function AdvertisementList() {
</SelectContent>
</Select>
<div className="relative flex-1 min-w-[160px]">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
placeholder="Search advertisements..."
className="pl-8 bg-background"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{/* TODO(ads-5): Verify this already satisfies the spec — search only
fires on button click / Enter (`search` state, not `searchInput`,
drives the fetch effect above), typing alone does not refetch.
Looks done already; double-check then mark complete. */}
<div className="flex items-center gap-2 flex-1 min-w-[160px]">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
placeholder="Search advertisements..."
className="pl-8 bg-background"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") setSearch(searchInput); }}
/>
</div>
<Button variant="outline" size="icon" className="shrink-0 bg-background" onClick={() => setSearch(searchInput)} aria-label="Search">
<Search className="size-4" />
</Button>
</div>
</div>
{/* TODO(ads-8): Add pagination controls for this grid — currently
always fetches page 1 / limit 24 with no way to reach further
pages (see `pagination` from useAdvertisements, already returned
by the API but unused here). */}
{/* ── Grid ───────────────────────────────────────────────────── */}
{loading ? (
<div className="flex items-center justify-center py-20">