- Pass currentLocale to jsonLd.serie and jsonLd.framework to ensure English headline, name, and properties - Localize JSON-LD @id anchors to #article, #about, #glossary, #organization, and #author for English routes - Pass isEn prop to PageHero across all internal pages to prevent breadcrumbs from defaulting to Portuguese - Add scripts/verifica-vazamentos.py to smoke-imagem.sh to permanently test all 36 English URLs (visible text and ld+json) Co-Authored-By: Antigravity <noreply@google.com>
195 lines
8.2 KiB
React
195 lines
8.2 KiB
React
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { usePathname } from "next/navigation";
|
|
import Header from "@/components/Header";
|
|
import Footer from "@/components/Footer";
|
|
import PageHero from "@/components/PageHero";
|
|
import Link from "next/link";
|
|
import seriesDataPt from "@/public/series.json";
|
|
import seriesDataEn from "@/data/en/series.json";
|
|
import { getLocalizedHref } from "@/i18n/links";
|
|
import {
|
|
AlertTriangle,
|
|
ArrowRight,
|
|
ArrowUpRight,
|
|
Compass,
|
|
Home,
|
|
Scale,
|
|
ShieldCheck,
|
|
Sparkles,
|
|
} from "lucide-react";
|
|
|
|
const FILTROS_PT = [
|
|
{ id: "todas", rotulo: "Todas", testa: () => true },
|
|
{ id: "riscv", rotulo: "RISC-V", testa: (s) => s.arquitetura.includes("RISC-V") },
|
|
{ id: "xtensa", rotulo: "Xtensa", testa: (s) => !s.arquitetura.includes("RISC-V") },
|
|
{ id: "matter", rotulo: "Matter", testa: (s) => s.matter === "Sim" },
|
|
{ id: "ia", rotulo: "Aceleração de IA", testa: (s) => Boolean(s.aceleradores_ia) },
|
|
];
|
|
|
|
const FILTROS_EN = [
|
|
{ id: "todas", rotulo: "All", testa: () => true },
|
|
{ id: "riscv", rotulo: "RISC-V", testa: (s) => s.arquitetura.includes("RISC-V") },
|
|
{ id: "xtensa", rotulo: "Xtensa", testa: (s) => !s.arquitetura.includes("RISC-V") },
|
|
{ id: "matter", rotulo: "Matter", testa: (s) => s.matter === "Yes" || s.matter === "Sim" },
|
|
{ id: "ia", rotulo: "AI Acceleration", testa: (s) => Boolean(s.aceleradores_ia) },
|
|
];
|
|
|
|
/** Marcador de segurança: o recurso que diferencia a série, não o que todas têm. */
|
|
function nivelSeguranca(serie, isEn) {
|
|
const seg = serie.seguranca || {};
|
|
if (seg.key_manager === "Sim" || seg.key_manager === "Yes") return "Key Manager";
|
|
if (String(seg.ecdsa).startsWith("Sim") || String(seg.ecdsa).startsWith("Yes")) {
|
|
return isEn ? "Hardware ECDSA" : "ECDSA em hardware";
|
|
}
|
|
if (String(seg.ecc).startsWith("Sim") || String(seg.ecc).startsWith("Yes")) {
|
|
return isEn ? "Hardware ECC" : "ECC em hardware";
|
|
}
|
|
if (String(seg.aes).startsWith("AES")) return "AES + SHA";
|
|
return isEn ? "SHA only" : "Só SHA";
|
|
}
|
|
|
|
const simPt = (v) => !String(v).includes("Não");
|
|
const simEn = (v) => !String(v).includes("No") && !String(v).includes("Não");
|
|
|
|
export default function Series() {
|
|
const pathname = usePathname();
|
|
const isEn = pathname?.startsWith("/en");
|
|
const currentLocale = isEn ? "en" : "pt";
|
|
const seriesData = isEn ? seriesDataEn : seriesDataPt;
|
|
const filtros = isEn ? FILTROS_EN : FILTROS_PT;
|
|
|
|
const [filtro, setFiltro] = useState("todas");
|
|
const todas = Object.entries(seriesData);
|
|
const ativo = filtros.find((f) => f.id === filtro) || filtros[0];
|
|
const series = useMemo(() => todas.filter(([, s]) => ativo.testa(s)), [todas, ativo]);
|
|
|
|
return (
|
|
<div className="site-canvas min-h-screen">
|
|
<Header />
|
|
|
|
<main id="conteudo">
|
|
<PageHero
|
|
isEn={isEn}
|
|
trail={[{ label: isEn ? "Series" : "Séries" }]}
|
|
icon={Compass}
|
|
label={isEn ? `${todas.length} Espressif families` : `${todas.length} famílias da Espressif`}
|
|
title={isEn ? "ESP32 Series" : "Séries ESP32"}
|
|
lede={
|
|
isEn
|
|
? "Each series was designed for a specific project type, from battery-powered sensors to AI accelerators. Open the sheet to see pinout, peripherals, and security."
|
|
: "Cada série foi desenhada para um tipo de projeto, do sensor a bateria ao acelerador de IA. Abra a ficha para ver pinagem, periféricos e segurança."
|
|
}
|
|
/>
|
|
|
|
<section className="shell pt-10 sm:pt-12" aria-label={isEn ? "Series list" : "Lista de séries"}>
|
|
<div role="group" aria-label={isEn ? "Filter series" : "Filtrar séries"} className="flex flex-wrap gap-2">
|
|
{filtros.map((f) => (
|
|
<button key={f.id} type="button" aria-pressed={filtro === f.id} onClick={() => setFiltro(f.id)} className="chip-btn">
|
|
{f.rotulo}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p role="status" aria-live="polite" className="mt-4 text-sm text-muted">
|
|
{series.length} {series.length === 1 ? (isEn ? "series" : "série") : (isEn ? "series" : "séries")}
|
|
</p>
|
|
|
|
<div className="mt-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{series.map(([key, serie]) => {
|
|
const riscv = serie.arquitetura.includes("RISC-V");
|
|
const fatos = isEn
|
|
? [
|
|
["Cores", serie.nucleos],
|
|
["Frequency", serie.frequencia],
|
|
["SRAM", serie.memoria_sram],
|
|
["Wi-Fi", simEn(serie.wifi) ? "Yes" : "No"],
|
|
["Bluetooth", String(serie.bluetooth).includes("No") || String(serie.bluetooth).includes("Não") ? "No" : "Yes"],
|
|
serie.consumo_energia?.deep_sleep ? ["Deep sleep", serie.consumo_energia.deep_sleep] : null,
|
|
].filter(Boolean)
|
|
: [
|
|
["Núcleos", serie.nucleos],
|
|
["Frequência", serie.frequencia],
|
|
["SRAM", serie.memoria_sram],
|
|
["Wi-Fi", simPt(serie.wifi) ? "Sim" : "Não"],
|
|
["Bluetooth", serie.bluetooth === "Não" ? "Não" : "Sim"],
|
|
serie.consumo_energia?.deep_sleep ? ["Deep sleep", serie.consumo_energia.deep_sleep] : null,
|
|
].filter(Boolean);
|
|
return (
|
|
<Link key={key} href={getLocalizedHref(`/series/${key}`, currentLocale)} className="card card-interactive group flex flex-col p-5 sm:p-6">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<h2 className="font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{key}</h2>
|
|
<span className="chip">{riscv ? "RISC-V" : "Xtensa"}</span>
|
|
</div>
|
|
<p className="mt-2 line-clamp-2 min-h-[2.9rem] text-sm leading-relaxed text-muted">{serie.descricao}</p>
|
|
|
|
<dl className="mt-5 divide-y divide-line border-y border-line text-sm">
|
|
{fatos.map(([rotulo, valor]) => (
|
|
<div key={rotulo} className="flex items-baseline justify-between gap-4 py-2">
|
|
<dt className="text-muted">{rotulo}</dt>
|
|
<dd className={`text-right font-medium ${valor === "Não" || valor === "No" ? "text-muted" : "text-ink"}`}>{valor}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
|
|
<ul className="mt-4 flex flex-1 flex-wrap content-start gap-1.5">
|
|
{serie.aceleradores_ia && (
|
|
<li className="chip chip-accent">
|
|
<Sparkles className="h-3 w-3" aria-hidden="true" />
|
|
{isEn ? "AI Accelerators" : "Aceleradores de IA"}
|
|
</li>
|
|
)}
|
|
<li className="chip">
|
|
<ShieldCheck className="h-3 w-3 text-accent" aria-hidden="true" />
|
|
{nivelSeguranca(serie, isEn)}
|
|
</li>
|
|
{(serie.matter === "Sim" || serie.matter === "Yes") && (
|
|
<li className="chip">
|
|
<Home className="h-3 w-3 text-accent" aria-hidden="true" />
|
|
Matter
|
|
</li>
|
|
)}
|
|
{serie.status_documentacao && (
|
|
<li className="chip">
|
|
<AlertTriangle className="h-3 w-3 text-gold" aria-hidden="true" />
|
|
{isEn ? "Preliminary documentation" : "Documentação preliminar"}
|
|
</li>
|
|
)}
|
|
</ul>
|
|
|
|
<span className="mt-5 inline-flex items-center gap-1 text-sm font-medium text-accent">
|
|
{isEn ? "Open datasheet" : "Abrir ficha"}
|
|
<ArrowUpRight className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
|
|
</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="shell section">
|
|
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
|
|
<div className="max-w-2xl">
|
|
<h2 className="section-title">
|
|
{isEn ? "Unsure between two series?" : "Ficou em dúvida entre duas séries?"}
|
|
</h2>
|
|
<p className="mt-3 text-base text-muted">
|
|
{isEn
|
|
? "The comparator tool puts up to four models side by side: cores, radio, memory, peripherals, and security."
|
|
: "O comparador coloca até quatro modelos lado a lado: núcleos, rádio, memória, periféricos e segurança."}
|
|
</p>
|
|
</div>
|
|
<Link href={getLocalizedHref("/comparacao", currentLocale)} className="btn btn-primary shrink-0">
|
|
<Scale className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "Compare series" : "Comparar séries"}
|
|
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
|
</Link>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|