mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
198 lines
11 KiB
React
198 lines
11 KiB
React
// modules/admin/pages/advertisements/ViewAdvertisement.jsx
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { House, Edit, ArrowLeft, Megaphone, MousePointerClick, ExternalLink } from "lucide-react";
|
|
|
|
import { useAdvertisements } from "@/contexts/AdminAdvertisementContext";
|
|
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
|
import { useDateFormat } from "@/hooks/useDateFormat";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
|
|
import { ADVERTISEMENT_TYPE_MAP, ADVERTISEMENT_STATUS_MAP } from "@/data/advertisement.data";
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function SectionCard({ title, description, children }) {
|
|
return (
|
|
<div className="rounded-lg border bg-card p-6 space-y-4">
|
|
{(title || description) && (
|
|
<div className="space-y-0.5 pb-1 border-b">
|
|
{title && <h2 className="text-sm font-semibold">{title}</h2>}
|
|
{description && <p className="text-xs text-muted-foreground">{description}</p>}
|
|
</div>
|
|
)}
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Field({ label, children }) {
|
|
return (
|
|
<div>
|
|
<p className="text-xs text-muted-foreground mb-0.5">{label}</p>
|
|
<div className="text-sm">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Page ───────────────────────────────────────────────────────────────────
|
|
|
|
export default function ViewAdvertisement() {
|
|
const navigate = useNavigate();
|
|
const { advertisementId } = useParams();
|
|
const { fetchAdvertisement, loading } = useAdvertisements();
|
|
const { fmtDateTime } = useDateFormat();
|
|
|
|
const [advertisement, setAdvertisement] = useState(null);
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
const res = await fetchAdvertisement(advertisementId);
|
|
setAdvertisement(res?.data?.data ?? null);
|
|
})();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [advertisementId]);
|
|
|
|
const breadcrumbItems = [
|
|
{ label: "Home", icon: <House className="size-4" />, to: "/admin" },
|
|
{ label: "Advertisements", to: "/admin/advertisements" },
|
|
{ label: "View" },
|
|
];
|
|
|
|
if (loading && !advertisement) {
|
|
return (
|
|
<section className="bg-muted h-full">
|
|
<div className="flex items-center justify-center py-32">
|
|
<Spinner className="size-6" />
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (!advertisement) {
|
|
return (
|
|
<section className="bg-muted h-full">
|
|
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4">
|
|
<div className="flex flex-col gap-2 my-6 w-full">
|
|
<AppBreadcrumb items={breadcrumbItems} />
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">Advertisement not found.</p>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const typeMeta = ADVERTISEMENT_TYPE_MAP[advertisement.type] ?? {};
|
|
const statusMeta = ADVERTISEMENT_STATUS_MAP[advertisement.status] ?? {};
|
|
const TypeIcon = typeMeta.icon ?? Megaphone;
|
|
|
|
const previewSrc = advertisement.image?.thumbnail_url || advertisement.image?.file_url || advertisement.image_url || null;
|
|
const ctas = Array.isArray(advertisement.ctas) ? advertisement.ctas : [];
|
|
|
|
return (
|
|
<section className="bg-muted h-full">
|
|
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-center px-4">
|
|
<div className="flex flex-col gap-2 my-6 w-full">
|
|
<AppBreadcrumb items={breadcrumbItems} />
|
|
</div>
|
|
|
|
<div className="w-full max-w-2xl pb-10 space-y-5">
|
|
|
|
{/* ── Header ─────────────────────────────────────────────────── */}
|
|
<div className="flex items-center justify-between flex-wrap gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="ghost" size="icon" onClick={() => navigate("/admin/advertisements")} aria-label="Back">
|
|
<ArrowLeft className="size-4" />
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-xl font-semibold tracking-tight">
|
|
{advertisement.headline || advertisement.badge_label || "Untitled advertisement"}
|
|
</h1>
|
|
<div className="flex items-center gap-1.5 mt-1">
|
|
<Badge variant="secondary" className="gap-1">
|
|
<TypeIcon className="size-3" />
|
|
{typeMeta.label ?? advertisement.type}
|
|
</Badge>
|
|
<Badge variant={advertisement.status === "active" ? "default" : "secondary"}>
|
|
{statusMeta.label ?? advertisement.status}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button onClick={() => navigate(`/admin/advertisements/${advertisementId}/edit`)}>
|
|
<Edit className="size-4" />
|
|
Edit
|
|
</Button>
|
|
</div>
|
|
|
|
{/* ── Preview ────────────────────────────────────────────────── */}
|
|
<SectionCard title="Preview">
|
|
<div className="h-48 rounded-lg bg-muted flex items-center justify-center overflow-hidden">
|
|
{previewSrc ? (
|
|
<img src={previewSrc} alt={advertisement.headline || advertisement.type} className="w-full h-full object-cover" />
|
|
) : (
|
|
<Megaphone className="size-8 text-muted-foreground" />
|
|
)}
|
|
</div>
|
|
</SectionCard>
|
|
|
|
{/* ── Content ────────────────────────────────────────────────── */}
|
|
<SectionCard title="Content">
|
|
<Field label="Badge label">{advertisement.badge_label || "—"}</Field>
|
|
<Field label="Headline">{advertisement.headline || "—"}</Field>
|
|
<Field label="Description">{advertisement.description || "—"}</Field>
|
|
</SectionCard>
|
|
|
|
{/* ── Calls to action ────────────────────────────────────────── */}
|
|
{ctas.length > 0 && (
|
|
<SectionCard title="Calls to action">
|
|
<div className="flex flex-col gap-2">
|
|
{ctas.map((cta, i) => (
|
|
<div key={i} className="flex items-center justify-between text-sm border rounded-md px-3 py-2">
|
|
<span className="font-medium">{cta.label}</span>
|
|
<a href={cta.link} target="_blank" rel="noreferrer" className="text-muted-foreground flex items-center gap-1 hover:text-foreground">
|
|
{cta.link}
|
|
<ExternalLink className="size-3" />
|
|
</a>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</SectionCard>
|
|
)}
|
|
|
|
{/* ── Scheduling & display ──────────────────────────────────── */}
|
|
<SectionCard title="Scheduling & display">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Field label="Start date">{fmtDateTime(advertisement.start_date)}</Field>
|
|
<Field label="End date">{fmtDateTime(advertisement.end_date)}</Field>
|
|
<Field label="Order">{advertisement.order ?? 0}</Field>
|
|
<Field label="Active">{advertisement.is_active ? "Yes" : "No"}</Field>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
{/* ── Metrics ────────────────────────────────────────────────── */}
|
|
<SectionCard title="Metrics">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<MousePointerClick className="size-4 text-muted-foreground" />
|
|
<span className="font-medium">{advertisement.click_count ?? 0}</span>
|
|
<span className="text-muted-foreground">clicks</span>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
{/* ── Audit ──────────────────────────────────────────────────── */}
|
|
<SectionCard title="Audit">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Field label="Created by">{advertisement.creator?.full_name || "—"}</Field>
|
|
<Field label="Created at">{fmtDateTime(advertisement.createdAt)}</Field>
|
|
<Field label="Last updated by">{advertisement.updater?.full_name || "—"}</Field>
|
|
<Field label="Last updated at">{fmtDateTime(advertisement.updatedAt)}</Field>
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
} |