mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
add: more commits
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -2,12 +2,16 @@
|
||||
|
||||
import { Megaphone } from "lucide-react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { resolveAssetSrc } from "@/utils/media.util";
|
||||
|
||||
// Height per size — controls the strip's visual weight, not its width (always full-width).
|
||||
// sm/md are fixed; lg/xl use clamp() so they scale with viewport width between
|
||||
// a min and max instead of jumping at breakpoints.
|
||||
const SIZE_HEIGHT = {
|
||||
sm: "h-20",
|
||||
md: "h-32",
|
||||
lg: "h-48",
|
||||
sm: "h-[80px]",
|
||||
md: "h-[140px]",
|
||||
lg: "h-[clamp(220px,26vw,320px)]",
|
||||
xl: "h-[clamp(320px,34vw,460px)]",
|
||||
};
|
||||
|
||||
// ── Banner ───────────────────────────────────────────────────────────────────
|
||||
@@ -19,13 +23,13 @@ const SIZE_HEIGHT = {
|
||||
*
|
||||
* Props:
|
||||
* ad — advertisement object { headline, ctas, image, image_url, advertisement_id }
|
||||
* size — "sm" | "md" | "lg" (default "md")
|
||||
* size — "sm" | "md" | "lg" | "xl" (default "md")
|
||||
* onCtaClick — (ad, cta) => void, called on click. cta may be undefined if the ad has none.
|
||||
*/
|
||||
export function Banner({ ad, size, onCtaClick }) {
|
||||
if (!ad) return null;
|
||||
|
||||
const imageSrc = ad.image?.file_url || ad.image_url || null;
|
||||
const imageSrc = resolveAssetSrc(ad.image) || ad.image_url || null;
|
||||
const ctas = Array.isArray(ad.ctas) ? ad.ctas : [];
|
||||
const resolvedSize = ad.size || size || "md";
|
||||
const heightClass = SIZE_HEIGHT[resolvedSize] ?? SIZE_HEIGHT.md;
|
||||
@@ -39,13 +43,13 @@ export function Banner({ ad, size, onCtaClick }) {
|
||||
className={`relative w-full rounded-lg bg-muted overflow-hidden flex items-center justify-center text-left ${heightClass}`}
|
||||
>
|
||||
{imageSrc ? (
|
||||
<img src={imageSrc} alt={ad.headline || "Advertisement"} className="w-full h-full object-cover" />
|
||||
<img src={imageSrc} alt={ad.headline || "Advertisement"} className="w-full h-full object-cover pointer-events-none select-none" />
|
||||
) : (
|
||||
<Megaphone className="size-6 text-muted-foreground" />
|
||||
<Megaphone className="size-6 text-muted-foreground pointer-events-none select-none" />
|
||||
)}
|
||||
|
||||
{ad.headline && (
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/0 to-black/0 flex items-end p-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/0 to-black/0 flex items-end p-4 pointer-events-none select-none">
|
||||
<p className="text-white font-medium text-sm sm:text-base">{ad.headline}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Megaphone } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { resolveAssetSrc } from "@/utils/media.util";
|
||||
|
||||
// ── Hero ─────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
@@ -18,24 +19,24 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
export function Hero({ ad, onCtaClick }) {
|
||||
if (!ad) return null;
|
||||
|
||||
const imageSrc = ad.image?.file_url || ad.image_url || null;
|
||||
const imageSrc = resolveAssetSrc(ad.image) || ad.image_url || null;
|
||||
const ctas = Array.isArray(ad.ctas) ? ad.ctas : [];
|
||||
|
||||
return (
|
||||
<div className="flex xs:flex-col lg:flex-row items-center gap-6">
|
||||
<div className="flex xs:flex-col lg:flex-row items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
{ad.badge_label && (
|
||||
<Badge variant="outline">
|
||||
<Badge variant="outline" className="pointer-events-none select-none">
|
||||
<Megaphone /> {ad.badge_label}
|
||||
</Badge>
|
||||
)}
|
||||
{ad.headline && (
|
||||
<div className="font-bold text-4xl leading-12">
|
||||
<div className="font-bold text-4xl leading-12 pointer-events-none select-none">
|
||||
{ad.headline}
|
||||
</div>
|
||||
)}
|
||||
{ad.description && (
|
||||
<p className="max-w-lg">
|
||||
<p className="max-w-lg pointer-events-none select-none">
|
||||
{ad.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -53,7 +54,7 @@ export function Hero({ ad, onCtaClick }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-lg bg-muted w-xl aspect-video flex items-center justify-center overflow-hidden">
|
||||
<div className="rounded-lg bg-muted w-xl aspect-video 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" />
|
||||
) : (
|
||||
@@ -68,7 +69,7 @@ export function Hero({ ad, onCtaClick }) {
|
||||
|
||||
export function HeroSkeleton() {
|
||||
return (
|
||||
<div className="flex xs:flex-col lg:flex-row items-center gap-6">
|
||||
<div className="flex xs:flex-col lg:flex-row items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-3 w-full max-w-lg">
|
||||
<Skeleton className="h-6 w-32 rounded-full" />
|
||||
<Skeleton className="h-10 w-3/4" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ResponsiveModal from "@/components/generic/ResponsiveModal";
|
||||
import { resolveAssetSrc } from "@/utils/media.util";
|
||||
|
||||
// ── Popup ────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
@@ -11,17 +12,23 @@ import ResponsiveModal from "@/components/generic/ResponsiveModal";
|
||||
* 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
|
||||
* 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 }) {
|
||||
export function Popup({ ad, open, onOpenChange, onCtaClick, onDismissForever }) {
|
||||
if (!ad) return null;
|
||||
|
||||
const imageSrc = ad.image?.file_url || ad.image_url || 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}
|
||||
@@ -45,10 +52,20 @@ export function Popup({ ad, open, onOpenChange, onCtaClick }) {
|
||||
}
|
||||
>
|
||||
{imageSrc && (
|
||||
<div className="rounded-lg bg-muted aspect-video flex items-center justify-center overflow-hidden">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Megaphone } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { resolveAssetSrc } from "@/utils/media.util";
|
||||
|
||||
// ── Sidebar ──────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
@@ -18,7 +19,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
export function Sidebar({ ad, onCtaClick }) {
|
||||
if (!ad) return null;
|
||||
|
||||
const imageSrc = ad.image?.file_url || ad.image_url || null;
|
||||
const imageSrc = resolveAssetSrc(ad.image) || ad.image_url || null;
|
||||
const ctas = Array.isArray(ad.ctas) ? ad.ctas : [];
|
||||
const primaryCta = ctas[0];
|
||||
|
||||
@@ -31,7 +32,7 @@ export function Sidebar({ ad, onCtaClick }) {
|
||||
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">
|
||||
<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" />
|
||||
) : (
|
||||
@@ -41,8 +42,8 @@ export function Sidebar({ ad, onCtaClick }) {
|
||||
|
||||
{(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">{ad.headline}</p>}
|
||||
{ad.description && <p className="text-xs text-muted-foreground line-clamp-2">{ad.description}</p>}
|
||||
{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"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRef, useState, useEffect, useCallback } from "react";
|
||||
import { Music2, RotateCcw, RotateCw, Play, Pause, VolumeOff, Volume2 } from "lucide-react";
|
||||
import api from "@/utils/api.util";
|
||||
import { MediaFallback } from "@/components/generic/MediaFallback";
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -172,11 +173,7 @@ export function AudioBlock({ content }) {
|
||||
// ── States ────────────────────────────────────────────────────────────────
|
||||
|
||||
if (fetchLoading) {
|
||||
return (
|
||||
<div className="w-full rounded-xl border border-border bg-card flex items-center justify-center h-28">
|
||||
<div className="w-5 h-5 rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground animate-spin" />
|
||||
</div>
|
||||
);
|
||||
return <MediaFallback className="w-full h-28 rounded-xl border border-border" />;
|
||||
}
|
||||
|
||||
if (fetchError) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import api from "@/utils/api.util";
|
||||
import { MediaFallback } from "@/components/generic/MediaFallback";
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -426,11 +427,7 @@ export function VideoBlock({ content }) {
|
||||
}
|
||||
|
||||
if (fetchLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center aspect-video rounded-lg bg-black/90">
|
||||
<div className="w-10 h-10 rounded-full border-4 border-white/20 border-t-white animate-spin" />
|
||||
</div>
|
||||
);
|
||||
return <MediaFallback className="aspect-video rounded-lg" />;
|
||||
}
|
||||
|
||||
if (fetchError || !blobUrl) {
|
||||
|
||||
Reference in New Issue
Block a user