mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
ready to test
Testing Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
// 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 { 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>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDateTime(iso) {
|
||||
if (!iso) return "—";
|
||||
return new Date(iso).toLocaleString(undefined, {
|
||||
month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Page ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function ViewAdvertisement() {
|
||||
const navigate = useNavigate();
|
||||
const { advertisementId } = useParams();
|
||||
const { fetchAdvertisement, loading } = useAdvertisements();
|
||||
|
||||
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/60 h-full">
|
||||
<div className="flex items-center justify-center py-32">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!advertisement) {
|
||||
return (
|
||||
<section className="bg-muted/60 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/60 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">{formatDateTime(advertisement.start_date)}</Field>
|
||||
<Field label="End date">{formatDateTime(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">{formatDateTime(advertisement.createdAt)}</Field>
|
||||
<Field label="Last updated by">{advertisement.updater?.full_name || "—"}</Field>
|
||||
<Field label="Last updated at">{formatDateTime(advertisement.updatedAt)}</Field>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user