Files
ESPDocs/app/[locale]/series/page.jsx
T
Lucas RayanandClaude Sonnet 5.5 5a9fe676fc
CI / Lint e auditorias (push) Successful in 1m10s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m39s
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 18s
Release / Cria a release no Gitea (push) Successful in 5s
feat(i18n): Simplified Chinese (zh-CN), search answers per locale, fr/de/es data fixes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 23:28:39 -03:00

316 lines
16 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 seriesDataEs from "@/data/es/series.json";
import seriesDataFr from "@/data/fr/series.json";
import seriesDataDe from "@/data/de/series.json";
import seriesDataZh from "@/data/zh-CN/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) },
];
const FILTROS_ES = [
{ 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 === "Yes" || s.matter === "Sim" || s.matter === "Sí" },
{ id: "ia", rotulo: "Aceleración de IA", testa: (s) => Boolean(s.aceleradores_ia) },
];
const FILTROS_FR = [
{ id: "todas", rotulo: "Toutes", 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 === "Oui" || s.matter === "Yes" || s.matter === "Sim" || s.matter === "Sí" },
{ id: "ia", rotulo: "Accélération IA", testa: (s) => Boolean(s.aceleradores_ia) },
];
const FILTROS_DE = [
{ id: "todas", rotulo: "Alle", 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 === "Ja" || s.matter === "是") || s.matter === "Yes" || s.matter === "Sim" || s.matter === "Sí" },
{ id: "ia", rotulo: "KI-Beschleunigung", testa: (s) => Boolean(s.aceleradores_ia) },
];
const FILTROS_ZH = [
{ id: "todas", rotulo: "全部", 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) => String(s.matter).startsWith("是") },
{ id: "ia", rotulo: "AI 加速", 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, isEs, isFr, isDe, isZh = false) {
const seg = serie.seguranca || {};
if (seg.key_manager === "Sim" || seg.key_manager === "Yes" || seg.key_manager === "Sí" || seg.key_manager === "Oui" || (seg.key_manager === "Ja" || seg.key_manager === "是")) return "Key Manager";
if (String(seg.ecdsa).startsWith("Sim") || String(seg.ecdsa).startsWith("Yes") || String(seg.ecdsa).startsWith("Sí") || String(seg.ecdsa).startsWith("Oui") || (String(seg.ecdsa).startsWith("Ja") || String(seg.ecdsa).startsWith("是"))) {
return isZh ? "硬件 ECDSA" : isDe ? "Hardware-ECDSA" : isFr ? "ECDSA matériel" : isEs ? "ECDSA en hardware" : isEn ? "Hardware ECDSA" : "ECDSA em hardware";
}
if (String(seg.ecc).startsWith("Sim") || String(seg.ecc).startsWith("Yes") || String(seg.ecc).startsWith("Sí") || String(seg.ecc).startsWith("Oui") || (String(seg.ecc).startsWith("Ja") || String(seg.ecc).startsWith("是"))) {
return isZh ? "硬件 ECC" : isDe ? "Hardware-ECC" : isFr ? "ECC matériel" : isEs ? "ECC en hardware" : isEn ? "Hardware ECC" : "ECC em hardware";
}
if (String(seg.aes).startsWith("AES")) return "AES + SHA";
return isZh ? "仅 SHA" : isDe ? "Nur SHA" : isFr ? "SHA uniquement" : isEs ? "Solo SHA" : 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");
const simEs = (v) => !String(v).includes("No") && !String(v).includes("Não");
const simFr = (v) => !String(v).includes("Non") && !String(v).includes("No") && !String(v).includes("Não");
const simDe = (v) => !(String(v).includes("Nein") || String(v).includes("否")) && !String(v).includes("No") && !String(v).includes("Não");
const simZh = (v) => !String(v).includes("否");
export default function Series() {
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const isEs = pathname?.startsWith("/es");
const isFr = pathname?.startsWith("/fr");
const isDe = pathname?.startsWith("/de");
const isZh = pathname?.startsWith("/zh-CN");
const currentLocale = isZh ? "zh-CN" : isDe ? "de" : isFr ? "fr" : isEs ? "es" : isEn ? "en" : "pt";
const seriesData = isZh ? seriesDataZh : isDe ? seriesDataDe : isFr ? seriesDataFr : isEs ? seriesDataEs : isEn ? seriesDataEn : seriesDataPt;
const filtros = isZh ? FILTROS_ZH : isDe ? FILTROS_DE : isFr ? FILTROS_FR : isEs ? FILTROS_ES : 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}
isEs={isEs}
isFr={isFr}
isDe={isDe} isZh={isZh}
trail={[{ label: isZh ? "系列" : isDe ? "Serien" : isFr ? "Séries" : isEs ? "Series" : isEn ? "Series" : "Séries" }]}
icon={Compass}
label={
isZh ? `${todas.length} 个乐鑫系列` : isDe
? `${todas.length} Espressif-Familien`
: isFr
? `${todas.length} familles d'Espressif`
: isEs
? `${todas.length} familias de Espressif`
: isEn
? `${todas.length} Espressif families`
: `${todas.length} famílias da Espressif`
}
title={isZh ? "ESP32 系列" : isDe ? "ESP32-Serien" : isFr ? "Séries ESP32" : isEs ? "Series ESP32" : isEn ? "ESP32 Series" : "Séries ESP32"}
lede={
isZh ? "每个系列都为特定类型的项目设计,从电池供电的传感器到 AI 加速器。展开卡片可查看引脚、外设与安全特性。" : isDe
? "Jede Serie wurde für einen bestimmten Projekttyp entwickelt, von Batteriesensoren bis hin zu KI-Beschleunigern. Öffnen Sie das Datenblatt für Pinbelegung, Peripherie und Sicherheit."
: isFr
? "Chaque série a été conçue pour un type de projet spécifique, des capteurs sur batterie aux accélérateurs IA. Ouvrez la fiche pour consulter le brochage, les périphériques et la sécurité."
: isEs
? "Cada serie fue diseñada para un tipo de proyecto específico, desde sensores a batería hasta aceleradores de IA. Abra la ficha para ver patillaje, periféricos y seguridad."
: 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={isZh ? "系列列表" : isDe ? "Serienliste" : isFr ? "Liste des séries" : isEs ? "Lista de series" : isEn ? "Series list" : "Lista de séries"}>
<div role="group" aria-label={isZh ? "筛选系列" : isDe ? "Serien filtern" : isFr ? "Filtrer les séries" : isEs ? "Filtrar series" : 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
? isZh ? "个系列" : isDe
? "Serie"
: isFr
? "série"
: isEs
? "serie"
: isEn
? "series"
: "série"
: isZh ? "个系列" : isDe
? "Serien"
: isFr
? "séries"
: isEs
? "series"
: 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 = isZh ? [
["内核", serie.nucleos],
["频率", serie.frequencia],
["SRAM", serie.memoria_sram],
["Wi-Fi", simZh(serie.wifi) ? "是" : "否"],
["蓝牙", String(serie.bluetooth).includes("否") ? "否" : "是"],
serie.consumo_energia?.deep_sleep ? ["深度睡眠", serie.consumo_energia.deep_sleep] : null,
].filter(Boolean) : isDe
? [
["Kerne", serie.nucleos],
["Taktfrequenz", serie.frequencia],
["SRAM", serie.memoria_sram],
["Wi-Fi", simDe(serie.wifi) ? "Ja" : "Nein"],
["Bluetooth", (String(serie.bluetooth).includes("Nein") || String(serie.bluetooth).includes("否")) || String(serie.bluetooth).includes("No") || String(serie.bluetooth).includes("Não") ? "Nein" : "Ja"],
serie.consumo_energia?.deep_sleep ? ["Deep sleep", serie.consumo_energia.deep_sleep] : null,
].filter(Boolean)
: isFr
? [
["Cœurs", serie.nucleos],
["Fréquence", serie.frequencia],
["SRAM", serie.memoria_sram],
["Wi-Fi", simFr(serie.wifi) ? "Oui" : "Non"],
["Bluetooth", String(serie.bluetooth).includes("Non") || String(serie.bluetooth).includes("No") || String(serie.bluetooth).includes("Não") ? "Non" : "Oui"],
serie.consumo_energia?.deep_sleep ? ["Deep sleep", serie.consumo_energia.deep_sleep] : null,
].filter(Boolean)
: isEs
? [
["Núcleos", serie.nucleos],
["Frecuencia", serie.frequencia],
["SRAM", serie.memoria_sram],
["Wi-Fi", simEs(serie.wifi) ? "Sí" : "No"],
["Bluetooth", String(serie.bluetooth).includes("No") || String(serie.bluetooth).includes("Não") ? "No" : "Sí"],
serie.consumo_energia?.deep_sleep ? ["Deep sleep", serie.consumo_energia.deep_sleep] : null,
].filter(Boolean)
: 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" || valor === "Non" || (valor === "Nein" || valor === "否") ? "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" />
{isZh ? "AI 加速器" : isDe ? "KI-Beschleuniger" : isFr ? "Accélérateurs IA" : isEs ? "Aceleradores de IA" : isEn ? "AI Accelerators" : "Aceleradores de IA"}
</li>
)}
<li className="chip">
<ShieldCheck className="h-3 w-3 text-accent" aria-hidden="true" />
{nivelSeguranca(serie, isEn, isEs, isFr, isDe, isZh)}
</li>
{(serie.matter === "Sim" || serie.matter === "Yes" || serie.matter === "Sí" || serie.matter === "Oui" || (serie.matter === "Ja" || serie.matter === "是")) && (
<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" />
{isZh ? "初步文档" : isDe ? "Vorläufige Dokumentation" : isFr ? "Documentation préliminaire" : isEs ? "Documentación preliminar" : isEn ? "Preliminary documentation" : "Documentação preliminar"}
</li>
)}
</ul>
<span className="mt-5 inline-flex items-center gap-1 text-sm font-medium text-accent">
{isZh ? "打开数据手册" : isDe ? "Datenblatt öffnen" : isFr ? "Ouvrir la fiche" : isEs ? "Abrir ficha" : 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">
{isZh ? "在两个系列之间犹豫?" : isDe ? "Unsicher zwischen zwei Serien?" : isFr ? "Vous hésitez entre deux séries ?" : isEs ? "¿Tiene dudas entre dos series?" : isEn ? "Unsure between two series?" : "Ficou em dúvida entre duas séries?"}
</h2>
<p className="mt-3 text-base text-muted">
{isZh ? "对比工具可把最多四个型号并排放在一起:内核、无线、内存、外设和安全。" : isDe
? "Der Vergleicher stellt bis zu vier Modelle gegenüber: Kerne, Funk, Speicher, Peripherie und Sicherheit."
: isFr
? "Le comparateur place jusqu'à quatre modèles côte à côte : cœurs, radio, mémoire, périphériques et sécurité."
: isEs
? "El comparador coloca hasta cuatro modelos lado a lado: núcleos, radio, memoria, periféricos y seguridad."
: 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" />
{isZh ? "对比系列" : isDe ? "Serien vergleichen" : isFr ? "Comparer les séries" : isEs ? "Comparar series" : isEn ? "Compare series" : "Comparar séries"}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</section>
</main>
<Footer />
</div>
);
}