// page-projects.jsx — live demo gallery; click → open in new tab
const { SiteCtx: SiteCtx_P, useReveal: useReveal_P, I18N: I18N_P } = window.MM_HOOKS;
function ProjectsPage() {
const { lang } = React.useContext(SiteCtx_P);
const t = I18N_P[lang];
const demos = window.SITE_DATA.DEMOS;
const [hRef, hShown] = useReveal_P({ threshold: 0.1 });
return (
{t.projects_eyebrow}
{t.projects_title}
{t.projects_sub}
{demos.map((d, i) => )}
[ {demos.filter(d => !d.placeholder).length} live · {demos.filter(d => d.placeholder).length} wip ]
);
}
function DemoCard({ d, i }) {
const { lang } = React.useContext(SiteCtx_P);
const [ref, shown] = useReveal_P({ threshold: 0.1 });
const [hovered, setHovered] = React.useState(false);
const [loaded, setLoaded] = React.useState(false);
const isLive = !d.placeholder && d.url && (d.url.startsWith("http") || d.url.endsWith(".html"));
const initials = (d.title_en || "X").split(/\s+/).slice(0, 2).map(w => w[0]).join("").toUpperCase();
const Tag = isLive ? "a" : "div";
const linkProps = isLive
? { href: d.url, target: "_blank", rel: "noopener noreferrer" }
: {};
return (
setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{isLive ? d.url.replace(/^https?:\/\//, "").replace(/\/$/, "").slice(0, 40) : "—— wip ——"}
{isLive ? "live" : "soon"}
{isLive ? (
<>
{!loaded &&
}
#{String(i + 1).padStart(2, "0")}
{lang === "zh" ? d.title_zh : d.title_en}
{lang === "zh" ? d.desc_zh : d.desc_en}
{isLive ? (
{lang === "zh" ? "试玩" : "Try it"} ↗
) : (
{lang === "zh" ? "建设中…" : "Work in progress…"}
)}
);
}
function DemoBlueprint({ d, initials, lang }) {
const variants = ["bars", "nodes", "grid", "wave"];
let h = 0;
for (const c of d.id) h = (h * 31 + c.charCodeAt(0)) | 0;
const variant = variants[Math.abs(h) % variants.length];
return (
{initials}
{lang === "zh" ? "蓝图阶段" : "BLUEPRINT"}
{d.id}
);
}
window.ProjectsPage = ProjectsPage;