Store badges in Postgres, expose public/admin APIs, render an infinite footer slider, and ship /admin for paste-embed CRUD.
74 lines
2.0 KiB
TypeScript
74 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
|
|
type PublicBadge = {
|
|
id: string;
|
|
name: string;
|
|
embedHtml: string | null;
|
|
imageUrl: string | null;
|
|
linkUrl: string | null;
|
|
};
|
|
|
|
/**
|
|
* Renders a badge embed safely inside the marquee slot.
|
|
* Prefer structured image+link; fall back to HTML (re-executing scripts when present).
|
|
*/
|
|
export function BadgeEmbed({ badge }: { badge: PublicBadge }) {
|
|
const hostRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const host = hostRef.current;
|
|
if (!host) return;
|
|
host.replaceChildren();
|
|
|
|
if (badge.imageUrl && badge.linkUrl) {
|
|
const a = document.createElement("a");
|
|
a.href = badge.linkUrl;
|
|
a.target = "_blank";
|
|
a.rel = "noopener noreferrer";
|
|
a.title = badge.name;
|
|
a.className = "badge-marquee-link";
|
|
const img = document.createElement("img");
|
|
img.src = badge.imageUrl;
|
|
img.alt = badge.name;
|
|
img.loading = "lazy";
|
|
img.decoding = "async";
|
|
a.appendChild(img);
|
|
host.appendChild(a);
|
|
return;
|
|
}
|
|
|
|
if (!badge.embedHtml) return;
|
|
|
|
const template = document.createElement("template");
|
|
template.innerHTML = badge.embedHtml.trim();
|
|
|
|
const scripts: HTMLScriptElement[] = [];
|
|
template.content.querySelectorAll("script").forEach((node) => {
|
|
scripts.push(node.cloneNode(true) as HTMLScriptElement);
|
|
node.remove();
|
|
});
|
|
|
|
host.appendChild(template.content.cloneNode(true));
|
|
|
|
for (const oldScript of scripts) {
|
|
const script = document.createElement("script");
|
|
for (const attr of oldScript.attributes) {
|
|
script.setAttribute(attr.name, attr.value);
|
|
}
|
|
if (oldScript.textContent) script.textContent = oldScript.textContent;
|
|
host.appendChild(script);
|
|
}
|
|
}, [badge.embedHtml, badge.imageUrl, badge.linkUrl, badge.name]);
|
|
|
|
return (
|
|
<div
|
|
ref={hostRef}
|
|
className="badge-marquee-slot"
|
|
data-badge-id={badge.id}
|
|
data-badge-name={badge.name}
|
|
/>
|
|
);
|
|
}
|