mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -1,71 +0,0 @@
|
||||
// components/blocks/Popup.jsx
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ResponsiveModal from "@/components/generic/ResponsiveModal";
|
||||
import { resolveAssetSrc } from "@/utils/media.util";
|
||||
|
||||
// ── Popup ────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
* Generic popup advertisement block.
|
||||
* Modal-style placement shown on page load — wraps ResponsiveModal so it gets
|
||||
* dialog/drawer behavior for free. Caller owns the `open` state (typically set
|
||||
* to true once an active popup ad resolves from the API).
|
||||
*
|
||||
* Props:
|
||||
* ad — advertisement object { headline, description, ctas, image, image_url, advertisement_id }
|
||||
* open — boolean, modal visibility
|
||||
* onOpenChange — (open: boolean) => void
|
||||
* onCtaClick — (ad, cta) => void, called when a footer CTA button is clicked
|
||||
* onDismissForever — () => void, called when the user picks "Don't show this ad again"
|
||||
*/
|
||||
export function Popup({ ad, open, onOpenChange, onCtaClick, onDismissForever }) {
|
||||
if (!ad) return null;
|
||||
|
||||
const imageSrc = resolveAssetSrc(ad.image) || ad.image_url || null;
|
||||
const ctas = Array.isArray(ad.ctas) ? ad.ctas : [];
|
||||
|
||||
const handleDismissForever = () => {
|
||||
onOpenChange?.(false);
|
||||
onDismissForever?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveModal
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={ad.headline || "Announcement"}
|
||||
description={ad.description || undefined}
|
||||
footer={
|
||||
ctas.length > 0 ? (
|
||||
<>
|
||||
{ctas.map((cta, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant={cta.variant === "outline" ? "outline" : "default"}
|
||||
onClick={() => onCtaClick?.(ad, cta)}
|
||||
>
|
||||
{cta.label}
|
||||
</Button>
|
||||
))}
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{imageSrc && (
|
||||
<div className="rounded-lg bg-muted aspect-video flex items-center justify-center overflow-hidden pointer-events-none select-none">
|
||||
<img src={imageSrc} alt={ad.headline || "Advertisement"} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onDismissForever && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDismissForever}
|
||||
className="w-fit justify-self-start rounded text-xs text-muted-foreground hover:text-foreground underline underline-offset-2 outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
>
|
||||
Don't show this ad again
|
||||
</button>
|
||||
)}
|
||||
</ResponsiveModal>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// components/blocks/Sidebar.jsx
|
||||
|
||||
import { Megaphone } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { resolveAssetSrc } from "@/utils/media.util";
|
||||
|
||||
// ── Sidebar ──────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
* Generic sidebar advertisement block.
|
||||
* Compact vertical card — image on top, optional short headline/description and
|
||||
* a single CTA below. Meant to sit in a narrow column (sidebars, rail layouts),
|
||||
* not stretch full-width like Hero/Banner.
|
||||
*
|
||||
* Props:
|
||||
* ad — advertisement object { headline, description, ctas, image, image_url, advertisement_id }
|
||||
* onCtaClick — (ad, cta) => void, called when the CTA button (or card, if no CTA) is clicked
|
||||
*/
|
||||
export function Sidebar({ ad, onCtaClick }) {
|
||||
if (!ad) return null;
|
||||
|
||||
const imageSrc = resolveAssetSrc(ad.image) || ad.image_url || null;
|
||||
const ctas = Array.isArray(ad.ctas) ? ad.ctas : [];
|
||||
const primaryCta = ctas[0];
|
||||
|
||||
const handleCardClick = () => {
|
||||
if (!primaryCta) onCtaClick?.(ad, undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full max-w-xs rounded-lg border bg-card overflow-hidden flex flex-col cursor-pointer"
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
<div className="aspect-square bg-muted flex items-center justify-center overflow-hidden pointer-events-none select-none">
|
||||
{imageSrc ? (
|
||||
<img src={imageSrc} alt={ad.headline || "Advertisement"} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<Megaphone className="size-6 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(ad.headline || ad.description || primaryCta) && (
|
||||
<div className="p-3 flex flex-col gap-1.5">
|
||||
{ad.headline && <p className="text-sm font-medium leading-snug pointer-events-none select-none">{ad.headline}</p>}
|
||||
{ad.description && <p className="text-xs text-muted-foreground line-clamp-2 pointer-events-none select-none">{ad.description}</p>}
|
||||
{primaryCta && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="mt-1 w-full"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCtaClick?.(ad, primaryCta);
|
||||
}}
|
||||
>
|
||||
{primaryCta.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── SidebarSkeleton ──────────────────────────────────────────────────────────
|
||||
|
||||
export function SidebarSkeleton() {
|
||||
return (
|
||||
<div className="w-full max-w-xs rounded-lg border bg-card overflow-hidden flex flex-col">
|
||||
<Skeleton className="aspect-square w-full" />
|
||||
<div className="p-3 flex flex-col gap-1.5">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-8 w-full mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user