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.
This commit is contained in:
Atakan Doğan Özban
2026-08-01 19:58:24 +02:00
parent 89c49bcd33
commit 6bf2bffc55
21 changed files with 1027 additions and 20 deletions
+31 -16
View File
@@ -1,6 +1,8 @@
import Link from "next/link";
import type { ReactNode } from "react";
import { FooterBadgeMarquee } from "@/components/FooterBadgeMarquee";
import { Logo } from "@/components/Logo";
import { prisma } from "@/lib/db";
import {
DOCKER_HUB_URL,
DOCS_URL,
@@ -92,13 +94,38 @@ const STATUS_URL = "https://status.atakanozban.com/status/2";
const DOCS_INTRO_URL = `${DOCS_URL.replace(/\/$/, "")}/docs/intro`;
const DOCS_API_URL = `${DOCS_URL.replace(/\/$/, "")}/docs/api/overview`;
export function Footer() {
export async function Footer() {
const year = new Date().getFullYear();
let initialBadges: {
id: string;
name: string;
embedHtml: string | null;
imageUrl: string | null;
linkUrl: string | null;
sortOrder: number;
}[] = [];
try {
initialBadges = await prisma.footerBadge.findMany({
where: { isActive: true },
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
select: {
id: true,
name: true,
embedHtml: true,
imageUrl: true,
linkUrl: true,
sortOrder: true,
},
});
} catch {
// Table may not exist yet during migrate; marquee client will retry via API.
}
return (
<footer className="mt-20 w-full border-t border-gray-800 bg-black/40 pb-8 pt-16 text-sm text-gray-400">
<div className="mx-auto max-w-6xl px-6">
<div className="grid grid-cols-1 gap-8 pb-12 md:grid-cols-12">
<div className="grid grid-cols-1 gap-8 pb-8 md:grid-cols-12">
<div className="flex flex-col gap-4 md:col-span-5">
<Logo size="sm" beta />
<p className="max-w-sm text-gray-500">
@@ -125,20 +152,6 @@ export function Footer() {
<DockerIcon className="h-5 w-5" />
</a>
</div>
<a
href="https://www.producthunt.com/products/songs2vid?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-songs2vid"
target="_blank"
rel="noopener noreferrer"
className="mt-1 inline-block"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt="Songs2VID - Bulk render audio & artworks into videos in seconds | Product Hunt"
width={250}
height={54}
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1206681&theme=dark&t=1785422871345"
/>
</a>
</div>
<div className="grid grid-cols-2 gap-8 sm:grid-cols-3 md:col-span-7">
@@ -185,6 +198,8 @@ export function Footer() {
</div>
</div>
<FooterBadgeMarquee initialBadges={initialBadges} />
<div className="flex flex-col items-center gap-3 border-t border-gray-900 pt-8 text-xs text-gray-500 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-2 sm:justify-start">
<span>© {year} Songs2VID. All rights reserved.</span>