Files
cydweb/components/firmware-preview.tsx

155 lines
5.1 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { useTranslations } from "next-intl";
import type { FirmwareInfo } from "@/lib/firmware";
import { boardPreviewSize } from "@/lib/boards";
import { sensorMediaForProject } from "@/lib/sensor-media";
import { cn } from "@/lib/utils";
type Tab = "ui" | "sensor";
/** On-device HTML UI mock + optional hardware still, switchable via tabs. */
export function FirmwarePreview({
fw,
className,
title,
}: {
fw: FirmwareInfo;
className?: string;
title?: string;
}) {
const t = useTranslations("firmware");
const hostRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
const { w: screenW, h: screenH } = boardPreviewSize(fw.boardId);
const sensor = sensorMediaForProject(fw.project);
const hasUi = Boolean(fw.media.preview);
const [tab, setTab] = useState<Tab>(hasUi ? "ui" : "sensor");
useEffect(() => {
setTab(hasUi ? "ui" : sensor ? "sensor" : "ui");
}, [fw.id, hasUi, sensor]);
useEffect(() => {
const el = hostRef.current;
if (!el || tab !== "ui") return;
const update = () => {
const { width, height } = el.getBoundingClientRect();
if (width <= 0 || height <= 0) return;
setScale(Math.min(width / screenW, height / screenH));
};
update();
const ro = new ResizeObserver(update);
ro.observe(el);
return () => ro.disconnect();
}, [fw.media.preview, tab, screenW, screenH]);
const showTabs = hasUi && Boolean(sensor);
const initials = fw.name
.replace(/[^A-Za-z0-9\u4e00-\u9fff]+/g, " ")
.trim()
.split(/\s+/)
.slice(0, 2)
.map((w) => w[0]?.toUpperCase() ?? "")
.join("");
return (
<div
className={cn(
"relative flex h-full w-full flex-col overflow-hidden bg-[#0A1016]",
className
)}
>
{showTabs && (
<div
className="absolute left-2 top-2 z-10 flex gap-0.5 rounded-md bg-black/60 p-0.5 backdrop-blur-sm"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<button
type="button"
className={cn(
"rounded px-2 py-0.5 text-[0.65rem] font-semibold tracking-wide transition-colors",
tab === "ui"
? "bg-[var(--accent)] text-black"
: "text-[var(--ink)]/80 hover:text-[var(--ink)]"
)}
onClick={(e) => {
e.stopPropagation();
setTab("ui");
}}
>
{t("tabUi")}
</button>
<button
type="button"
className={cn(
"rounded px-2 py-0.5 text-[0.65rem] font-semibold tracking-wide transition-colors",
tab === "sensor"
? "bg-[var(--accent)] text-black"
: "text-[var(--ink)]/80 hover:text-[var(--ink)]"
)}
onClick={(e) => {
e.stopPropagation();
setTab("sensor");
}}
>
{t("tabSensor")}
</button>
</div>
)}
<div className="relative min-h-0 flex-1">
{tab === "ui" && hasUi ? (
<div
ref={hostRef}
className="relative h-full w-full overflow-hidden bg-[#0A1016]"
>
<iframe
title={title || `${fw.name} UI preview`}
src={fw.media.preview!}
width={screenW}
height={screenH}
loading="lazy"
sandbox="allow-scripts"
tabIndex={-1}
className="pointer-events-none absolute left-1/2 top-1/2 border-0 bg-[#0A1016]"
style={{
transform: `translate(-50%, -50%) scale(${scale})`,
transformOrigin: "center center",
}}
/>
</div>
) : tab === "sensor" && sensor ? (
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-[radial-gradient(ellipse_at_50%_30%,rgba(240,197,10,0.08),transparent_60%),#0A1016] p-3">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={sensor.src}
alt={sensor.label}
className="max-h-[78%] max-w-full object-contain drop-shadow-[0_8px_24px_rgba(0,0,0,0.45)]"
loading="lazy"
draggable={false}
/>
<span className="text-center text-[0.7rem] font-medium tracking-wide text-[var(--muted)]">
{sensor.label}
</span>
</div>
) : (
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-[radial-gradient(ellipse_at_30%_20%,rgba(240,197,10,0.18),transparent_55%),linear-gradient(160deg,#1a1610,#0e0c09)]">
<span className="font-[family-name:var(--font-display)] text-3xl font-extrabold tracking-tight text-[var(--accent-hot)]/80">
{initials || "FW"}
</span>
<span className="text-[0.65rem] uppercase tracking-[0.14em] text-[var(--muted)]">
HTML preview soon
</span>
</div>
)}
</div>
</div>
);
}