Board chips reserve future models; firmware uses searchable cards with detail pane and optional meta.json media/docs. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import { useTranslations } from "next-intl";
|
|
import { ExternalLink, FileText } from "lucide-react";
|
|
import type { FirmwareInfo } from "@/lib/firmware";
|
|
import { formatBytes } from "@/lib/utils";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { EspWebInstall } from "@/components/esp-web-install";
|
|
|
|
export function FirmwareDetail({
|
|
fw,
|
|
serialOk,
|
|
}: {
|
|
fw: FirmwareInfo | null;
|
|
serialOk: boolean;
|
|
}) {
|
|
const t = useTranslations();
|
|
|
|
if (!fw) {
|
|
return (
|
|
<div className="rounded-[14px] border border-dashed border-[var(--line)] bg-[rgba(0,0,0,0.2)] px-5 py-10 text-center text-sm text-[var(--muted)]">
|
|
{t("firmware.noneSelected")}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const preview = fw.media.video || fw.media.animation || fw.media.cover;
|
|
|
|
return (
|
|
<aside className="sticky top-4 flex flex-col overflow-hidden rounded-[14px] border border-[var(--line)] bg-[color-mix(in_srgb,var(--bg-elev)_92%,transparent)] backdrop-blur-sm">
|
|
<div className="border-b border-[var(--line)] px-4 py-3">
|
|
<p className="m-0 text-xs text-[var(--muted)]">
|
|
{t("firmware.selectedLabel")}
|
|
</p>
|
|
<h3 className="m-0 mt-0.5 truncate font-[family-name:var(--font-display)] text-lg font-bold">
|
|
{fw.name}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="aspect-video w-full border-b border-[var(--line)] bg-black/40">
|
|
{fw.media.video ? (
|
|
<video
|
|
key={fw.media.video}
|
|
className="h-full w-full object-cover"
|
|
controls
|
|
playsInline
|
|
preload="metadata"
|
|
poster={fw.media.cover || undefined}
|
|
>
|
|
<source src={fw.media.video} />
|
|
</video>
|
|
) : preview ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={preview}
|
|
alt=""
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="flex h-full flex-col items-center justify-center gap-1 px-4 text-center">
|
|
<p className="m-0 text-sm text-[var(--muted)]">
|
|
{t("firmware.mediaPlaceholder")}
|
|
</p>
|
|
<p className="m-0 text-xs text-[var(--muted)] opacity-70">
|
|
meta.json → media.cover / animation / video
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4 p-4">
|
|
{(fw.description || fw.summary) && (
|
|
<p className="m-0 text-sm leading-relaxed text-[var(--muted)]">
|
|
{fw.description || fw.summary}
|
|
</p>
|
|
)}
|
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{fw.tags.map((tag) => (
|
|
<Badge key={tag}>{tag}</Badge>
|
|
))}
|
|
<Badge variant="muted">{formatBytes(fw.size)}</Badge>
|
|
{fw.flash_size && <Badge variant="muted">{fw.flash_size}</Badge>}
|
|
{fw.git && <Badge variant="muted">git {fw.git}</Badge>}
|
|
{fw.date && <Badge variant="muted">{fw.date}</Badge>}
|
|
</div>
|
|
|
|
<p className="m-0 text-xs text-[var(--muted)]">
|
|
{fw.project}/{fw.variant} · {fw.board}
|
|
</p>
|
|
|
|
{fw.media.docs && (
|
|
<a
|
|
href={fw.media.docs}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-2 text-sm text-[var(--accent-hot)] underline-offset-2 hover:underline"
|
|
>
|
|
<FileText className="size-4" />
|
|
{t("firmware.docs")}
|
|
<ExternalLink className="size-3 opacity-70" />
|
|
</a>
|
|
)}
|
|
|
|
<EspWebInstall
|
|
manifest={fw.manifestUrl}
|
|
label={t("install.cta")}
|
|
disabled={!serialOk}
|
|
/>
|
|
|
|
<ol className="m-0 list-decimal space-y-1 pl-4 text-xs text-[var(--muted)]">
|
|
<li>{t("install.steps.1")}</li>
|
|
<li>{t("install.steps.2")}</li>
|
|
<li>{t("install.steps.3")}</li>
|
|
<li>{t("install.steps.4")}</li>
|
|
</ol>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|