Files
ESPDocs/app/[locale]/series/page.jsx
T
Lucas RayanandAntigravity 83c0ba3e74 feat(i18n): routing and localization infrastructure (phase 1)
Implement Phase 1 of i18n spec:
- Configure next-intl with 6 locales (pt, en, es, fr, de, zh-CN) and pathnames
- Move routes to app/[locale] structure with localized layouts and metadata
- Add middleware for root language negotiation (307), legacy redirects (308), and 404 for non-pt book
- Implement language selector dropdown in header with cookie persistence
- Add system typography rules for zh-CN and word-break/hyphenation for de/fr

Co-Authored-By: Antigravity <noreply@google.com>
2026-11-01 09:31:22 -03:00

155 lines
5.8 KiB
React

"use client";
import { useMemo, useState } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import Link from "next/link";
import seriesData from "@/public/series.json";
import {
AlertTriangle,
ArrowRight,
ArrowUpRight,
Compass,
Home,
Scale,
ShieldCheck,
Sparkles,
} from "lucide-react";
const FILTROS = [
{ 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) },
];
/** Marcador de segurança: o recurso que diferencia a série, não o que todas têm. */
function nivelSeguranca(serie) {
const seg = serie.seguranca || {};
if (seg.key_manager === "Sim") return "Key Manager";
if (String(seg.ecdsa).startsWith("Sim")) return "ECDSA em hardware";
if (String(seg.ecc).startsWith("Sim")) return "ECC em hardware";
if (String(seg.aes).startsWith("AES")) return "AES + SHA";
return "Só SHA";
}
const sim = (v) => !String(v).includes("Não");
export default function Series() {
const [filtro, setFiltro] = useState("todas");
const todas = Object.entries(seriesData);
const ativo = FILTROS.find((f) => f.id === filtro);
const series = useMemo(() => todas.filter(([, s]) => ativo.testa(s)), [todas, ativo]);
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
<PageHero
trail={[{ label: "Séries" }]}
icon={Compass}
label={`${todas.length} famílias da Espressif`}
title="Séries ESP32"
lede="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="Lista de séries">
<div role="group" aria-label="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 ? "série" : "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 = [
["Núcleos", serie.nucleos],
["Frequência", serie.frequencia],
["SRAM", serie.memoria_sram],
["Wi-Fi", sim(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={`/series/${key}`} 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.035em] 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" ? "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" />
Aceleradores de IA
</li>
)}
<li className="chip">
<ShieldCheck className="h-3 w-3 text-accent" aria-hidden="true" />
{nivelSeguranca(serie)}
</li>
{serie.matter === "Sim" && (
<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" />
Documentação preliminar
</li>
)}
</ul>
<span className="mt-5 inline-flex items-center gap-1 text-sm font-medium text-accent">
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">Ficou em dúvida entre duas séries?</h2>
<p className="mt-3 text-base text-muted">
O comparador coloca até quatro modelos lado a lado: núcleos, rádio, memória, periféricos e segurança.
</p>
</div>
<Link href="/comparacao" className="btn btn-primary shrink-0">
<Scale className="h-4 w-4" aria-hidden="true" />
Comparar séries
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</section>
</main>
<Footer />
</div>
);
}