Add Hardware tab with sensor stills next to UI previews.
Map each firmware project to a Commons or original module photo for local catalog browsing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { FirmwareInfo } from "@/lib/firmware";
|
||||
import { sensorMediaForProject } from "@/lib/sensor-media";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const SCREEN_W = 320;
|
||||
const SCREEN_H = 240;
|
||||
|
||||
/** Renders HTML on-device UI mock (preferred) — never uses raster covers. */
|
||||
type Tab = "ui" | "sensor";
|
||||
|
||||
/** On-device HTML UI mock + optional hardware still, switchable via tabs. */
|
||||
export function FirmwarePreview({
|
||||
fw,
|
||||
className,
|
||||
@@ -17,12 +21,20 @@ export function FirmwarePreview({
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
const t = useTranslations("firmware");
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
const [scale, setScale] = useState(1);
|
||||
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) return;
|
||||
if (!el || tab !== "ui") return;
|
||||
|
||||
const update = () => {
|
||||
const { width, height } = el.getBoundingClientRect();
|
||||
@@ -34,34 +46,9 @@ export function FirmwarePreview({
|
||||
const ro = new ResizeObserver(update);
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [fw.media.preview]);
|
||||
}, [fw.media.preview, tab]);
|
||||
|
||||
if (fw.media.preview) {
|
||||
return (
|
||||
<div
|
||||
ref={hostRef}
|
||||
className={cn(
|
||||
"relative h-full w-full overflow-hidden bg-[#0A1016]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<iframe
|
||||
title={title || `${fw.name} UI preview`}
|
||||
src={fw.media.preview}
|
||||
width={SCREEN_W}
|
||||
height={SCREEN_H}
|
||||
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>
|
||||
);
|
||||
}
|
||||
const showTabs = hasUi && Boolean(sensor);
|
||||
|
||||
const initials = fw.name
|
||||
.replace(/[^A-Za-z0-9\u4e00-\u9fff]+/g, " ")
|
||||
@@ -74,16 +61,95 @@ export function FirmwarePreview({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"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)]",
|
||||
"relative flex h-full w-full flex-col overflow-hidden bg-[#0A1016]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
{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={SCREEN_W}
|
||||
height={SCREEN_H}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user