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
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
454 lines
19 KiB
React
454 lines
19 KiB
React
"use client";
|
||
|
||
import { useState, useMemo, useRef } 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 * as glossarioPt from "@/lib/glossarioData";
|
||
import * as glossarioEn from "@/lib/data/en/glossario";
|
||
import * as glossarioEs from "@/lib/data/es/glossario";
|
||
import * as glossarioFr from "@/lib/data/fr/glossario";
|
||
import * as glossarioDe from "@/lib/data/de/glossario";
|
||
import * as glossarioZh from "@/lib/data/zh-CN/glossario";
|
||
import { getLocalizedHref } from "@/i18n/links";
|
||
import {
|
||
ArrowRight,
|
||
BookOpen,
|
||
Check,
|
||
Code2,
|
||
Cpu,
|
||
Database,
|
||
HelpCircle,
|
||
LayoutGrid,
|
||
Lightbulb,
|
||
Link2,
|
||
BatteryCharging,
|
||
RotateCcw,
|
||
Search,
|
||
ShieldCheck,
|
||
Sliders,
|
||
Sparkles,
|
||
Wifi,
|
||
X,
|
||
} from "lucide-react";
|
||
|
||
const BUSCAS_POPULARES = ["GPIO", "ULP", "PSRAM", "Strapping Pins", "Deep Sleep", "OTA", "Matter", "Secure Boot", "Wi-Fi 6", "MIPI"];
|
||
|
||
const ICONES = { Cpu, Database, Wifi, Sliders, Sparkles, BatteryCharging, Code2, ShieldCheck };
|
||
|
||
/** "Processamento, CPU & Silício" vira "Processamento": cabe no filtro sem truncar. */
|
||
const nomeCurto = (nome) => nome.split(/,| &/)[0].trim();
|
||
|
||
export default function GlossarioPage() {
|
||
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 { categoriasGlossario } = isZh ? glossarioZh : isDe ? glossarioDe : isFr ? glossarioFr : isEs ? glossarioEs : isEn ? glossarioEn : glossarioPt;
|
||
|
||
const [busca, setBusca] = useState("");
|
||
const [categoriaSelecionada, setCategoriaSelecionada] = useState("todas");
|
||
const [letraFiltro, setLetraFiltro] = useState(null);
|
||
const [copiadoSlug, setCopiadoSlug] = useState(null);
|
||
const searchInputRef = useRef(null);
|
||
|
||
const todosOsTermos = useMemo(
|
||
() =>
|
||
categoriasGlossario.flatMap((cat) =>
|
||
cat.termos.map((t) => ({
|
||
...t,
|
||
categoriaId: cat.id,
|
||
categoriaNome: cat.nome,
|
||
categoriaIcone: cat.icone,
|
||
slug: t.termo.toLowerCase().replace(/[\s()/²]+/g, "-"),
|
||
primeiraLetra: t.termo.charAt(0).toUpperCase(),
|
||
}))
|
||
),
|
||
[categoriasGlossario]
|
||
);
|
||
|
||
const alfabeto = useMemo(() => {
|
||
const disponiveis = new Set(todosOsTermos.map((t) => t.primeiraLetra));
|
||
return "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((letra) => ({ letra, temTermos: disponiveis.has(letra) }));
|
||
}, [todosOsTermos]);
|
||
|
||
const termosFiltrados = useMemo(() => {
|
||
const termoBusca = busca.toLowerCase().trim();
|
||
return todosOsTermos.filter((item) => {
|
||
const matchCategoria = categoriaSelecionada === "todas" || item.categoriaId === categoriaSelecionada;
|
||
const matchLetra = !letraFiltro || item.primeiraLetra === letraFiltro;
|
||
const matchBusca =
|
||
!termoBusca ||
|
||
item.termo.toLowerCase().includes(termoBusca) ||
|
||
item.definicao.toLowerCase().includes(termoBusca) ||
|
||
item.exemplo.toLowerCase().includes(termoBusca) ||
|
||
item.categoriaNome.toLowerCase().includes(termoBusca);
|
||
return matchCategoria && matchLetra && matchBusca;
|
||
});
|
||
}, [todosOsTermos, categoriaSelecionada, letraFiltro, busca]);
|
||
|
||
const temFiltroAtivo = busca.trim() !== "" || categoriaSelecionada !== "todas" || letraFiltro !== null;
|
||
|
||
const limparTodosFiltros = () => {
|
||
setBusca("");
|
||
setCategoriaSelecionada("todas");
|
||
setLetraFiltro(null);
|
||
searchInputRef.current?.focus();
|
||
};
|
||
|
||
const copiarLinkTermo = (slug) => {
|
||
const basePath = getLocalizedHref("/glossario", currentLocale);
|
||
navigator.clipboard.writeText(`${window.location.origin}${basePath}#${slug}`);
|
||
setCopiadoSlug(slug);
|
||
setTimeout(() => setCopiadoSlug(null), 2000);
|
||
};
|
||
|
||
const total = termosFiltrados.length;
|
||
|
||
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 ? "Glossar" : isFr ? "Glossaire" : isEs ? "Glosario" : isEn ? "Glossary" : "Glossário" }]}
|
||
icon={BookOpen}
|
||
label={
|
||
isZh ? `已解释 ${todosOsTermos.length} 个术语` : isDe
|
||
? `${todosOsTermos.length} Begriffe erklärt`
|
||
: isFr
|
||
? `${todosOsTermos.length} termes expliqués`
|
||
: isEs
|
||
? `${todosOsTermos.length} términos explicados`
|
||
: isEn
|
||
? `${todosOsTermos.length} terms explained`
|
||
: `${todosOsTermos.length} termos explicados`
|
||
}
|
||
title={
|
||
isZh ? "ESP32 技术术语表" : isDe
|
||
? "Technisches Glossar für ESP32"
|
||
: isFr
|
||
? "Glossaire technique ESP32"
|
||
: isEs
|
||
? "Glosario técnico de ESP32"
|
||
: isEn
|
||
? "ESP32 Technical Glossary"
|
||
: "Glossário técnico do ESP32"
|
||
}
|
||
lede={
|
||
isZh ? "为生态中的每个术语提供直接定义、硬件参数和实用示例,从 ADC 到 Secure Boot。" : isDe
|
||
? "Präzise Definitionen, Hardware-Parameter und praktische Beispiele für jeden Begriff des Ökosystems – von ADC bis Secure Boot."
|
||
: isFr
|
||
? "Définitions précises, paramètres matériels et exemples pratiques pour chaque terme de l'écosystème, de l'ADC au Secure Boot."
|
||
: isEs
|
||
? "Definiciones directas, parámetros de hardware y un ejemplo práctico para cada término del ecosistema, desde ADC hasta Secure Boot."
|
||
: isEn
|
||
? "Direct definitions, hardware parameters, and practical examples for every ecosystem term, from ADC to Secure Boot."
|
||
: "Definições diretas, parâmetros de hardware e um exemplo prático para cada termo do ecossistema, de ADC a Secure Boot."
|
||
}
|
||
/>
|
||
|
||
<section className="shell pt-10 sm:pt-12" aria-label={isZh ? "搜索与筛选" : isDe ? "Suche und Filter" : isFr ? "Recherche et filtres" : isEs ? "Búsqueda y filtros" : isEn ? "Search and filters" : "Busca e filtros"}>
|
||
<div className="panel p-5 sm:p-7">
|
||
<div className="relative">
|
||
<label htmlFor="busca-glossario" className="sr-only">
|
||
{isZh ? "搜索术语、缩写或概念" : isDe ? "Nach Begriff, Abkürzung oder Konzept suchen" : isFr ? "Rechercher un terme, un sigle ou un concept" : isEs ? "Buscar término, sigla o concepto" : isEn ? "Search term, acronym, or concept" : "Buscar termo, sigla ou conceito"}
|
||
</label>
|
||
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
|
||
<input
|
||
id="busca-glossario"
|
||
ref={searchInputRef}
|
||
type="search"
|
||
value={busca}
|
||
onChange={(e) => setBusca(e.target.value)}
|
||
placeholder={isZh ? "搜索术语、缩写或概念" : isDe ? "Nach Begriff, Abkürzung oder Konzept suchen" : isFr ? "Rechercher un terme, un sigle ou un concept" : isEs ? "Buscar término, sigla o concepto" : isEn ? "Search term, acronym, or concept" : "Buscar termo, sigla ou conceito"}
|
||
autoComplete="off"
|
||
className={`field pl-12 text-base ${busca ? "pr-28" : "pr-4"}`}
|
||
/>
|
||
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
||
{busca && (
|
||
<button type="button" onClick={() => { setBusca(""); searchInputRef.current?.focus(); }} className="btn btn-ghost btn-sm">
|
||
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
||
{isZh ? "清除" : isDe ? "Löschen" : isFr ? "Effacer" : isEs ? "Limpiar" : isEn ? "Clear" : "Limpar"}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||
<span className="mr-1 text-sm text-muted">{isZh ? "热门搜索" : isDe ? "Häufig gesucht" : isFr ? "Plus recherchés" : isEs ? "Más buscados" : isEn ? "Popular searches" : "Mais buscados"}</span>
|
||
{BUSCAS_POPULARES.map((rotulo) => (
|
||
<button
|
||
key={rotulo}
|
||
type="button"
|
||
onClick={() => { setBusca(rotulo); setCategoriaSelecionada("todas"); setLetraFiltro(null); }}
|
||
className="chip-btn"
|
||
>
|
||
{rotulo}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="mt-5 border-t border-line pt-5">
|
||
<h2 className="text-sm font-semibold text-ink">{isZh ? "类别" : isDe ? "Kategorie" : isFr ? "Catégorie" : isEs ? "Categoría" : isEn ? "Category" : "Categoria"}</h2>
|
||
<div className="mt-3 flex flex-wrap gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setCategoriaSelecionada("todas")}
|
||
aria-pressed={categoriaSelecionada === "todas"}
|
||
className="chip-btn"
|
||
>
|
||
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
|
||
{isZh ? "全部" : isDe ? "Alle" : isFr ? "Toutes" : isEs ? "Todas" : isEn ? "All" : "Todas"}
|
||
<span className="tabular-nums opacity-80">{todosOsTermos.length}</span>
|
||
</button>
|
||
{categoriasGlossario.map((cat) => {
|
||
const Icone = ICONES[cat.icone] ?? Cpu;
|
||
return (
|
||
<button
|
||
key={cat.id}
|
||
type="button"
|
||
onClick={() => setCategoriaSelecionada(cat.id)}
|
||
aria-pressed={categoriaSelecionada === cat.id}
|
||
className="chip-btn"
|
||
>
|
||
<Icone className="h-4 w-4" aria-hidden="true" />
|
||
{nomeCurto(cat.nome)}
|
||
<span className="tabular-nums opacity-80">{cat.termos.length}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* O filtro A–Z não faz sentido em chinês: os termos começam por ideogramas. */}
|
||
{!isZh && (
|
||
<div className="mt-6 border-t border-line pt-6">
|
||
<h2 className="text-sm font-semibold text-ink">{isZh ? "按字母顺序" : isDe ? "Alphabetische Reihenfolge" : isFr ? "Ordre alphabétique" : isEs ? "Orden alfabético" : isEn ? "Alphabetical order" : "Ordem alfabética"}</h2>
|
||
<div className="mt-3 flex flex-wrap gap-1.5" role="group" aria-label={isZh ? "按首字母筛选" : isDe ? "Nach Anfangsbuchstaben filtern" : isFr ? "Filtrer par lettre initiale" : isEs ? "Filtrar por letra inicial" : isEn ? "Filter by first letter" : "Filtrar pela letra inicial"}>
|
||
<button type="button" onClick={() => setLetraFiltro(null)} aria-pressed={letraFiltro === null} className="chip-btn">
|
||
{isZh ? "全部" : isDe ? "Alle" : isFr ? "Toutes" : isEs ? "Todas" : isEn ? "All" : "Todas"}
|
||
</button>
|
||
{alfabeto.map(({ letra, temTermos }) => (
|
||
<button
|
||
key={letra}
|
||
type="button"
|
||
disabled={!temTermos}
|
||
onClick={() => setLetraFiltro(letraFiltro === letra ? null : letra)}
|
||
aria-pressed={letraFiltro === letra}
|
||
className="chip-btn min-w-[2.25rem] justify-center text-xs"
|
||
>
|
||
{letra}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
<section className="shell section pt-8 sm:pt-10" aria-labelledby="resultado-titulo">
|
||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||
<h2 id="resultado-titulo" className="text-base font-semibold text-ink" role="status" aria-live="polite">
|
||
{total}{" "}
|
||
{total === 1
|
||
? isZh ? "个术语" : isDe
|
||
? "Begriff"
|
||
: isFr
|
||
? "terme"
|
||
: isEs
|
||
? "término"
|
||
: isEn
|
||
? "term"
|
||
: "termo"
|
||
: isZh ? "个术语" : isDe
|
||
? "Begriffe"
|
||
: isFr
|
||
? "termes"
|
||
: isEs
|
||
? "términos"
|
||
: isEn
|
||
? "terms"
|
||
: "termos"}
|
||
{temFiltroAtivo && (
|
||
<span className="font-normal text-muted">
|
||
{isZh ? "(当前筛选下)" : isDe
|
||
? " mit den aktuellen Filtern"
|
||
: isFr
|
||
? " avec les filtres actuels"
|
||
: isEs
|
||
? " con los filtros actuales"
|
||
: isEn
|
||
? " with current filters"
|
||
: " com os filtros atuais"}
|
||
</span>
|
||
)}
|
||
</h2>
|
||
{temFiltroAtivo && (
|
||
<button type="button" onClick={limparTodosFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
||
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
||
{isZh ? "重置筛选" : isDe ? "Filter zurücksetzen" : isFr ? "Réinitialiser les filtres" : isEs ? "Restablecer filtros" : isEn ? "Reset filters" : "Redefinir filtros"}
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{total === 0 ? (
|
||
<div className="card mt-6 px-6 py-14 text-center">
|
||
<HelpCircle className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
|
||
<h3 className="mt-4 font-display text-xl font-semibold text-ink">
|
||
{isZh ? "未找到术语" : isDe ? "Keine Begriffe gefunden" : isFr ? "Aucun terme trouvé" : isEs ? "No se encontraron términos" : isEn ? "No terms found" : "Nenhum termo encontrado"}
|
||
</h3>
|
||
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
|
||
{isZh ? "没有任何术语符合当前的搜索和筛选。请尝试更短的缩写,或重置为全部类别。" : isDe
|
||
? "Nichts entspricht Ihrer aktuellen Suche und den Filtern. Versuchen Sie eine kürzere Abkürzung oder wählen Sie alle Kategorien."
|
||
: isFr
|
||
? "Rien ne correspond à votre recherche et à vos filtres actuels. Essayez avec un sigle plus court ou revenez à toutes les catégories."
|
||
: isEs
|
||
? "Nada coincide con la búsqueda y los filtros actuales. Pruebe con una sigla más corta o vuelva a todas las categorías."
|
||
: isEn
|
||
? "Nothing matches your current search and filters. Try a shorter acronym or reset to all categories."
|
||
: "Nada combina com a busca e os filtros atuais. Tente uma sigla mais curta ou volte para todas as categorias."}
|
||
</p>
|
||
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">
|
||
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
||
{isZh ? "清除筛选" : isDe ? "Filter zurücksetzen" : isFr ? "Réinitialiser les filtres" : isEs ? "Limpiar filtros" : isEn ? "Clear filters" : "Limpar filtros"}
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||
{termosFiltrados.map((item) => {
|
||
const Icone = ICONES[item.categoriaIcone] ?? Cpu;
|
||
return (
|
||
<article key={item.slug} id={item.slug} className="card flex scroll-mt-28 flex-col p-5 sm:p-6">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<span className="chip">
|
||
<Icone className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
|
||
{nomeCurto(item.categoriaNome)}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => copiarLinkTermo(item.slug)}
|
||
aria-label={
|
||
isZh ? `复制 ${item.termo} 的链接` : isDe
|
||
? `Link für ${item.termo} kopieren`
|
||
: isFr
|
||
? `Copier le lien pour ${item.termo}`
|
||
: isEs
|
||
? `Copiar enlace para ${item.termo}`
|
||
: isEn
|
||
? `Copy link for ${item.termo}`
|
||
: `Copiar link para ${item.termo}`
|
||
}
|
||
className="btn-icon inline-flex h-11 w-11 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-accent"
|
||
>
|
||
{copiadoSlug === item.slug ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Link2 className="h-4 w-4" aria-hidden="true" />}
|
||
</button>
|
||
</div>
|
||
|
||
<h3 className="mt-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{item.termo}</h3>
|
||
<p className="mt-2 text-[0.9375rem] leading-relaxed text-muted">
|
||
<TextoComLinks texto={item.definicao} currentLocale={currentLocale} />
|
||
</p>
|
||
|
||
<div className="callout mt-5">
|
||
<p className="flex items-center gap-1.5 text-sm font-semibold text-ink">
|
||
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
|
||
{isZh ? "实际应用" : isDe ? "In der Praxis" : isFr ? "En pratique" : isEs ? "En la práctica" : isEn ? "In practice" : "Na prática"}
|
||
</p>
|
||
<p className="mt-1">
|
||
<TextoComLinks texto={item.exemplo} currentLocale={currentLocale} />
|
||
</p>
|
||
</div>
|
||
</article>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<section className="shell pb-20 md:pb-28">
|
||
<div className="panel p-8 text-center sm:p-12">
|
||
<h2 className="section-title mx-auto max-w-2xl">
|
||
{isZh ? "从概念到合适的芯片。" : isDe ? "Vom Konzept zum richtigen Chip." : isFr ? "Du concept à la bonne puce." : isEs ? "Del concepto al chip adecuado." : isEn ? "From concept to the right chip." : "Do conceito ao chip certo."}
|
||
</h2>
|
||
<p className="lede mx-auto mt-4 max-w-2xl">
|
||
{isZh ? "掌握这些术语后,可以并排对比各系列,或回答选型问题,找到适合你项目的型号。" : isDe
|
||
? "Mit den Begriffen zur Hand vergleichen Sie die Serien Seite an Seite oder nutzen Sie die Auswahlhilfe, um das Modell für Ihr Projekt zu finden."
|
||
: isFr
|
||
? "Avec les termes sous la main, comparez les séries côte à côte ou répondez au sélecteur pour trouver le modèle adapté à votre projet."
|
||
: isEs
|
||
? "Con los términos a mano, compare las series lado a lado o responda al selector para encontrar el modelo que se adapta a su proyecto."
|
||
: isEn
|
||
? "With the terms in hand, compare series side by side or answer the selector to find the model that fits your project."
|
||
: "Com os termos em mãos, compare as séries lado a lado ou responda ao seletor para chegar ao modelo que cabe no seu projeto."}
|
||
</p>
|
||
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||
<Link href={getLocalizedHref("/seletor", currentLocale)} className="btn btn-primary w-full sm:w-auto">
|
||
<Sliders className="h-4 w-4" aria-hidden="true" />
|
||
{isZh ? "选择我的芯片" : isDe ? "Meinen Chip wählen" : isFr ? "Choisir ma puce" : isEs ? "Elegir mi chip" : isEn ? "Choose my chip" : "Escolher meu chip"}
|
||
</Link>
|
||
<Link href={getLocalizedHref("/series", currentLocale)} className="btn btn-ghost w-full sm:w-auto">
|
||
{isZh ? "浏览系列" : isDe ? "Serien erkunden" : isFr ? "Explorer les séries" : isEs ? "Explorar las series" : isEn ? "Explore series" : "Explorar as séries"}
|
||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<Footer />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Função utilitária que detecta nomes de séries do ESP32 dentro do texto e os converte em links clicáveis.
|
||
*/
|
||
function TextoComLinks({ texto, currentLocale = "pt" }) {
|
||
if (!texto) return null;
|
||
const seriesKeys = [
|
||
"ESP32-S31",
|
||
"ESP32-C61",
|
||
"ESP32-S2",
|
||
"ESP32-S3",
|
||
"ESP32-C2",
|
||
"ESP32-C3",
|
||
"ESP32-C5",
|
||
"ESP32-C6",
|
||
"ESP32-P4",
|
||
"ESP32-H2",
|
||
"ESP32-H4",
|
||
"ESP32",
|
||
];
|
||
const regex = new RegExp(`\\b(${seriesKeys.join("|")})\\b`, "g");
|
||
const partes = texto.split(regex);
|
||
|
||
return (
|
||
<>
|
||
{partes.map((parte, i) => {
|
||
if (seriesKeys.includes(parte)) {
|
||
return (
|
||
<Link
|
||
key={i}
|
||
href={getLocalizedHref(`/series/${parte}`, currentLocale)}
|
||
className="font-semibold text-accent underline decoration-accent/30 underline-offset-2 hover:decoration-accent"
|
||
>
|
||
{parte}
|
||
</Link>
|
||
);
|
||
}
|
||
return parte;
|
||
})}
|
||
</>
|
||
);
|
||
}
|