Add dynamic footer badge marquee with admin management.

Store badges in Postgres, expose public/admin APIs, render an infinite footer slider, and ship /admin for paste-embed CRUD.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Atakan Doğan Özban
2026-08-01 19:58:24 +02:00
co-authored by Cursor
parent 42ad9c38e3
commit 7d22f722f6
21 changed files with 1027 additions and 20 deletions
+62
View File
@@ -0,0 +1,62 @@
"use client";
import { useEffect, useState } from "react";
import { BadgeEmbed } from "@/components/BadgeEmbed";
export type PublicFooterBadge = {
id: string;
name: string;
embedHtml: string | null;
imageUrl: string | null;
linkUrl: string | null;
sortOrder: number;
};
type Props = {
initialBadges?: PublicFooterBadge[];
};
export function FooterBadgeMarquee({ initialBadges = [] }: Props) {
const [badges, setBadges] = useState<PublicFooterBadge[]>(initialBadges);
useEffect(() => {
let cancelled = false;
fetch("/api/badges")
.then(async (res) => {
if (!res.ok) return;
const data = await res.json();
if (!cancelled && Array.isArray(data.badges)) {
setBadges(data.badges);
}
})
.catch(() => {});
return () => {
cancelled = true;
};
}, []);
if (badges.length === 0) return null;
// Duplicate track for seamless infinite scroll
const loop = [...badges, ...badges];
const durationSec = Math.max(18, badges.length * 10);
return (
<div className="badge-marquee mb-12" aria-label="Featured on">
<div
className="badge-marquee-track"
style={{ ["--badge-marquee-duration" as string]: `${durationSec}s` }}
>
{loop.map((badge, index) => (
<div
key={`${badge.id}-${index}`}
className="badge-marquee-item"
aria-hidden={index >= badges.length ? true : undefined}
>
<BadgeEmbed badge={badge} />
</div>
))}
</div>
</div>
);
}