add: more commits

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-03 16:21:27 +08:00
parent 17326b2c2e
commit 7e964f2432
112 changed files with 9160 additions and 3461 deletions
@@ -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) {