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>
This commit is contained in:
1 parent
e92aea0c84
commit
83c0ba3e74
38 files changed
+1699
-324
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import { paginaMeta, jsonLd } from "@/lib/seo";
|
||||
|
||||
export const metadata = paginaMeta({
|
||||
titulo: "Catálogo de placas ESP32",
|
||||
descricao: "Placas de desenvolvimento com ESP32 disponíveis no Brasil, com filtro por série, conector e conversor USB-serial.",
|
||||
caminho: "/catalogo",
|
||||
keywords: ["placas ESP32", "DevKit ESP32", "comprar ESP32", "ESP32 DevKitC"],
|
||||
});
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.aplicacaoWeb({
|
||||
nome: "Catálogo de Placas ESP32",
|
||||
descricao: "Guia e catálogo interativo de placas de desenvolvimento e DevKits com microcontroladores ESP32.",
|
||||
caminho: "/catalogo",
|
||||
})
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Catálogo de Placas", caminho: "/catalogo" },
|
||||
])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import BoardCard from "@/components/BoardCard";
|
||||
import { AlertTriangle, RefreshCw, Search, ShoppingCart, SlidersHorizontal } from "lucide-react";
|
||||
|
||||
export default function PlacasPage() {
|
||||
const [boards, setBoards] = useState([]);
|
||||
const [filteredBoards, setFilteredBoards] = useState([]);
|
||||
const [headers, setHeaders] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [activeFilters, setActiveFilters] = useState({});
|
||||
const [filterOptions, setFilterOptions] = useState({});
|
||||
const [filtrosAbertos, setFiltrosAbertos] = useState(false);
|
||||
|
||||
const excludedColumns = ['Nome', 'Descrição', 'Link', 'Imagem', 'Loja Oficial'];
|
||||
|
||||
useEffect(() => {
|
||||
fetchBoards();
|
||||
// No desktop os filtros ficam sempre à vista; no celular começam recolhidos.
|
||||
setFiltrosAbertos(window.matchMedia('(min-width: 1024px)').matches);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
applyFilters();
|
||||
}, [boards, searchTerm, activeFilters]);
|
||||
|
||||
const fetchBoards = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/placas');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erro ao carregar dados das placas');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setBoards(result.data);
|
||||
setHeaders(result.headers);
|
||||
setFilteredBoards(result.data);
|
||||
|
||||
generateFilterOptions(result.data, result.headers);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateFilterOptions = (data, headers) => {
|
||||
const options = {};
|
||||
const filterableColumns = headers.filter(h => !excludedColumns.includes(h));
|
||||
|
||||
filterableColumns.forEach(column => {
|
||||
const uniqueValues = new Set();
|
||||
data.forEach(item => {
|
||||
const value = item[column];
|
||||
if (value && value.toString().trim()) {
|
||||
uniqueValues.add(value.toString().trim());
|
||||
}
|
||||
});
|
||||
|
||||
const values = Array.from(uniqueValues).sort();
|
||||
|
||||
// Verificar se é um filtro binário (apenas Sim/Não/Opcional)
|
||||
const binaryValues = ['Sim', 'Não', 'Opcional'];
|
||||
const isBinaryFilter = values.every(v => binaryValues.includes(v));
|
||||
|
||||
options[column] = {
|
||||
values: values,
|
||||
isBinary: isBinaryFilter
|
||||
};
|
||||
});
|
||||
|
||||
setFilterOptions(options);
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
let filtered = [...boards];
|
||||
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(board => {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
return (
|
||||
board.Nome?.toLowerCase().includes(searchLower) ||
|
||||
board.Descrição?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(activeFilters).forEach(column => {
|
||||
if (activeFilters[column] && activeFilters[column].length > 0) {
|
||||
filtered = filtered.filter(board => {
|
||||
const boardValue = board[column]?.toString().trim();
|
||||
|
||||
// Para filtros binários, o valor armazenado é 'binary-enabled'
|
||||
if (activeFilters[column].includes('binary-enabled')) {
|
||||
return boardValue === 'Sim' || boardValue === 'Opcional';
|
||||
}
|
||||
|
||||
return activeFilters[column].includes(boardValue);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setFilteredBoards(filtered);
|
||||
};
|
||||
|
||||
const handleFilterToggle = (column, value) => {
|
||||
setActiveFilters(prev => {
|
||||
const newFilters = { ...prev };
|
||||
|
||||
if (!newFilters[column]) {
|
||||
newFilters[column] = [value];
|
||||
} else {
|
||||
const index = newFilters[column].indexOf(value);
|
||||
if (index > -1) {
|
||||
// Remove o valor
|
||||
newFilters[column] = newFilters[column].filter(v => v !== value);
|
||||
if (newFilters[column].length === 0) {
|
||||
delete newFilters[column];
|
||||
}
|
||||
} else {
|
||||
// Adiciona o valor
|
||||
newFilters[column] = [...newFilters[column], value];
|
||||
}
|
||||
}
|
||||
|
||||
return newFilters;
|
||||
});
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setActiveFilters({});
|
||||
setSearchTerm('');
|
||||
};
|
||||
|
||||
const activeFilterCount = Object.keys(activeFilters).length + (searchTerm ? 1 : 0);
|
||||
|
||||
const filterableColumns = headers.filter((h) => !excludedColumns.includes(h));
|
||||
|
||||
const hero = (
|
||||
<PageHero
|
||||
trail={[{ label: "Catálogo de placas" }]}
|
||||
icon={ShoppingCart}
|
||||
label="DevKits e módulos reais"
|
||||
title="Catálogo de placas ESP32"
|
||||
lede="Compare placas oficiais e alternativas para o seu projeto: interface USB, sensores integrados e link direto para a loja."
|
||||
/>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
<main id="conteudo">
|
||||
{hero}
|
||||
<section className="shell section pt-10" aria-busy="true" aria-label="Carregando placas">
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="card h-72 animate-pulse bg-surface-2" />
|
||||
))}
|
||||
</div>
|
||||
<p role="status" className="mt-6 text-center text-sm text-muted">Consultando o banco de placas…</p>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
<main id="conteudo">
|
||||
{hero}
|
||||
<section className="shell section pt-10">
|
||||
<div role="alert" className="callout callout-danger flex items-start gap-3">
|
||||
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-danger" aria-hidden="true" />
|
||||
<div>
|
||||
<p className="font-semibold text-ink">Não foi possível carregar as placas</p>
|
||||
<p className="mt-1">{error}. Verifique a conexão e tente de novo.</p>
|
||||
<button type="button" onClick={fetchBoards} className="btn btn-primary btn-sm mt-4">
|
||||
<RefreshCw className="h-4 w-4" aria-hidden="true" />
|
||||
Tentar novamente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const filtros = (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="busca-placa" className="text-sm font-semibold text-ink">
|
||||
Buscar placa
|
||||
</label>
|
||||
<div className="relative mt-2">
|
||||
<Search className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted" aria-hidden="true" />
|
||||
<input
|
||||
id="busca-placa"
|
||||
type="search"
|
||||
placeholder="Nome ou descrição"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="field pl-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filterableColumns.map((column) => {
|
||||
const filterData = filterOptions[column];
|
||||
if (!filterData) return null;
|
||||
|
||||
if (filterData.isBinary) {
|
||||
return (
|
||||
<label key={column} className="flex min-h-11 cursor-pointer items-center justify-between gap-3 border-t border-line pt-4 text-sm font-medium text-ink-soft">
|
||||
{column}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={activeFilters[column]?.includes("binary-enabled") || false}
|
||||
onChange={() => handleFilterToggle(column, "binary-enabled")}
|
||||
className="h-5 w-5 shrink-0 cursor-pointer accent-[var(--accent)]"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset key={column} className="border-t border-line pt-4">
|
||||
<legend className="flex items-center gap-2 pr-2 text-sm font-semibold text-ink">
|
||||
{column}
|
||||
{activeFilters[column]?.length > 0 && <span className="chip chip-accent px-2 py-0">{activeFilters[column].length}</span>}
|
||||
</legend>
|
||||
<div className="mt-2 max-h-44 space-y-0.5 overflow-y-auto pr-1">
|
||||
{filterData.values.map((option) => (
|
||||
<label key={option} className="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-2 text-sm text-muted transition-colors hover:bg-surface-2 hover:text-ink">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={activeFilters[column]?.includes(option) || false}
|
||||
onChange={() => handleFilterToggle(column, option)}
|
||||
className="h-4 w-4 shrink-0 cursor-pointer accent-[var(--accent)]"
|
||||
/>
|
||||
{option}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
{hero}
|
||||
|
||||
<section className="shell section pt-10 sm:pt-12" aria-label="Placas">
|
||||
<div className="grid gap-8 lg:grid-cols-[17rem_1fr] lg:gap-10">
|
||||
<aside className="lg:sticky lg:top-28 lg:self-start">
|
||||
<details className="panel" open={filtrosAbertos} onToggle={(e) => setFiltrosAbertos(e.currentTarget.open)}>
|
||||
<summary className="flex min-h-12 cursor-pointer list-none items-center justify-between gap-3 px-5 py-3 lg:cursor-default">
|
||||
<span className="flex items-center gap-2 text-sm font-semibold text-ink">
|
||||
<SlidersHorizontal className="h-4 w-4 text-accent" aria-hidden="true" />
|
||||
Filtros
|
||||
{activeFilterCount > 0 && <span className="chip chip-accent px-2 py-0">{activeFilterCount}</span>}
|
||||
</span>
|
||||
{activeFilterCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.preventDefault(); clearFilters(); }}
|
||||
className="text-sm font-medium text-accent hover:text-accent-strong"
|
||||
>
|
||||
Limpar
|
||||
</button>
|
||||
)}
|
||||
</summary>
|
||||
<div className="border-t border-line p-5 lg:max-h-[calc(100vh-11rem)] lg:overflow-y-auto">{filtros}</div>
|
||||
</details>
|
||||
</aside>
|
||||
|
||||
<div>
|
||||
<p role="status" aria-live="polite" className="text-sm text-muted">
|
||||
<strong className="font-semibold text-ink">{filteredBoards.length}</strong>{" "}
|
||||
{filteredBoards.length === 1 ? "placa encontrada" : "placas encontradas"}
|
||||
</p>
|
||||
|
||||
{filteredBoards.length === 0 ? (
|
||||
<div className="card mt-4 px-6 py-14 text-center">
|
||||
<Search className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
|
||||
<h2 className="mt-4 font-display text-xl font-semibold text-ink">Nenhuma placa com esses filtros</h2>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Remova alguns filtros ou busque por outro nome.</p>
|
||||
<button type="button" onClick={clearFilters} className="btn btn-primary mt-6">
|
||||
Limpar filtros
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredBoards.map((board, index) => (
|
||||
<BoardCard key={index} board={board} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { paginaMeta, jsonLd } from "@/lib/seo";
|
||||
|
||||
export const metadata = paginaMeta({
|
||||
titulo: "Comparar séries ESP32",
|
||||
descricao: "Compare até quatro séries de ESP32 lado a lado: processador, Wi-Fi, Bluetooth, memória, GPIO, periféricos e segurança em hardware, com os dados dos datasheets oficiais.",
|
||||
caminho: "/comparacao",
|
||||
keywords: ["comparar ESP32", "ESP32 vs ESP32-S3", "tabela comparativa ESP32", "qual ESP32 é melhor"],
|
||||
});
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
pergunta: "Como comparar microcontroladores ESP32 lado a lado?",
|
||||
resposta: "O comparador do ESPDocs permite selecionar até quatro séries simultaneamente para confrontar arquitetura, frequência de clock, memória SRAM/PSRAM, padrões de rádio (Wi-Fi 4/6, Bluetooth 5/6, 802.15.4), interfaces de periféricos e blocos de segurança em hardware.",
|
||||
},
|
||||
{
|
||||
pergunta: "Qual a diferença entre o ESP32 clássico e o ESP32-S3?",
|
||||
resposta: "O ESP32 clássico usa núcleos Xtensa LX6 e Bluetooth clássico/LE. O ESP32-S3 traz núcleos Xtensa LX7 com instruções vetoriais para inteligência artificial (TinyML), USB nativo OTG, mais GPIOs (45 contra 36) e Bluetooth 5.0 LE.",
|
||||
},
|
||||
{
|
||||
pergunta: "Qual a diferença entre ESP32-C3 e ESP32-C6?",
|
||||
resposta: "O ESP32-C3 é um microcontrolador RISC-V com Wi-Fi 4 e Bluetooth 5 LE. O ESP32-C6 evolui para Wi-Fi 6 (802.11ax), Bluetooth 5.4 LE, rádio IEEE 802.15.4 para Zigbee 3.0 e Thread 1.3, além de coprocessador de ultrabaixo consumo.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.aplicacaoWeb({
|
||||
nome: "Comparador de Séries ESP32",
|
||||
descricao: "Tabela comparativa técnica para analisar especificações, pinagens e recursos de múltiplas séries de ESP32.",
|
||||
caminho: "/comparacao",
|
||||
})
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Comparar Séries", caminho: "/comparacao" },
|
||||
])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(jsonLd.perguntas(faqs)),
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
"use client";
|
||||
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import { useEffect, useState, Fragment } from "react";
|
||||
import Link from "next/link";
|
||||
import seriesData from "@/public/series.json";
|
||||
import { ArrowRight, ArrowUpRight, Check, ChevronDown, Scale } from "lucide-react";
|
||||
|
||||
const MIN_SERIES = 2;
|
||||
const MAX_SERIES = 4;
|
||||
|
||||
/* Lê "seguranca.ecc" além de chaves simples. */
|
||||
function valorDe(objeto, caminho) {
|
||||
return caminho.split(".").reduce((atual, parte) => atual?.[parte], objeto);
|
||||
}
|
||||
|
||||
export default function Comparacao() {
|
||||
const [selectedSeries, setSelectedSeries] = useState(["ESP32", "ESP32-S3"]);
|
||||
const [soDiferencas, setSoDiferencas] = useState(false);
|
||||
const [collapsedCategories, setCollapsedCategories] = useState({
|
||||
"Interfaces Especiais": true,
|
||||
"Especificações Ambientais": true,
|
||||
});
|
||||
|
||||
const series = Object.entries(seriesData);
|
||||
|
||||
// O seletor envia as séries finalistas em ?chips=ESP32-S3,ESP32-C6.
|
||||
useEffect(() => {
|
||||
const chips = new URLSearchParams(window.location.search).get("chips");
|
||||
if (!chips) return;
|
||||
const validas = [...new Set(chips.split(","))].filter((k) => seriesData[k]).slice(0, MAX_SERIES);
|
||||
if (validas.length >= MIN_SERIES) setSelectedSeries(validas);
|
||||
}, []);
|
||||
|
||||
const toggleSeries = (seriesKey) => {
|
||||
if (selectedSeries.includes(seriesKey)) {
|
||||
if (selectedSeries.length > MIN_SERIES) setSelectedSeries(selectedSeries.filter((s) => s !== seriesKey));
|
||||
} else if (selectedSeries.length < MAX_SERIES) {
|
||||
setSelectedSeries([...selectedSeries, seriesKey]);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleCategory = (category) => {
|
||||
setCollapsedCategories({ ...collapsedCategories, [category]: !collapsedCategories[category] });
|
||||
};
|
||||
|
||||
const compareFields = [
|
||||
{ category: "Geral", fields: [
|
||||
{ key: "nome_completo", label: "Nome Completo" },
|
||||
{ key: "descricao", label: "Descrição" },
|
||||
]},
|
||||
{ category: "Disponibilidade e Ferramentas", fields: [
|
||||
{ key: "esp_idf_minimo", label: "ESP-IDF mínimo" },
|
||||
{ key: "arduino_core", label: "Core Arduino" },
|
||||
{ key: "depuracao", label: "Depuração / JTAG" },
|
||||
{ key: "encapsulamento", label: "Encapsulamento" },
|
||||
{ key: "modulos", label: "Módulos disponíveis" },
|
||||
]},
|
||||
{ category: "Processador", fields: [
|
||||
{ key: "arquitetura", label: "Arquitetura" },
|
||||
{ key: "nucleos", label: "Núcleos" },
|
||||
{ key: "frequencia", label: "Frequência" },
|
||||
{ key: "coprocessador_ulp", label: "Coprocessador ULP" },
|
||||
{ key: "aceleradores_ia", label: "Aceleradores de IA" },
|
||||
]},
|
||||
{ category: "Conectividade", fields: [
|
||||
{ key: "wifi", label: "Wi-Fi" },
|
||||
{ key: "bluetooth", label: "Bluetooth" },
|
||||
{ key: "zigbee_thread", label: "Zigbee/Thread" },
|
||||
{ key: "matter", label: "Matter" },
|
||||
{ key: "ethernet", label: "Ethernet" },
|
||||
]},
|
||||
{ category: "Memória", fields: [
|
||||
{ key: "memoria_sram", label: "SRAM" },
|
||||
{ key: "memoria_sram_rtc", label: "SRAM RTC" },
|
||||
{ key: "memoria_rom", label: "ROM" },
|
||||
{ key: "flash_externa", label: "Flash Externa" },
|
||||
{ key: "psram_externa", label: "PSRAM Externa" },
|
||||
]},
|
||||
{ category: "Periféricos Digitais", fields: [
|
||||
{ key: "gpio", label: "GPIO" },
|
||||
{ key: "uart", label: "UART" },
|
||||
{ key: "spi", label: "SPI" },
|
||||
{ key: "i2c", label: "I²C" },
|
||||
{ key: "i2s", label: "I²S" },
|
||||
{ key: "pwm", label: "PWM" },
|
||||
{ key: "can", label: "CAN" },
|
||||
{ key: "usb", label: "USB" },
|
||||
{ key: "depuracao", label: "Depuração / JTAG" },
|
||||
{ key: "sdio", label: "SDIO" },
|
||||
]},
|
||||
{ category: "Periféricos Analógicos", fields: [
|
||||
{ key: "adc", label: "ADC" },
|
||||
{ key: "dac", label: "DAC" },
|
||||
{ key: "touch", label: "Touch Capacitivo" },
|
||||
{ key: "sensor_hall", label: "Sensor Hall" },
|
||||
{ key: "sensor_temperatura", label: "Sensor de Temperatura" },
|
||||
]},
|
||||
{ category: "Segurança", fields: [
|
||||
{ key: "seguranca.aes", label: "AES" },
|
||||
{ key: "seguranca.sha", label: "SHA" },
|
||||
{ key: "seguranca.rsa", label: "RSA / MPI" },
|
||||
{ key: "seguranca.ecc", label: "ECC (curvas elípticas)" },
|
||||
{ key: "seguranca.ecdsa", label: "ECDSA em hardware" },
|
||||
{ key: "seguranca.hmac", label: "HMAC" },
|
||||
{ key: "seguranca.assinatura_digital", label: "Assinatura Digital (DS)" },
|
||||
{ key: "seguranca.key_manager", label: "Key Manager" },
|
||||
{ key: "seguranca.rng", label: "Gerador aleatório (RNG)" },
|
||||
{ key: "seguranca.criptografia_flash", label: "Criptografia de flash" },
|
||||
{ key: "seguranca.secure_boot", label: "Secure Boot" },
|
||||
{ key: "seguranca.protecao_dpa", label: "Proteção contra DPA" },
|
||||
]},
|
||||
{ category: "Interfaces Especiais", fields: [
|
||||
{ key: "lcd", label: "LCD" },
|
||||
{ key: "camera", label: "Câmera" },
|
||||
{ key: "mipi_csi", label: "MIPI CSI" },
|
||||
{ key: "mipi_dsi", label: "MIPI DSI" },
|
||||
{ key: "ethernet_mac", label: "Ethernet MAC" },
|
||||
]},
|
||||
{ category: "Especificações Ambientais", fields: [
|
||||
{ key: "temperatura_operacao", label: "Temp. Operação" },
|
||||
{ key: "temperatura_armazenamento", label: "Temp. Armazenamento" },
|
||||
]},
|
||||
];
|
||||
|
||||
const renderValue = (value) => {
|
||||
if (!value || value === "Não") return <span className="text-muted">Não</span>;
|
||||
if (value === "Sim") {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 font-medium text-ok">
|
||||
<Check className="h-4 w-4" aria-hidden="true" />
|
||||
Sim
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span className="font-medium text-ink">{value}</span>;
|
||||
};
|
||||
|
||||
const valores = (key) => selectedSeries.map((k) => String(valorDe(seriesData[k], key) ?? "Não"));
|
||||
const diferem = (key) => new Set(valores(key)).size > 1;
|
||||
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
<PageHero
|
||||
trail={[{ label: "Comparador" }]}
|
||||
icon={Scale}
|
||||
label="Até quatro séries lado a lado"
|
||||
title="Comparador de séries"
|
||||
lede="Coloque núcleos, rádio, memória, periféricos e segurança na mesma tabela e veja o que realmente muda de um chip para outro."
|
||||
/>
|
||||
|
||||
<section className="shell pt-10 sm:pt-12" aria-labelledby="escolha-titulo">
|
||||
<div className="panel p-5 sm:p-7">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h2 id="escolha-titulo" className="text-base font-semibold text-ink">Escolha de {MIN_SERIES} a {MAX_SERIES} séries</h2>
|
||||
<p role="status" aria-live="polite" className="mt-1 text-sm text-muted">
|
||||
{selectedSeries.length} de {MAX_SERIES} selecionadas
|
||||
{selectedSeries.length === MAX_SERIES && ". Remova uma para trocar."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{series.map(([key]) => {
|
||||
const marcada = selectedSeries.includes(key);
|
||||
const bloqueada = !marcada && selectedSeries.length >= MAX_SERIES;
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => toggleSeries(key)}
|
||||
aria-pressed={marcada}
|
||||
aria-disabled={bloqueada}
|
||||
className={`chip-btn ${bloqueada ? "opacity-50" : ""}`}
|
||||
>
|
||||
{marcada && <Check className="h-4 w-4" aria-hidden="true" />}
|
||||
{key}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell mt-8" aria-labelledby="tabela-titulo">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 id="tabela-titulo" className="text-base font-semibold text-ink">Especificações</h2>
|
||||
<label className="flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-medium text-ink-soft">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={soDiferencas}
|
||||
onChange={(e) => setSoDiferencas(e.target.checked)}
|
||||
className="h-5 w-5 cursor-pointer accent-[var(--accent)]"
|
||||
/>
|
||||
Mostrar só as diferenças
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="panel mt-3 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[30rem] border-collapse text-sm">
|
||||
<caption className="sr-only">Comparação das séries {selectedSeries.join(", ")}</caption>
|
||||
<thead>
|
||||
<tr className="border-b border-line bg-surface-2">
|
||||
<th scope="col" className="sticky left-0 z-20 w-28 bg-surface-2 px-4 py-4 text-left font-semibold text-muted sm:w-52 sm:px-6">
|
||||
Especificação
|
||||
</th>
|
||||
{selectedSeries.map((seriesKey) => (
|
||||
<th key={seriesKey} scope="col" className="min-w-40 px-4 py-4 text-left sm:px-6">
|
||||
<Link href={`/series/${seriesKey}`} className="inline-flex min-h-11 items-center gap-1 font-display text-lg font-semibold tracking-[-0.03em] text-ink hover:text-accent">
|
||||
{seriesKey}
|
||||
<ArrowUpRight className="h-4 w-4 text-muted" aria-hidden="true" />
|
||||
</Link>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{compareFields.map(({ category, fields }) => {
|
||||
const visiveis = soDiferencas ? fields.filter(({ key }) => diferem(key)) : fields;
|
||||
if (soDiferencas && visiveis.length === 0) return null;
|
||||
const isCollapsed = collapsedCategories[category] && !soDiferencas;
|
||||
return (
|
||||
<Fragment key={category}>
|
||||
<tr className="border-y border-line bg-surface-2/60">
|
||||
<th scope="rowgroup" colSpan={selectedSeries.length + 1} className="p-0 text-left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCategory(category)}
|
||||
aria-expanded={!isCollapsed}
|
||||
className="flex min-h-12 w-full cursor-pointer items-center justify-between gap-3 px-4 py-3 text-left font-semibold text-ink sm:px-6"
|
||||
>
|
||||
<span className="sticky left-4 sm:left-6">{category}</span>
|
||||
<ChevronDown className={`h-4 w-4 shrink-0 text-muted transition-transform ${isCollapsed ? "" : "rotate-180"}`} aria-hidden="true" />
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
{!isCollapsed &&
|
||||
visiveis.map(({ key, label }) => (
|
||||
<tr key={key} className="border-b border-line last:border-b-0 hover:bg-surface-2/50">
|
||||
<th scope="row" className="sticky left-0 z-10 bg-surface px-4 py-3.5 text-left align-top font-medium text-muted sm:px-6">
|
||||
{label}
|
||||
</th>
|
||||
{selectedSeries.map((seriesKey) => (
|
||||
<td key={seriesKey} className="px-4 py-3.5 align-top sm:px-6">
|
||||
{renderValue(valorDe(seriesData[seriesKey], key))}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell section" aria-labelledby="docs-titulo">
|
||||
<h2 id="docs-titulo" className="section-title">Documentação oficial</h2>
|
||||
<div className="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{selectedSeries.map((seriesKey) => {
|
||||
const seriesItem = seriesData[seriesKey];
|
||||
return (
|
||||
<div key={seriesKey} className="card flex flex-col justify-between gap-5 p-5">
|
||||
<Link href={`/series/${seriesKey}`} className="inline-flex min-h-11 items-center font-display text-lg font-semibold tracking-[-0.03em] text-ink hover:text-accent">
|
||||
{seriesKey}
|
||||
</Link>
|
||||
{seriesItem.datasheet ? (
|
||||
<a href={seriesItem.datasheet} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
|
||||
Datasheet oficial
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only"> (abre em nova aba)</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm text-muted">Datasheet ainda não publicado</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="panel mt-10 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">Quer testar na bancada?</h2>
|
||||
<p className="mt-3 text-base text-muted">Veja as placas de desenvolvimento com pinagem, periféricos e link para a loja.</p>
|
||||
</div>
|
||||
<Link href="/catalogo" className="btn btn-primary shrink-0">
|
||||
Ver catálogo de placas
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { paginaMeta, jsonLd } from "@/lib/seo";
|
||||
|
||||
export const metadata = paginaMeta({
|
||||
titulo: "ESP Component Registry & IDF Component Manager · ESPDocs",
|
||||
descricao: "Catálogo oficial e interativo do ESP Component Registry: drivers de displays, LVGL, reconhecimento de voz ESP-SR, RainMaker, BSPs e gerador de idf_component.yml.",
|
||||
caminho: "/componentes",
|
||||
keywords: [
|
||||
"ESP Component Registry",
|
||||
"IDF Component Manager",
|
||||
"idf_component.yml",
|
||||
"idf.py add-dependency",
|
||||
"componentes ESP32",
|
||||
"espressif esp_lvgl_port",
|
||||
"espressif esp_rainmaker",
|
||||
"espressif esp-sr",
|
||||
"bibliotecas ESP-IDF",
|
||||
],
|
||||
});
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
pergunta: "O que é o ESP Component Registry e o IDF Component Manager?",
|
||||
resposta: "O ESP Component Registry (components.espressif.com) é o repositório oficial de pacotes modulares da Espressif. O IDF Component Manager é a ferramenta integrada ao ESP-IDF (a partir do v4.4 e padrão no v5.x+) que baixa, compila e resolve versões de dependências automaticamente a partir do manifesto idf_component.yml.",
|
||||
},
|
||||
{
|
||||
pergunta: "Como adicionar um componente ao meu projeto ESP-IDF?",
|
||||
resposta: "Você pode executar o comando 'idf.py add-dependency \"namespace/componente^versao\"' no terminal do projeto (por exemplo: idf.py add-dependency \"espressif/esp_lvgl_port^2.4.0\") ou editar diretamente o arquivo main/idf_component.yml.",
|
||||
},
|
||||
{
|
||||
pergunta: "Qual a diferença entre usar o Component Registry e git submodules?",
|
||||
resposta: "O Component Registry elimina a complexidade de gerenciar submódulos Git pesados e versões manuais. Ele oferece versionamento semântico (^ e ~), resolução automática de dependências transitivas e compilação modular sob demanda.",
|
||||
},
|
||||
{
|
||||
pergunta: "Posso criar e publicar meus próprios componentes?",
|
||||
resposta: "Sim, qualquer desenvolvedor ou empresa pode publicar componentes públicos ou privados no ESP Component Registry utilizando a ferramenta oficial Compote CLI (pip install idf-component-manager).",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ComponentesLayout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "ESP Component Registry", caminho: "/componentes" },
|
||||
])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(jsonLd.perguntas(faqs)),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.aplicacaoWeb({
|
||||
nome: "Hub do ESP Component Registry & Construtor de Manifesto",
|
||||
descricao: "Catálogo curado e gerador de manifesto idf_component.yml para componentes modulares do ESP-IDF.",
|
||||
caminho: "/componentes",
|
||||
})
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import SectionHead from "@/components/SectionHead";
|
||||
import {
|
||||
CATEGORIAS_COMPONENTES,
|
||||
COMPONENTES_REGISTRY,
|
||||
GUIA_COMPONENT_MANAGER
|
||||
} from "@/lib/componentRegistryData";
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Box,
|
||||
Brain,
|
||||
Check,
|
||||
CircuitBoard,
|
||||
Cloud,
|
||||
Copy,
|
||||
Cpu,
|
||||
Download,
|
||||
HelpCircle,
|
||||
Home,
|
||||
Layers,
|
||||
Monitor,
|
||||
Plus,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
Wrench,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
const ICONES_CATEGORIA = {
|
||||
ui_display: Monitor,
|
||||
cloud_conectividade: Cloud,
|
||||
audio_voz_ia: Brain,
|
||||
smart_home_mesh: Home,
|
||||
drivers_perifericos: Cpu,
|
||||
bsp_placas: CircuitBoard,
|
||||
sistema_utilitarios: Wrench,
|
||||
};
|
||||
|
||||
export default function ComponentRegistryPage() {
|
||||
const [busca, setBusca] = useState("");
|
||||
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
|
||||
const [chipFiltro, setChipFiltro] = useState("todos");
|
||||
const [copiadoId, setCopiadoId] = useState(null);
|
||||
const [manifestoCopiado, setManifestoCopiado] = useState(false);
|
||||
|
||||
// Componentes selecionados no construtor de idf_component.yml
|
||||
const [componentesSelecionados, setComponentesSelecionados] = useState([
|
||||
"espressif-esp_lvgl_port",
|
||||
"espressif-led_indicator"
|
||||
]);
|
||||
|
||||
// Lista de chips únicos para filtro
|
||||
const chipsDisponiveis = ["todos", "ESP32", "ESP32-S3", "ESP32-C6", "ESP32-P4", "ESP32-H2", "ESP32-C3"];
|
||||
|
||||
// Filtragem dos componentes
|
||||
const componentesFiltrados = useMemo(() => {
|
||||
return COMPONENTES_REGISTRY.filter((comp) => {
|
||||
const matchCategoria = categoriaAtiva === "todos" || comp.categoria === categoriaAtiva;
|
||||
const matchChip = chipFiltro === "todos" || (comp.chips && comp.chips.includes(chipFiltro));
|
||||
const matchBusca =
|
||||
busca.trim() === "" ||
|
||||
comp.name.toLowerCase().includes(busca.toLowerCase()) ||
|
||||
comp.namespace.toLowerCase().includes(busca.toLowerCase()) ||
|
||||
comp.titulo.toLowerCase().includes(busca.toLowerCase()) ||
|
||||
comp.descricao.toLowerCase().includes(busca.toLowerCase()) ||
|
||||
comp.tags.some((t) => t.toLowerCase().includes(busca.toLowerCase()));
|
||||
|
||||
return matchCategoria && matchChip && matchBusca;
|
||||
});
|
||||
}, [busca, categoriaAtiva, chipFiltro]);
|
||||
|
||||
// Manipuladores de cópia
|
||||
const copiarComando = (id, comando) => {
|
||||
navigator.clipboard.writeText(comando);
|
||||
setCopiadoId(id);
|
||||
setTimeout(() => setCopiadoId(null), 2000);
|
||||
};
|
||||
|
||||
const toggleSelecaoManifesto = (id) => {
|
||||
setComponentesSelecionados((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
// Geração dinâmica do manifesto YAML
|
||||
const manifestoYaml = useMemo(() => {
|
||||
const selecionados = COMPONENTES_REGISTRY.filter((c) =>
|
||||
componentesSelecionados.includes(c.id)
|
||||
);
|
||||
|
||||
if (selecionados.length === 0) {
|
||||
return `# Nenhum componente selecionado.\n# Clique em "+ Adicionar ao Manifesto" nos cards abaixo.`;
|
||||
}
|
||||
|
||||
let yaml = `# main/idf_component.yml\n# Gerado via ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/componentes)\n\ndependencies:\n`;
|
||||
selecionados.forEach((c) => {
|
||||
yaml += ` ${c.namespace}/${c.name}: "${c.versao}"\n`;
|
||||
});
|
||||
return yaml;
|
||||
}, [componentesSelecionados]);
|
||||
|
||||
const copiarManifesto = () => {
|
||||
navigator.clipboard.writeText(manifestoYaml);
|
||||
setManifestoCopiado(true);
|
||||
setTimeout(() => setManifestoCopiado(false), 2000);
|
||||
};
|
||||
|
||||
const baixarManifesto = () => {
|
||||
const blob = new Blob([manifestoYaml], { type: "text/yaml;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "idf_component.yml";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const limparFiltros = () => {
|
||||
setBusca("");
|
||||
setCategoriaAtiva("todos");
|
||||
setChipFiltro("todos");
|
||||
};
|
||||
|
||||
const totalFiltros = (busca.trim() ? 1 : 0) + (categoriaAtiva !== "todos" ? 1 : 0) + (chipFiltro !== "todos" ? 1 : 0);
|
||||
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
<PageHero
|
||||
trail={[{ label: "Component Registry" }]}
|
||||
icon={Box}
|
||||
label="ESP-IDF Component Manager"
|
||||
title="ESP Component Registry"
|
||||
lede="O catálogo oficial de pacotes reutilizáveis da Espressif para o ESP-IDF. Escolha drivers, voz e pilhas industriais, copie o comando e gere o manifesto do projeto."
|
||||
>
|
||||
<a href="https://components.espressif.com" target="_blank" rel="noopener noreferrer" className="btn btn-primary">
|
||||
Abrir o registro oficial
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only"> (abre em nova aba)</span>
|
||||
</a>
|
||||
<a href="#manifesto-builder" className="btn btn-ghost">
|
||||
Gerar idf_component.yml
|
||||
</a>
|
||||
</PageHero>
|
||||
|
||||
<section className="shell pt-10 sm:pt-12" aria-label="Busca e filtros">
|
||||
<div className="panel p-5 sm:p-6">
|
||||
<div className="relative">
|
||||
<label htmlFor="busca-componente" className="sr-only">Buscar por nome, namespace ou tag</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-componente"
|
||||
type="search"
|
||||
value={busca}
|
||||
onChange={(e) => setBusca(e.target.value)}
|
||||
placeholder="Buscar por nome, namespace ou tag"
|
||||
autoComplete="off"
|
||||
className="field pl-12 pr-4 text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<h2 className="text-sm font-semibold text-ink">Categoria</h2>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{CATEGORIAS_COMPONENTES.map((cat) => {
|
||||
const Icone = ICONES_CATEGORIA[cat.id] ?? Layers;
|
||||
const contagem = cat.id === "todos" ? COMPONENTES_REGISTRY.length : COMPONENTES_REGISTRY.filter((c) => c.categoria === cat.id).length;
|
||||
return (
|
||||
<button key={cat.id} type="button" onClick={() => setCategoriaAtiva(cat.id)} aria-pressed={categoriaAtiva === cat.id} className="chip-btn">
|
||||
<Icone className="h-4 w-4" aria-hidden="true" />
|
||||
{cat.id === "todos" ? "Todos" : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
|
||||
<span className="tabular-nums opacity-80">{contagem}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 border-t border-line pt-5">
|
||||
<h2 className="text-sm font-semibold text-ink">Chip compatível</h2>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{chipsDisponiveis.map((chip) => (
|
||||
<button key={chip} type="button" onClick={() => setChipFiltro(chip)} aria-pressed={chipFiltro === chip} className="chip-btn">
|
||||
{chip === "todos" ? "Todos" : chip}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell section pt-8 sm:pt-10" aria-labelledby="lista-titulo">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 id="lista-titulo" role="status" aria-live="polite" className="text-base font-semibold text-ink">
|
||||
{componentesFiltrados.length} {componentesFiltrados.length === 1 ? "componente" : "componentes"}
|
||||
</h2>
|
||||
<div className="flex flex-wrap items-center gap-x-5 gap-y-1">
|
||||
{totalFiltros > 0 && (
|
||||
<button type="button" onClick={limparFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
||||
<X className="h-4 w-4" aria-hidden="true" />
|
||||
Limpar filtros
|
||||
</button>
|
||||
)}
|
||||
<a href="#manifesto-builder" className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
||||
Manifesto: {componentesSelecionados.length} {componentesSelecionados.length === 1 ? "componente" : "componentes"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{componentesFiltrados.length === 0 ? (
|
||||
<div className="card mt-6 px-6 py-14 text-center">
|
||||
<Box className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
|
||||
<h3 className="mt-4 font-display text-xl font-semibold text-ink">Nenhum componente encontrado</h3>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Busque por um termo mais geral ou volte para todas as categorias.</p>
|
||||
<button type="button" onClick={limparFiltros} className="btn btn-primary mt-6">Limpar filtros</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{componentesFiltrados.map((comp) => {
|
||||
const selecionado = componentesSelecionados.includes(comp.id);
|
||||
const cliCmd = `idf.py add-dependency "${comp.namespace}/${comp.name}${comp.versao}"`;
|
||||
const foiCopiado = copiadoId === comp.id;
|
||||
|
||||
return (
|
||||
<article key={comp.id} className={`card flex flex-col overflow-hidden ${selecionado ? "border-accent ring-2 ring-accent/20" : ""}`}>
|
||||
<div className="flex-1 p-5 sm:p-6">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="flex flex-wrap items-center gap-2 text-xs text-muted">
|
||||
{comp.namespace}/
|
||||
{comp.oficial && (
|
||||
<span className="chip chip-accent px-2 py-0">
|
||||
<ShieldCheck className="h-3 w-3" aria-hidden="true" />
|
||||
Oficial
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<h3 className="mt-1 break-words font-display text-xl font-semibold tracking-[-0.03em] text-ink">{comp.name}</h3>
|
||||
<p className="mt-0.5 text-sm font-medium text-accent">{comp.titulo}</p>
|
||||
</div>
|
||||
<span className="shrink-0 rounded-md border border-line bg-surface-2 px-2 py-1 font-mono text-xs text-ink-soft">{comp.versao}</span>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 line-clamp-3 text-sm leading-relaxed text-muted">{comp.descricao}</p>
|
||||
|
||||
<ul className="mt-4 flex flex-wrap gap-1.5">
|
||||
{comp.tags.map((tag) => (
|
||||
<li key={tag} className="chip">{tag}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-line bg-surface-2/60 p-5 sm:px-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copiarComando(comp.id, cliCmd)}
|
||||
className="code-panel flex w-full min-h-11 items-center justify-between gap-3 rounded-lg px-3 py-2 text-left font-mono text-xs transition-colors hover:border-white/30"
|
||||
aria-label={`Copiar comando para adicionar ${comp.name}`}
|
||||
>
|
||||
<span className="truncate text-[#e9eef4]">{cliCmd}</span>
|
||||
{foiCopiado ? <Check className="h-4 w-4 shrink-0 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4 shrink-0 text-[#a6afbd]" aria-hidden="true" />}
|
||||
</button>
|
||||
<p role="status" className="sr-only">{foiCopiado ? "Comando copiado" : ""}</p>
|
||||
|
||||
<p className="mt-3 text-xs text-muted">
|
||||
<span className="font-medium text-ink-soft">Chips: </span>
|
||||
{comp.chips.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 border-t border-line p-4 sm:px-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSelecaoManifesto(comp.id)}
|
||||
aria-pressed={selecionado}
|
||||
className={`btn btn-sm ${selecionado ? "btn-primary" : "btn-ghost"}`}
|
||||
>
|
||||
{selecionado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Plus className="h-4 w-4" aria-hidden="true" />}
|
||||
{selecionado ? "No manifesto" : "Adicionar ao manifesto"}
|
||||
</button>
|
||||
<a
|
||||
href={comp.urlRegistry}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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"
|
||||
aria-label={`Abrir ${comp.name} no registro oficial (nova aba)`}
|
||||
>
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section id="manifesto-builder" className="shell scroll-mt-28 pb-16" aria-labelledby="manifesto-titulo">
|
||||
<div className="panel p-6 sm:p-9">
|
||||
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
|
||||
<div className="max-w-2xl">
|
||||
<p className="eyebrow">Gerador de manifesto</p>
|
||||
<h2 id="manifesto-titulo" className="section-title mt-3">Seu idf_component.yml</h2>
|
||||
<p className="mt-3 text-sm leading-relaxed text-muted">
|
||||
Salve o arquivo na pasta <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> do projeto e o
|
||||
ESP-IDF baixa todas as bibliotecas na compilação.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button type="button" onClick={copiarManifesto} className="btn btn-primary btn-sm">
|
||||
{manifestoCopiado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
|
||||
<span role="status">{manifestoCopiado ? "Copiado" : "Copiar YAML"}</span>
|
||||
</button>
|
||||
<button type="button" onClick={baixarManifesto} className="btn btn-ghost btn-sm">
|
||||
<Download className="h-4 w-4" aria-hidden="true" />
|
||||
Baixar .yml
|
||||
</button>
|
||||
{componentesSelecionados.length > 0 && (
|
||||
<button type="button" onClick={() => setComponentesSelecionados([])} className="btn btn-ghost btn-sm">
|
||||
<Trash2 className="h-4 w-4" aria-hidden="true" />
|
||||
Limpar
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="code-panel mt-6">
|
||||
<pre tabIndex={0} aria-label="Conteúdo do idf_component.yml" className="!text-sm"><code className="text-[#e9eef4]">{manifestoYaml}</code></pre>
|
||||
</div>
|
||||
|
||||
{componentesSelecionados.length > 0 && (
|
||||
<ul className="mt-5 flex flex-wrap items-center gap-2" aria-label="Componentes incluídos">
|
||||
{componentesSelecionados.map((id) => {
|
||||
const c = COMPONENTES_REGISTRY.find((item) => item.id === id);
|
||||
if (!c) return null;
|
||||
return (
|
||||
<li key={id} className="chip chip-accent gap-1 pr-1">
|
||||
{c.name}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSelecaoManifesto(id)}
|
||||
className="-my-2 grid h-11 w-11 place-items-center rounded-full hover:bg-accent/15"
|
||||
aria-label={`Remover ${c.name} do manifesto`}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell section border-t border-line pt-14" aria-labelledby="guia-titulo">
|
||||
<SectionHead id="guia-titulo" eyebrow="Guia rápido" title={GUIA_COMPONENT_MANAGER.titulo} lede={GUIA_COMPONENT_MANAGER.resumo} />
|
||||
|
||||
<ul className="mt-8 grid gap-3 md:grid-cols-2 lg:grid-cols-4">
|
||||
{GUIA_COMPONENT_MANAGER.vantagens.map((v) => (
|
||||
<li key={v.titulo} className="card p-5">
|
||||
<h3 className="font-semibold text-ink">{v.titulo}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted">{v.descricao}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h3 className="mt-12 flex items-center gap-2 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
||||
<Terminal className="h-5 w-5 text-accent" aria-hidden="true" />
|
||||
Comandos essenciais
|
||||
</h3>
|
||||
<ul className="mt-4 space-y-3">
|
||||
{GUIA_COMPONENT_MANAGER.comandosCli.map((cmd, i) => (
|
||||
<li key={cmd.exemplo} className="code-panel flex flex-col justify-between gap-3 p-4 md:flex-row md:items-center">
|
||||
<div className="min-w-0">
|
||||
<code className="block break-all font-mono text-sm text-[#5eead4]">$ {cmd.exemplo}</code>
|
||||
<p className="mt-1 text-sm text-[#a6afbd]">{cmd.explicacao}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copiarComando(`cmd-${i}`, cmd.exemplo)}
|
||||
className="inline-flex min-h-11 shrink-0 items-center gap-1.5 self-start rounded-full border border-white/15 px-4 text-sm font-medium text-[#e9eef4] transition-colors hover:bg-white/10 md:self-auto"
|
||||
>
|
||||
{copiadoId === `cmd-${i}` ? <Check className="h-4 w-4 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
|
||||
{copiadoId === `cmd-${i}` ? "Copiado" : "Copiar"}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="callout mt-8">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-ink">
|
||||
<HelpCircle className="h-4 w-4 text-accent" aria-hidden="true" />
|
||||
Versionamento semântico no ESP-IDF
|
||||
</h3>
|
||||
<ul className="mt-2 list-disc space-y-1.5 pl-5">
|
||||
<li><code>^1.2.0</code>: aceita atualizações menores e correções (1.2.1, 1.3.0), mas bloqueia mudanças grandes de API (2.0.0). <strong>Recomendado.</strong></li>
|
||||
<li><code>~1.2.0</code>: aceita só correções de bugs (1.2.1, 1.2.5).</li>
|
||||
<li><code>1.2.0</code>: trava na versão exata.</li>
|
||||
<li><code>*</code>: aceita qualquer versão. Evite em produção.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { paginaMeta, jsonLd } from "@/lib/seo";
|
||||
|
||||
export const metadata = paginaMeta({
|
||||
titulo: "Diagnóstico de Erros Comuns no ESP32: Guia Profissional",
|
||||
descricao: "Guia técnico definitivo para identificar e resolver Brownout, Guru Meditation, Task Watchdog, Stack Overflow, vazamento de memória e falhas de boot no ESP32 (Arduino e ESP-IDF).",
|
||||
caminho: "/diagnostico",
|
||||
keywords: [
|
||||
"erros comuns ESP32",
|
||||
"brownout detector was triggered",
|
||||
"guru meditation error",
|
||||
"task watchdog got triggered",
|
||||
"stack canary watchpoint triggered",
|
||||
"como debugar ESP32",
|
||||
"ESP32 reiniciando em loop",
|
||||
"failed to connect to esp32",
|
||||
],
|
||||
});
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
pergunta: "O que causa o erro 'Brownout detector was triggered' no ESP32?",
|
||||
resposta: "Ocorre quando a tensão no pino 3V3 cai abaixo de 2.8V ~ 2.43V devido a picos rápidos de corrente do rádio Wi-Fi/BLE (superando 500mA), cabos USB finos ou regulador LDO inadequado. A solução de engenharia envolve adicionar capacitores Low-ESR de 10µF a 100µF próximos ao VCC e usar LDOs robustos de 600mA+.",
|
||||
},
|
||||
{
|
||||
pergunta: "Como resolver o 'Guru Meditation Error (LoadProhibited)'?",
|
||||
resposta: "LoadProhibited indica tentativa de acessar um ponteiro nulo (NULL) ou inválido na memória. A solução consiste em usar o Espressif Exception Decoder ou addr2line para localizar a linha exata no código e implementar validações defensivas como if (ponteiro != NULL).",
|
||||
},
|
||||
{
|
||||
pergunta: "Por que o Task Watchdog Timer (TWDT) dispara no FreeRTOS?",
|
||||
resposta: "O TWDT dispara quando uma tarefa de prioridade maior monopoliza a CPU em um loop bloqueante (ex: while(1) sem vTaskDelay), impedindo a tarefa IDLE de alimentar o watchdog do sistema operacional.",
|
||||
},
|
||||
{
|
||||
pergunta: "Por que conexões HTTPS e TLS falham com código -0x2700 no ESP32?",
|
||||
resposta: "Porque ao ligar, o ESP32 inicia o relógio interno no ano de 1970. O validador de certificados TLS considera o certificado do servidor inválido por estar no 'futuro'. Sincronizar o relógio via SNTP (configTime) antes da conexão resolve o problema.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function DiagnosticoLayout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Diagnóstico & Erros Comuns", caminho: "/diagnostico" },
|
||||
])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.perguntas(faqs)
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.aplicacaoWeb({
|
||||
nome: "Guia de Diagnóstico e Erros no ESP32",
|
||||
descricao: "Ferramenta e guia técnico de análise de falhas, pânicos de CPU e decodificação de logs no ESP32.",
|
||||
caminho: "/diagnostico",
|
||||
})
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import {
|
||||
categoriasDiagnostico,
|
||||
sintomasRapidos,
|
||||
errosCatalogo,
|
||||
regrasDecodificadorLog,
|
||||
} from "@/lib/diagnosticoData";
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
Copy,
|
||||
Cpu,
|
||||
ExternalLink,
|
||||
HelpCircle,
|
||||
Layers,
|
||||
LayoutGrid,
|
||||
RotateCcw,
|
||||
Search,
|
||||
ShieldAlert,
|
||||
Terminal,
|
||||
Wifi,
|
||||
Wrench,
|
||||
X,
|
||||
XCircle,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
|
||||
/** Os rótulos dos sintomas vêm com emoji nos dados; a interface usa só texto e ícones. */
|
||||
const semEmoji = (texto) => texto.replace(/^[\p{Extended_Pictographic}\uFE0F\s]+/u, "");
|
||||
|
||||
const ICONES = { Zap, Cpu, Layers, Activity, Terminal, Wifi };
|
||||
|
||||
const EXEMPLOS_LOG = [
|
||||
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
|
||||
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
|
||||
["Task watchdog", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
|
||||
["Handshake TLS", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
|
||||
];
|
||||
|
||||
const GRAVIDADE = {
|
||||
"Crítica": "text-danger",
|
||||
Alta: "text-gold",
|
||||
};
|
||||
|
||||
const CHECKLISTS = [
|
||||
{
|
||||
icon: Zap,
|
||||
titulo: "Hardware e alimentação",
|
||||
itens: [
|
||||
"Capacitor de 10 µF a 100 µF de baixo ESR no VCC, mais um cerâmico de 100 nF junto ao chip.",
|
||||
"LDO dimensionado para picos de 600 mA a 1 A, com folga térmica.",
|
||||
"Nenhum periférico forçando nível lógico nos pinos de strapping durante o boot.",
|
||||
"Capacitor de 10 µF ou 1 µF no pino EN (RST) para auto-reset estável na gravação.",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Layers,
|
||||
titulo: "Firmware e FreeRTOS",
|
||||
itens: [
|
||||
"Toda tarefa FreeRTOS cede a CPU com vTaskDelay, filas ou semáforos.",
|
||||
"Toda ISR marcada com IRAM_ATTR e sem chamadas bloqueantes.",
|
||||
"Sem uso contínuo de String do Arduino em laços, para evitar fragmentação do heap.",
|
||||
"Horário sincronizado por SNTP antes de abrir conexões TLS/HTTPS.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function DiagnosticoPage() {
|
||||
const [termoBusca, setTermoBusca] = useState("");
|
||||
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
|
||||
const [sintomaAtivo, setSintomaAtivo] = useState(null);
|
||||
const [cardAberto, setCardAberto] = useState(null);
|
||||
const [linguagemCodigo, setLinguagemCodigo] = useState({}); // { [id]: 'arduino' | 'espidf' }
|
||||
const [copiadoId, setCopiadoId] = useState(null);
|
||||
const searchInputRef = useRef(null);
|
||||
|
||||
// Estado do Analisador de Logs
|
||||
const [logInput, setLogInput] = useState("");
|
||||
const [resultadoLog, setResultadoLog] = useState(null);
|
||||
|
||||
// Função de Análise do Log
|
||||
const analisarLog = (texto) => {
|
||||
setLogInput(texto);
|
||||
if (!texto || texto.trim() === "") {
|
||||
setResultadoLog(null);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const regra of regrasDecodificadorLog) {
|
||||
if (regra.padrao.test(texto)) {
|
||||
setResultadoLog(regra);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setResultadoLog({
|
||||
titulo: "Padrão de erro não categorizado automaticamente",
|
||||
tipo: "Desconhecido",
|
||||
explicacao:
|
||||
"Não identificamos uma assinatura clássica de Guru Meditation ou Brownout neste trecho. Verifique se a taxa de baud rate (ex: 115200) está correta e se o cabeçalho 'rst:' ou 'panic' está presente.",
|
||||
acaoRecomendada:
|
||||
"Procure por palavras-chave como 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' ou 'panic' no console serial.",
|
||||
});
|
||||
};
|
||||
|
||||
const copiarParaClipboard = (texto, id) => {
|
||||
navigator.clipboard.writeText(texto);
|
||||
setCopiadoId(id);
|
||||
setTimeout(() => setCopiadoId(null), 2000);
|
||||
};
|
||||
|
||||
// Filtragem dos erros
|
||||
const errosFiltrados = useMemo(() => {
|
||||
const termo = termoBusca.toLowerCase().trim();
|
||||
return errosCatalogo.filter((erro) => {
|
||||
const matchCategoria =
|
||||
categoriaAtiva === "todos" || erro.categoria === categoriaAtiva;
|
||||
const matchSintoma =
|
||||
!sintomaAtivo || erro.sintomas.includes(sintomaAtivo);
|
||||
const matchBusca =
|
||||
!termo ||
|
||||
erro.titulo.toLowerCase().includes(termo) ||
|
||||
erro.resumo.toLowerCase().includes(termo) ||
|
||||
erro.causaRaiz.toLowerCase().includes(termo) ||
|
||||
erro.logSerial.toLowerCase().includes(termo);
|
||||
|
||||
return matchCategoria && matchSintoma && matchBusca;
|
||||
});
|
||||
}, [categoriaAtiva, sintomaAtivo, termoBusca]);
|
||||
|
||||
const temFiltroAtivo =
|
||||
termoBusca.trim() !== "" ||
|
||||
categoriaAtiva !== "todos" ||
|
||||
sintomaAtivo !== null;
|
||||
|
||||
const limparTodosFiltros = () => {
|
||||
setTermoBusca("");
|
||||
setCategoriaAtiva("todos");
|
||||
setSintomaAtivo(null);
|
||||
searchInputRef.current?.focus();
|
||||
};
|
||||
|
||||
const toggleCard = (id) => {
|
||||
setCardAberto((prev) => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
const setLang = (id, lang) => {
|
||||
setLinguagemCodigo((prev) => ({ ...prev, [id]: lang }));
|
||||
};
|
||||
|
||||
|
||||
const totalFiltros = (termoBusca.trim() ? 1 : 0) + (categoriaAtiva !== "todos" ? 1 : 0) + (sintomaAtivo ? 1 : 0);
|
||||
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
<PageHero
|
||||
trail={[{ label: "Diagnóstico" }]}
|
||||
icon={AlertTriangle}
|
||||
label="Depuração de firmware"
|
||||
title="Diagnóstico de falhas do ESP32"
|
||||
lede="Entenda a causa de Brownout, Guru Meditation, Watchdog e Stack Overflow, e aplique a correção de engenharia, não o remendo."
|
||||
>
|
||||
<a
|
||||
href="https://cienciaembarcada.com.br/publicacoes/esp32-5-erros-comuns-e-como-resolve-los-profissionalmente/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong"
|
||||
>
|
||||
Baseado no artigo do Ciência Embarcada
|
||||
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only"> (abre em nova aba)</span>
|
||||
</a>
|
||||
</PageHero>
|
||||
|
||||
<section className="shell pt-10 sm:pt-12" aria-labelledby="decodificador-titulo">
|
||||
<div className="panel p-5 sm:p-8">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-end">
|
||||
<div className="max-w-xl">
|
||||
<h2 id="decodificador-titulo" className="section-title">Cole o log e veja a causa</h2>
|
||||
<p className="mt-2 text-sm text-muted">Copie o trecho do monitor serial. O diagnóstico aparece na hora.</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm text-muted">Testar com</span>
|
||||
{EXEMPLOS_LOG.map(([rotulo, texto]) => (
|
||||
<button key={rotulo} type="button" onClick={() => analisarLog(texto)} className="chip-btn">
|
||||
{rotulo}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-5">
|
||||
<label htmlFor="log-serial" className="sr-only">Log do monitor serial</label>
|
||||
<textarea
|
||||
id="log-serial"
|
||||
rows={4}
|
||||
value={logInput}
|
||||
onChange={(e) => analisarLog(e.target.value)}
|
||||
placeholder="Cole o log do monitor serial aqui"
|
||||
spellCheck={false}
|
||||
className="code-panel field !min-h-32 w-full resize-y !rounded-xl !border-white/10 !bg-[var(--code-bg)] p-4 font-mono text-sm !text-[#e9eef4] placeholder:!text-[#a6afbd]"
|
||||
/>
|
||||
{logInput && (
|
||||
<button type="button" onClick={() => analisarLog("")} className="absolute right-3 top-3 inline-flex min-h-11 items-center gap-1 rounded-full border border-white/15 px-3 text-xs font-medium text-[#e9eef4] hover:bg-white/10">
|
||||
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Limpar
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div role="status" aria-live="polite">
|
||||
{resultadoLog && (
|
||||
<div className="callout callout-warn mt-4 flex items-start gap-3">
|
||||
<ShieldAlert className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
|
||||
<div className="min-w-0 space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="font-semibold text-ink">{resultadoLog.titulo}</h3>
|
||||
<span className="chip">{resultadoLog.tipo}</span>
|
||||
</div>
|
||||
<p><strong className="font-semibold text-ink">Causa provável:</strong> {resultadoLog.explicacao}</p>
|
||||
<p><strong className="font-semibold text-ink">O que fazer:</strong> {resultadoLog.acaoRecomendada}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell section pt-12 sm:pt-14" aria-labelledby="catalogo-titulo">
|
||||
<h2 id="catalogo-titulo" className="section-title">Catálogo de falhas</h2>
|
||||
|
||||
<div className="panel mt-6 p-5 sm:p-6">
|
||||
<div className="relative">
|
||||
<label htmlFor="busca-erro" className="sr-only">Buscar por erro, sintoma ou mensagem serial</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-erro"
|
||||
ref={searchInputRef}
|
||||
type="search"
|
||||
value={termoBusca}
|
||||
onChange={(e) => setTermoBusca(e.target.value)}
|
||||
placeholder="Buscar erro, sintoma ou mensagem"
|
||||
autoComplete="off"
|
||||
className="field pl-12 pr-4 text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<h3 className="text-sm font-semibold text-ink">Sintoma</h3>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{sintomasRapidos.map((sintoma) => (
|
||||
<button key={sintoma.id} type="button" onClick={() => setSintomaAtivo(sintomaAtivo === sintoma.id ? null : sintoma.id)} aria-pressed={sintomaAtivo === sintoma.id} className="chip-btn">
|
||||
{semEmoji(sintoma.rotulo)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 border-t border-line pt-5">
|
||||
<h3 className="text-sm font-semibold text-ink">Área</h3>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button type="button" onClick={() => setCategoriaAtiva("todos")} aria-pressed={categoriaAtiva === "todos"} className="chip-btn">
|
||||
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
|
||||
Todas
|
||||
<span className="tabular-nums opacity-80">{errosCatalogo.length}</span>
|
||||
</button>
|
||||
{categoriasDiagnostico.map((cat) => {
|
||||
const Icone = ICONES[cat.icone] ?? AlertTriangle;
|
||||
const total = errosCatalogo.filter((e) => e.categoria === cat.id).length;
|
||||
return (
|
||||
<button key={cat.id} type="button" onClick={() => setCategoriaAtiva(cat.id)} aria-pressed={categoriaAtiva === cat.id} className="chip-btn">
|
||||
<Icone className="h-4 w-4" aria-hidden="true" />
|
||||
{cat.nome.split(/ &|,/)[0]}
|
||||
<span className="tabular-nums opacity-80">{total}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap items-center justify-between gap-3">
|
||||
<p role="status" aria-live="polite" className="text-sm font-semibold text-ink">
|
||||
{errosFiltrados.length} {errosFiltrados.length === 1 ? "falha" : "falhas"}
|
||||
{totalFiltros > 0 && <span className="font-normal text-muted"> com os filtros atuais</span>}
|
||||
</p>
|
||||
{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" />
|
||||
Redefinir filtros
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-3">
|
||||
{errosFiltrados.length === 0 ? (
|
||||
<div className="card 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">Nenhuma falha com esses filtros</h3>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Busque por um termo mais geral ou remova o sintoma e a área selecionados.</p>
|
||||
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">Limpar filtros</button>
|
||||
</div>
|
||||
) : (
|
||||
errosFiltrados.map((erro) => {
|
||||
const estaAberto = cardAberto === erro.id;
|
||||
const lang = linguagemCodigo[erro.id] || "arduino";
|
||||
const codigoAtual =
|
||||
lang === "arduino" ? erro.codigoArduino : lang === "espidf" ? erro.codigoEspIdf : erro.codigoMicroPython || "# Exemplo não aplicável";
|
||||
const catInfo = categoriasDiagnostico.find((c) => c.id === erro.categoria);
|
||||
const IconeCat = ICONES[catInfo?.icone] ?? AlertTriangle;
|
||||
|
||||
return (
|
||||
<article key={erro.id} id={erro.id} className={`card scroll-mt-28 overflow-hidden ${estaAberto ? "border-line-strong" : ""}`}>
|
||||
<h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCard(erro.id)}
|
||||
aria-expanded={estaAberto}
|
||||
aria-controls={`detalhe-${erro.id}`}
|
||||
className="flex w-full cursor-pointer items-start justify-between gap-4 p-5 text-left sm:p-6"
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<span className="chip">
|
||||
<IconeCat className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
|
||||
{catInfo?.nome}
|
||||
</span>
|
||||
<span className={`chip ${GRAVIDADE[erro.gravidade] ?? "text-accent"}`}>Gravidade {erro.gravidade.toLowerCase()}</span>
|
||||
</span>
|
||||
<span className="mt-3 block font-display text-xl font-semibold tracking-[-0.03em] text-ink">{erro.titulo}</span>
|
||||
<span className="mt-1.5 block text-sm font-normal leading-relaxed text-muted">{erro.resumo}</span>
|
||||
</span>
|
||||
<span className="mt-1 grid h-9 w-9 shrink-0 place-items-center rounded-full bg-surface-2 text-muted">
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${estaAberto ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
</span>
|
||||
</button>
|
||||
</h3>
|
||||
|
||||
{estaAberto && (
|
||||
<div id={`detalhe-${erro.id}`} className="space-y-6 border-t border-line p-5 sm:p-6">
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
|
||||
<Terminal className="h-4 w-4 text-accent" aria-hidden="true" />
|
||||
Como aparece no monitor serial
|
||||
</h4>
|
||||
<button type="button" onClick={() => copiarParaClipboard(erro.logSerial, `log-${erro.id}`)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
||||
{copiadoId === `log-${erro.id}` ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
|
||||
{copiadoId === `log-${erro.id}` ? "Copiado" : "Copiar log"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="code-panel mt-1">
|
||||
<pre tabIndex={0}><code className="text-[#e9eef4]">{erro.logSerial}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
|
||||
<Cpu className="h-4 w-4 text-accent" aria-hidden="true" />
|
||||
Causa raiz
|
||||
</h4>
|
||||
<p className="callout mt-2">{erro.causaRaiz}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="callout callout-danger">
|
||||
<p className="flex items-center gap-2 font-semibold text-ink">
|
||||
<XCircle className="h-4 w-4 shrink-0 text-danger" aria-hidden="true" />
|
||||
O remendo comum
|
||||
</p>
|
||||
<p className="mt-2 font-medium text-ink">{erro.gambiarra.titulo}</p>
|
||||
<p className="mt-1">{erro.gambiarra.descricao}</p>
|
||||
<p className="mt-2"><strong className="font-semibold text-danger">Consequência:</strong> {erro.gambiarra.consequencia}</p>
|
||||
</div>
|
||||
|
||||
<div className="callout callout-ok">
|
||||
<p className="flex items-center gap-2 font-semibold text-ink">
|
||||
<CheckCircle2 className="h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
|
||||
A solução de engenharia
|
||||
</p>
|
||||
<p className="mt-2 font-medium text-ink">{erro.solucaoEngenharia.titulo}</p>
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{erro.solucaoEngenharia.passos.map((passo) => (
|
||||
<li key={passo} className="flex items-start gap-2">
|
||||
<Check className="mt-1 h-3.5 w-3.5 shrink-0 text-ok" aria-hidden="true" />
|
||||
{passo}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div role="group" aria-label="Linguagem do exemplo" className="flex flex-wrap gap-2">
|
||||
{[
|
||||
["arduino", "Arduino C++"],
|
||||
["espidf", "ESP-IDF (C)"],
|
||||
["micropython", "MicroPython"],
|
||||
].map(([id, rotulo]) => (
|
||||
<button key={id} type="button" onClick={() => setLang(erro.id, id)} aria-pressed={lang === id} className="chip-btn">
|
||||
{rotulo}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button type="button" onClick={() => copiarParaClipboard(codigoAtual, `code-${erro.id}`)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
||||
{copiadoId === `code-${erro.id}` ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
|
||||
{copiadoId === `code-${erro.id}` ? "Copiado" : "Copiar código"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="code-panel mt-3">
|
||||
<pre tabIndex={0}><code className="text-[#e9eef4]">{codigoAtual}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="callout callout-warn flex items-start gap-3">
|
||||
<Wrench className="mt-0.5 h-4 w-4 shrink-0 text-gold" aria-hidden="true" />
|
||||
<p><strong className="font-semibold text-ink">Dica de engenharia:</strong> {erro.dicaPro}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell pb-20 md:pb-28" aria-labelledby="checklist-titulo">
|
||||
<h2 id="checklist-titulo" className="section-title">Checklist antes de ir para campo</h2>
|
||||
<p className="mt-3 max-w-2xl text-base text-muted">Itens essenciais para validar no hardware e no firmware antes de entregar o produto.</p>
|
||||
<div className="mt-8 grid gap-4 md:grid-cols-2">
|
||||
{CHECKLISTS.map(({ icon: Icon, titulo, itens }) => (
|
||||
<div key={titulo} className="card p-5 sm:p-7">
|
||||
<h3 className="flex items-center gap-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
||||
<span className="grid h-9 w-9 place-items-center rounded-full bg-accent-soft text-accent">
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
{titulo}
|
||||
</h3>
|
||||
<ul className="mt-5 space-y-3">
|
||||
{itens.map((item) => (
|
||||
<li key={item} className="flex items-start gap-3 text-sm leading-relaxed text-ink-soft">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { paginaMeta, jsonLd } from "@/lib/seo";
|
||||
import Link from "next/link";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import { notFound } from "next/navigation";
|
||||
import CodeExample from "@/components/CodeExample";
|
||||
import frameworksData from "@/public/frameworks.json";
|
||||
import { ArrowRight, ArrowUpRight, BookOpen, Box, Check, Code, Layers } from "lucide-react";
|
||||
|
||||
export default async function FrameworkDetail({ params }) {
|
||||
const { key } = await params;
|
||||
const framework = frameworksData[key];
|
||||
|
||||
if (!framework) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const features = framework.caracteristicas || [];
|
||||
const useCases = framework.casos_uso || [];
|
||||
const isEspIdf = key === "ESP-IDF";
|
||||
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
{/* Ficha técnica do framework e trilha de navegação para o Google */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.framework(key, framework)) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Frameworks", caminho: "/frameworks" },
|
||||
{ nome: framework.nome, caminho: `/frameworks/${encodeURIComponent(key)}` },
|
||||
])),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
<PageHero
|
||||
trail={[{ label: "Frameworks e SDKs", href: "/frameworks" }, { label: framework.nome }]}
|
||||
icon={Layers}
|
||||
label={framework.tipo}
|
||||
title={framework.nome}
|
||||
lede={framework.descricao}
|
||||
>
|
||||
{framework.documentacao && (
|
||||
<a href={framework.documentacao} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
|
||||
<BookOpen className="h-4 w-4" aria-hidden="true" />
|
||||
Documentação oficial
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
{framework.repositorio && (
|
||||
<a href={framework.repositorio} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
|
||||
<Code className="h-4 w-4" aria-hidden="true" />
|
||||
Repositório no GitHub
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
</PageHero>
|
||||
|
||||
<section className="shell mt-10" aria-label="Resumo">
|
||||
<dl className="grid gap-3 md:grid-cols-3">
|
||||
<div className="tile">
|
||||
<dt className="text-xs font-medium text-muted">Linguagem</dt>
|
||||
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{framework.linguagem}</dd>
|
||||
<dd className="mt-0.5 text-xs text-muted">{framework.nome_completo}</dd>
|
||||
</div>
|
||||
<div className="tile">
|
||||
<dt className="text-xs font-medium text-muted">Categoria</dt>
|
||||
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{framework.tipo}</dd>
|
||||
</div>
|
||||
<div className="tile">
|
||||
<dt className="text-xs font-medium text-muted">Objetivo principal</dt>
|
||||
<dd className="mt-1 text-sm font-medium leading-relaxed text-ink">{framework.funcao_principal}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section className="shell mt-12 grid gap-5 lg:grid-cols-2" aria-label="Recursos e aplicações">
|
||||
<div className="card p-5 sm:p-7">
|
||||
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Recursos de destaque</h2>
|
||||
<ul className="mt-5 space-y-3">
|
||||
{features.map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-3 text-sm leading-relaxed text-ink-soft">
|
||||
<Check className="mt-0.5 h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="card p-5 sm:p-7">
|
||||
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Quando usar</h2>
|
||||
<ul className="mt-5 space-y-2.5">
|
||||
{useCases.map((useCase) => (
|
||||
<li key={useCase} className="tile text-sm font-medium text-ink-soft">
|
||||
{useCase}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{isEspIdf && (
|
||||
<section className="shell mt-8">
|
||||
<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">
|
||||
<span className="chip chip-accent">
|
||||
<Box className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
ESP-IDF 4.4 ou superior
|
||||
</span>
|
||||
<h2 className="section-title mt-4">Módulos com o ESP Component Registry</h2>
|
||||
<p className="mt-3 text-base leading-relaxed text-muted">
|
||||
Nos projetos ESP-IDF v4.4+ e v5.x, as bibliotecas não precisam mais entrar como submódulos Git. Use o{" "}
|
||||
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> e o comando{" "}
|
||||
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf.py add-dependency</code> para baixar drivers de display (LVGL), pilhas industriais (Modbus), RainMaker e mais.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/componentes" className="btn btn-primary shrink-0">
|
||||
Explorar componentes
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{framework.exemplo && (
|
||||
<section className="shell mt-8">
|
||||
<CodeExample framework={framework} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="shell section pt-10">
|
||||
<Link href="/frameworks" className="inline-flex min-h-11 items-center gap-2 text-sm font-medium text-accent hover:text-accent-strong">
|
||||
<ArrowRight className="h-4 w-4 rotate-180" aria-hidden="true" />
|
||||
Ver todos os frameworks
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { key } = await params;
|
||||
const framework = frameworksData[key];
|
||||
|
||||
if (!framework) {
|
||||
return { title: "Framework não encontrado", robots: { index: false, follow: false } };
|
||||
}
|
||||
|
||||
return {
|
||||
...paginaMeta({
|
||||
titulo: `${framework.nome} para ESP32 · Guia Técnico`,
|
||||
descricao: `${framework.nome_completo}: ${framework.descricao} Guia com exemplos práticos de código e casos de uso.`,
|
||||
caminho: `/frameworks/${encodeURIComponent(key)}`,
|
||||
tipo: "article",
|
||||
keywords: [
|
||||
framework.nome, framework.nome_completo,
|
||||
`${framework.nome} ESP32`, `programar ESP32 com ${framework.nome}`,
|
||||
"ESP32", "framework ESP32",
|
||||
],
|
||||
}),
|
||||
other: { "article:section": "Frameworks" },
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(frameworksData).map((key) => ({
|
||||
key: key,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { paginaMeta, jsonLd } from "@/lib/seo";
|
||||
|
||||
export const metadata = paginaMeta({
|
||||
titulo: "Frameworks & SDKs para ESP32: Guia Completo e Comparativo",
|
||||
descricao: "Guia completo de desenvolvimento: ESP-IDF, ESP-RainMaker, ESP-Matter, Rust, ESP-ADF, ESP-SR, ESP-WHO, Arduino e MicroPython com exemplos de código.",
|
||||
caminho: "/frameworks",
|
||||
keywords: [
|
||||
"ESP-IDF",
|
||||
"ESP-RainMaker",
|
||||
"ESP-Matter",
|
||||
"Rust ESP32",
|
||||
"ESP-SR",
|
||||
"ESP-ADF",
|
||||
"ESP-IoT-Solution",
|
||||
"Arduino ESP32",
|
||||
"MicroPython ESP32",
|
||||
"frameworks ESP32",
|
||||
"programar ESP32",
|
||||
],
|
||||
});
|
||||
|
||||
export default function FrameworksLayout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Frameworks", caminho: "/frameworks" },
|
||||
])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import SectionHead from "@/components/SectionHead";
|
||||
import Link from "next/link";
|
||||
import frameworksData from "@/public/frameworks.json";
|
||||
import { ArrowRight, ArrowUpRight, Box, Brain, Cloud, GraduationCap, Layers, Mic, Music, Sparkles } from "lucide-react";
|
||||
|
||||
const GUIAS = [
|
||||
{
|
||||
icon: GraduationCap,
|
||||
titulo: "Estou começando",
|
||||
texto: "Arduino ou MicroPython são os caminhos mais rápidos para testar sensores e APIs. Para produto, vá de ESP-IDF ou Rust.",
|
||||
links: [
|
||||
["Arduino", "/frameworks/Arduino"],
|
||||
["MicroPython", "/frameworks/MicroPython"],
|
||||
["Rust", "/frameworks/Rust-ESP"],
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Cloud,
|
||||
titulo: "Nuvem e casa conectada",
|
||||
texto: "ESP-Matter para interoperabilidade, ESP-RainMaker para nuvem pronta e Zigbee para redes locais de baixo consumo.",
|
||||
links: [
|
||||
["RainMaker", "/frameworks/ESP-RainMaker"],
|
||||
["Matter", "/frameworks/ESP-Matter"],
|
||||
["Zigbee", "/frameworks/ESP-Zigbee-SDK"],
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
titulo: "Áudio, voz e IA",
|
||||
texto: "ESP-ADF para áudio, ESP-SR para reconhecimento de fala offline e ESP-NN para acelerar TinyML.",
|
||||
links: [
|
||||
["Áudio", "/frameworks/ESP-ADF", Music],
|
||||
["Voz", "/frameworks/ESP-SR", Mic],
|
||||
["IA", "/frameworks/ESP-NN", Brain],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Frameworks() {
|
||||
const frameworks = Object.entries(frameworksData);
|
||||
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
<PageHero
|
||||
trail={[{ label: "Frameworks e SDKs" }]}
|
||||
icon={Layers}
|
||||
label={`${frameworks.length} frameworks e SDKs`}
|
||||
title="Frameworks e SDKs do ESP32"
|
||||
lede="Do C/C++ nativo com FreeRTOS ao Rust, passando por nuvem IoT, reconhecimento de voz, áudio e Matter. Escolha pelo que você quer construir."
|
||||
/>
|
||||
|
||||
<section className="shell pt-10 sm:pt-12" aria-label="Por onde começar">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{GUIAS.map(({ icon: Icon, titulo, texto, links }) => (
|
||||
<div key={titulo} className="card flex flex-col p-5 sm:p-6">
|
||||
<span className="grid h-10 w-10 place-items-center rounded-full bg-accent-soft text-accent">
|
||||
<Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
<h2 className="mt-4 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{titulo}</h2>
|
||||
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted">{texto}</p>
|
||||
<ul className="mt-5 flex flex-wrap gap-2 border-t border-line pt-4">
|
||||
{links.map(([rotulo, href, Extra]) => (
|
||||
<li key={rotulo}>
|
||||
<Link href={href} className="chip-btn">
|
||||
{Extra && <Extra className="h-4 w-4" aria-hidden="true" />}
|
||||
{rotulo}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell mt-10">
|
||||
<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">
|
||||
<span className="chip chip-accent">
|
||||
<Box className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Arquitetura modular
|
||||
</span>
|
||||
<h2 className="section-title mt-4">ESP Component Registry</h2>
|
||||
<p className="mt-3 text-base leading-relaxed text-muted">
|
||||
Instale drivers de display (LVGL), pilhas industriais (Modbus), recursos de nuvem e pacotes de placa
|
||||
(BSPs) com uma linha no manifesto <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code>.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/componentes" className="btn btn-primary shrink-0">
|
||||
Explorar componentes
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell section" aria-labelledby="todos-titulo">
|
||||
<SectionHead id="todos-titulo" eyebrow="Todos" title="Todos os frameworks" lede="Cada ficha traz recursos, aplicações ideais, exemplo de código e links oficiais." />
|
||||
|
||||
<div className="mt-8 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{frameworks.map(([key, framework]) => (
|
||||
<Link key={key} href={`/frameworks/${encodeURIComponent(key)}`} className="card card-interactive group flex flex-col p-5 sm:p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<h3 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">{framework.nome}</h3>
|
||||
<span className="chip max-w-full">{framework.linguagem}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-muted">{framework.tipo}</p>
|
||||
<p className="mt-3 line-clamp-3 text-sm leading-relaxed text-muted">{framework.descricao}</p>
|
||||
|
||||
<ul className="mt-5 flex flex-1 flex-wrap content-start gap-1.5">
|
||||
{framework.casos_uso.slice(0, 3).map((caso) => (
|
||||
<li key={caso} className="chip max-w-full">
|
||||
<span className="truncate">{caso}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<span className="mt-5 inline-flex items-center gap-1 border-t border-line pt-4 text-sm font-medium text-accent">
|
||||
Ver detalhes e exemplo
|
||||
<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>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { paginaMeta, jsonLd } from "@/lib/seo";
|
||||
import { categoriasGlossario } from "@/lib/glossarioData";
|
||||
|
||||
export const metadata = paginaMeta({
|
||||
titulo: "Glossário Técnico do ESP32: Termos e Conceitos",
|
||||
descricao: "Mais de 50 termos e conceitos essenciais do ecossistema ESP32 explicados em português: GPIO, Strapping Pins, ULP, PSRAM, FreeRTOS, eFuse, Deep Sleep, OTA, Secure Boot, ECDSA, Matter e Thread.",
|
||||
caminho: "/glossario",
|
||||
keywords: [
|
||||
"glossário ESP32",
|
||||
"o que é GPIO",
|
||||
"o que é strapping pins",
|
||||
"o que é ULP ESP32",
|
||||
"o que é PSRAM ESP32",
|
||||
"termos técnicos microcontrolador",
|
||||
"conceitos ESP32 português",
|
||||
],
|
||||
});
|
||||
|
||||
export default function GlossarioLayout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(jsonLd.glossario(categoriasGlossario)),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Glossário Técnico", caminho: "/glossario" },
|
||||
])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.aplicacaoWeb({
|
||||
nome: "Glossário Técnico do ESP32",
|
||||
descricao: "Enciclopédia de termos técnicos e conceitos de hardware e software do ecossistema ESP32.",
|
||||
caminho: "/glossario",
|
||||
})
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import Link from "next/link";
|
||||
import { categoriasGlossario } from "@/lib/glossarioData";
|
||||
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 [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(),
|
||||
}))
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
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) => {
|
||||
navigator.clipboard.writeText(`${window.location.origin}/glossario#${slug}`);
|
||||
setCopiadoSlug(slug);
|
||||
setTimeout(() => setCopiadoSlug(null), 2000);
|
||||
};
|
||||
|
||||
const total = termosFiltrados.length;
|
||||
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
<PageHero
|
||||
trail={[{ label: "Glossário" }]}
|
||||
icon={BookOpen}
|
||||
label={`${todosOsTermos.length} termos explicados`}
|
||||
title="Glossário técnico do ESP32"
|
||||
lede="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="Busca e filtros">
|
||||
<div className="panel p-5 sm:p-7">
|
||||
<div className="relative">
|
||||
<label htmlFor="busca-glossario" className="sr-only">
|
||||
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="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" />
|
||||
Limpar
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
<span className="mr-1 text-sm text-muted">Mais buscados</span>
|
||||
{BUSCAS_POPULARES.map((rotulo) => (
|
||||
<button
|
||||
key={rotulo}
|
||||
type="button"
|
||||
onClick={() => setBusca(rotulo)}
|
||||
aria-pressed={busca.toLowerCase() === rotulo.toLowerCase()}
|
||||
className="chip-btn"
|
||||
>
|
||||
{rotulo}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-7 border-t border-line pt-6">
|
||||
<h2 className="text-sm font-semibold text-ink">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" />
|
||||
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>
|
||||
|
||||
<div className="mt-6 border-t border-line pt-6">
|
||||
<h2 className="text-sm font-semibold text-ink">Ordem alfabética</h2>
|
||||
<div className="mt-3 flex flex-wrap gap-1.5" role="group" aria-label="Filtrar pela letra inicial">
|
||||
<button type="button" onClick={() => setLetraFiltro(null)} aria-pressed={letraFiltro === null} className="chip-btn">
|
||||
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-11 justify-center px-0 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-surface"
|
||||
>
|
||||
{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 ? "termo" : "termos"}
|
||||
{temFiltroAtivo && <span className="font-normal text-muted"> 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" />
|
||||
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">Nenhum termo encontrado</h3>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
|
||||
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" />
|
||||
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={`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} />
|
||||
</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" />
|
||||
Na prática
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
<TextoComLinks texto={item.exemplo} />
|
||||
</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">Do conceito ao chip certo.</h2>
|
||||
<p className="lede mx-auto mt-4 max-w-2xl">
|
||||
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="/seletor" className="btn btn-primary w-full sm:w-auto">
|
||||
<Sliders className="h-4 w-4" aria-hidden="true" />
|
||||
Escolher meu chip
|
||||
</Link>
|
||||
<Link href="/series" className="btn btn-ghost w-full sm:w-auto">
|
||||
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 }) {
|
||||
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={`/series/${parte}`}
|
||||
className="font-semibold text-accent underline decoration-accent/30 underline-offset-2 hover:decoration-accent"
|
||||
>
|
||||
{parte}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return parte;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { JetBrains_Mono, Manrope, Space_Grotesk } from "next/font/google";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import "../globals.css";
|
||||
import Providers from "@/components/Providers";
|
||||
import { SITE, jsonLd } from "@/lib/seo";
|
||||
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-space-grotesk",
|
||||
display: "swap",
|
||||
weight: ["500", "600", "700"],
|
||||
});
|
||||
|
||||
const manrope = Manrope({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-manrope",
|
||||
display: "swap",
|
||||
weight: ["400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-jetbrains",
|
||||
display: "swap",
|
||||
weight: ["400", "500", "600"],
|
||||
});
|
||||
|
||||
const LANG_MAP = {
|
||||
pt: "pt-BR",
|
||||
en: "en",
|
||||
es: "es-419",
|
||||
fr: "fr",
|
||||
de: "de",
|
||||
"zh-CN": "zh-CN",
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { locale } = await params;
|
||||
const ogLocaleMap = {
|
||||
pt: "pt_BR",
|
||||
en: "en_US",
|
||||
es: "es_419",
|
||||
fr: "fr_FR",
|
||||
de: "de_DE",
|
||||
"zh-CN": "zh_CN",
|
||||
};
|
||||
|
||||
return {
|
||||
metadataBase: new URL(SITE.url),
|
||||
title: {
|
||||
default: "ESPDocs · Documentação do ESP32 em português",
|
||||
template: "%s · ESPDocs",
|
||||
},
|
||||
description:
|
||||
"Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro. Datasheets, periféricos, segurança em hardware e diagramas de conexão.",
|
||||
applicationName: "ESPDocs",
|
||||
authors: [{ name: "Lucas Rayan Guerra", url: "https://lucasrguerra.dev.br" }],
|
||||
creator: "Lucas Rayan Guerra",
|
||||
publisher: "Ciência Embarcada",
|
||||
category: "technology",
|
||||
keywords: [
|
||||
"ESP32", "ESP32 português", "documentação ESP32", "pinout ESP32",
|
||||
"ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-P4", "ESP32-H2",
|
||||
"comparar ESP32", "qual ESP32 escolher", "datasheet ESP32",
|
||||
"microcontrolador", "Espressif", "ESP-IDF", "IoT", "sistemas embarcados",
|
||||
],
|
||||
alternates: { canonical: `/${locale}` },
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: ogLocaleMap[locale] || "pt_BR",
|
||||
url: `${SITE.url}/${locale}`,
|
||||
siteName: SITE.nome,
|
||||
title: "ESPDocs · Documentação do ESP32 em português",
|
||||
description:
|
||||
"Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro.",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "ESPDocs · Documentação do ESP32 em português",
|
||||
description:
|
||||
"Especificações, pinagem, comparação e seleção das 12 séries de ESP32, em português.",
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
"max-video-preview": -1,
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.ico", sizes: "48x48", type: "image/x-icon" },
|
||||
{ url: "/favicon-48x48.png", sizes: "48x48", type: "image/png" },
|
||||
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
||||
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
||||
{ url: "/icon-192.png", sizes: "192x192", type: "image/png" },
|
||||
{ url: "/icon-512.png", sizes: "512x512", type: "image/png" },
|
||||
{ url: "/icon.svg", type: "image/svg+xml" },
|
||||
],
|
||||
apple: [
|
||||
{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
|
||||
],
|
||||
shortcut: "/favicon.ico",
|
||||
},
|
||||
manifest: "/site.webmanifest",
|
||||
formatDetection: { telephone: false, address: false, email: false },
|
||||
};
|
||||
}
|
||||
|
||||
export const viewport = {
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#f5f6f8" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#07090d" },
|
||||
],
|
||||
};
|
||||
|
||||
export default async function RootLayout({ children, params }) {
|
||||
const { locale } = await params;
|
||||
if (!routing.locales.includes(locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const lang = LANG_MAP[locale] || locale;
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang={lang} className={`${spaceGrotesk.variable} ${manrope.variable} ${jetbrainsMono.variable}`} suppressHydrationWarning>
|
||||
<head>
|
||||
{/* Dados estruturados do site e da organização. É o que permite ao
|
||||
Google montar o box de busca interna e entender a autoria. */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.siteESociedade()) }}
|
||||
/>
|
||||
</head>
|
||||
<body className="font-sans antialiased bg-canvas text-ink overflow-x-clip" suppressHydrationWarning>
|
||||
<a
|
||||
href="#conteudo"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[200] focus:rounded-full focus:bg-ink focus:px-5 focus:py-3 focus:text-sm focus:font-semibold focus:text-canvas"
|
||||
>
|
||||
Pular para o conteúdo
|
||||
</a>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import BookCover from "@/components/BookCover";
|
||||
import CodeSample from "@/components/CodeSample";
|
||||
import Link from "next/link";
|
||||
import { jsonLd, paginaMeta, SITE } from "@/lib/seo";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
BookOpen,
|
||||
Cpu,
|
||||
Gauge,
|
||||
Radio,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Wifi,
|
||||
} from "lucide-react";
|
||||
|
||||
export function generateStaticParams() {
|
||||
return [{ locale: "pt" }];
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { locale } = await params;
|
||||
if (locale !== "pt") {
|
||||
return {};
|
||||
}
|
||||
return paginaMeta({
|
||||
titulo: "Microcontroladores ESP32, o livro",
|
||||
descricao:
|
||||
"Conheça o livro Microcontroladores ESP32: conectividade, eficiência e desempenho. 12 partes e 78 capítulos em português sobre arquitetura, energia, rádio, periféricos e firmware.",
|
||||
caminho: "/pt/livro",
|
||||
keywords: [
|
||||
"livro ESP32",
|
||||
"Microcontroladores ESP32",
|
||||
"livro de ESP32 em português",
|
||||
"Lucas Rayan Guerra",
|
||||
"Ciência Embarcada",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const CHAPTERS = [
|
||||
{
|
||||
title: "O ecossistema ESP32",
|
||||
desc: "Como as famílias se organizam, o que muda entre Xtensa e RISC-V e por que a escolha do chip começa pelo problema.",
|
||||
topics: ["Famílias e sufixos", "Critérios de escolha", "Ferramentas de apoio"],
|
||||
icon: Cpu,
|
||||
},
|
||||
{
|
||||
title: "Arquitetura e memória",
|
||||
desc: "Núcleos, caches, SRAM, PSRAM e o caminho entre o código em flash e a execução em alta velocidade.",
|
||||
topics: ["Mapa de memória", "PSRAM na prática", "Caches e latência"],
|
||||
icon: Gauge,
|
||||
},
|
||||
{
|
||||
title: "Energia e autonomia",
|
||||
desc: "Os modos de sono, o custo real do rádio e as decisões de hardware que definem a vida útil da bateria.",
|
||||
topics: ["Deep sleep", "Domínios de energia", "Orçamento de consumo"],
|
||||
icon: Gauge,
|
||||
},
|
||||
{
|
||||
title: "Conectividade",
|
||||
desc: "Wi-Fi, BLE, Thread, Zigbee e Matter: o que cada rádio resolve e onde a arquitetura começa a pesar.",
|
||||
topics: ["Wi-Fi 6 e TWT", "BLE e malhas", "Matter e 802.15.4"],
|
||||
icon: Radio,
|
||||
},
|
||||
{
|
||||
title: "Periféricos e pinagem",
|
||||
desc: "GPIO, ADC, I2S, USB, MIPI e as restrições que só aparecem quando o hardware já está na placa.",
|
||||
topics: ["Pinos de strapping", "ADC e rádio", "USB nativo"],
|
||||
icon: Wifi,
|
||||
},
|
||||
{
|
||||
title: "Do protótipo ao produto",
|
||||
desc: "Firmware, partições, OTA, segurança e os cuidados de engenharia que sobrevivem ao primeiro deploy.",
|
||||
topics: ["ESP-IDF e Arduino", "OTA e rollback", "Secure boot"],
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
];
|
||||
|
||||
const ESP_IDF = `#include "driver/gpio.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#define LED GPIO_NUM_2
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
gpio_reset_pin(LED);
|
||||
gpio_set_direction(LED, GPIO_MODE_OUTPUT);
|
||||
|
||||
while (1) {
|
||||
gpio_set_level(LED, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
gpio_set_level(LED, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
}
|
||||
}`;
|
||||
|
||||
const ARDUINO = `#include <WiFi.h>
|
||||
|
||||
const char *ssid = "SUA_REDE";
|
||||
const char *senha = "SUA_SENHA";
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
WiFi.begin(ssid, senha);
|
||||
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
}
|
||||
|
||||
Serial.println(WiFi.localIP());
|
||||
}
|
||||
|
||||
void loop() {}`;
|
||||
|
||||
const DEEP_SLEEP = `#include "esp_sleep.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
uint64_t intervalo_us = 30ULL * 1000000ULL;
|
||||
esp_sleep_enable_timer_wakeup(intervalo_us);
|
||||
|
||||
ESP_LOGI("energia", "dormindo por 30 segundos");
|
||||
esp_deep_sleep_start();
|
||||
}`;
|
||||
|
||||
const FACTS = [
|
||||
["Idioma", "Português"],
|
||||
["Partes", "12"],
|
||||
["Capítulos", "78"],
|
||||
["Digital", "Amazon Kindle"],
|
||||
["Impresso", "UICLAP"],
|
||||
["ISBN", SITE.livroIsbn],
|
||||
];
|
||||
|
||||
export default async function LivroPage({ params }) {
|
||||
const { locale } = await params;
|
||||
if (locale !== "pt") {
|
||||
notFound();
|
||||
}
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.livro()) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Livro", caminho: "/livro" },
|
||||
])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
<section className="shell pt-8 sm:pt-12">
|
||||
<nav aria-label="Trilha de navegação" className="flex items-center gap-2 text-sm text-muted">
|
||||
<Link href="/" className="transition-colors hover:text-accent">
|
||||
Início
|
||||
</Link>
|
||||
<span aria-hidden="true" className="text-line-strong">/</span>
|
||||
<span aria-current="page" className="text-ink">Livro</span>
|
||||
</nav>
|
||||
|
||||
<div className="mt-7 grid items-center gap-10 lg:grid-cols-[0.84fr_1.16fr] lg:gap-16">
|
||||
<div className="order-1 lg:order-2">
|
||||
<span className="chip chip-accent">
|
||||
<BookOpen className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Livro novo · 1ª edição
|
||||
</span>
|
||||
<h1 className="page-title mt-5 text-[clamp(1.85rem,8vw,2.5rem)] sm:text-[clamp(2.5rem,5.4vw,4.4rem)]">
|
||||
Microcontroladores ESP32
|
||||
</h1>
|
||||
<p className="mt-3 text-lg font-medium text-copper">{SITE.livroSubtitulo}</p>
|
||||
<p className="lede mt-6 max-w-2xl">
|
||||
Um percurso completo pelo ecossistema ESP32 — da arquitetura do chip à implantação
|
||||
de produtos conectados. São 12 partes e 78 capítulos que cobrem Arduino e ESP-IDF,
|
||||
FreeRTOS, Wi-Fi, BLE, Thread e Matter, periféricos, depuração, baixo consumo e segurança.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
|
||||
<a
|
||||
href={SITE.livroAmazon}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-primary w-full sm:w-auto"
|
||||
>
|
||||
<BookOpen className="h-4 w-4" aria-hidden="true" />
|
||||
Comprar digital
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
<a
|
||||
href={SITE.livroUiclap}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-ghost w-full sm:w-auto"
|
||||
>
|
||||
Comprar impresso
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
<Link href="/series" className="inline-flex min-h-11 items-center justify-center gap-1.5 px-2 text-sm font-medium text-accent transition-colors hover:text-accent-strong">
|
||||
Explorar a documentação
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<dl className="mt-9 grid grid-cols-2 gap-2.5 sm:grid-cols-3">
|
||||
{FACTS.map(([label, value]) => (
|
||||
<div key={label} className="rounded-2xl bg-surface-2 px-4 py-3">
|
||||
<dt className="text-[0.6875rem] font-semibold text-muted">{label}</dt>
|
||||
<dd className="mt-0.5 text-sm font-semibold text-ink">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="relative order-2 overflow-hidden rounded-[2rem] bg-[#0b0d10] p-8 ring-1 ring-white/10 sm:rounded-[2.5rem] sm:p-12 lg:order-1">
|
||||
<div
|
||||
className="pointer-events-none absolute left-1/2 top-1/2 -z-0 h-72 w-72 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#2dd4bf]/15 blur-3xl"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative mx-auto w-[min(16rem,76%)]">
|
||||
<BookCover />
|
||||
</div>
|
||||
<div className="relative mt-9 flex items-end justify-between gap-4 border-t border-white/10 pt-4 text-xs text-[#a6afbd]">
|
||||
<span>Lucas Rayan Guerra</span>
|
||||
<span>Ciência Embarcada</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell section" aria-labelledby="sumario-titulo">
|
||||
<div className="grid gap-8 lg:grid-cols-[0.72fr_1.28fr] lg:gap-16">
|
||||
<div>
|
||||
<p className="eyebrow">Sumário</p>
|
||||
<h2 id="sumario-titulo" className="display mt-4 text-[clamp(2rem,4.6vw,3.4rem)]">
|
||||
Uma linha de raciocínio, do chip ao produto.
|
||||
</h2>
|
||||
</div>
|
||||
<div className="lg:pt-2">
|
||||
<p className="lede">
|
||||
A ordem acompanha o projeto real: entender o chip, medir energia, conectar, expor
|
||||
periféricos e sustentar o produto. Os seis eixos abaixo mostram como as 12 partes
|
||||
se organizam.
|
||||
</p>
|
||||
<p className="mt-4 text-sm text-muted">
|
||||
78 capítulos, com exercícios e projetos ao longo do percurso.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{CHAPTERS.map((chapter) => {
|
||||
const Icon = chapter.icon;
|
||||
return (
|
||||
<article key={chapter.title} className="card flex flex-col p-5 sm:p-6">
|
||||
<span className="grid h-11 w-11 place-items-center rounded-2xl bg-accent-soft text-accent">
|
||||
<Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
<h3 className="mt-5 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{chapter.title}</h3>
|
||||
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted">{chapter.desc}</p>
|
||||
<ul className="mt-5 flex flex-wrap gap-1.5">
|
||||
{chapter.topics.map((topic) => (
|
||||
<li key={topic} className="chip">
|
||||
{topic}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-y border-line bg-surface/70" aria-labelledby="codigo-titulo">
|
||||
<div className="shell section">
|
||||
<div className="grid gap-10 lg:grid-cols-[0.7fr_1.3fr] lg:gap-16">
|
||||
<div className="min-w-0 lg:sticky lg:top-28 lg:self-start">
|
||||
<p className="eyebrow">Código</p>
|
||||
<h2 id="codigo-titulo" className="display mt-4 text-[clamp(2rem,4.6vw,3.4rem)]">
|
||||
Exemplos curtos, prontos para a bancada.
|
||||
</h2>
|
||||
<p className="lede mt-4">
|
||||
Os trechos abaixo são originais e servem como ponto de partida. Ajuste o pino, a rede
|
||||
e a frequência ao seu hardware antes de gravar.
|
||||
</p>
|
||||
<div className="mt-6 rounded-2xl border border-line bg-surface p-4">
|
||||
<p className="flex items-center gap-2 text-sm font-semibold text-ink">
|
||||
<Terminal className="h-4 w-4 text-accent" aria-hidden="true" />
|
||||
Sem conteúdo protegido
|
||||
</p>
|
||||
<p className="mt-2 text-xs leading-relaxed text-muted">
|
||||
Esta página apresenta apenas a ficha do livro e exemplos autorais. O texto integral,
|
||||
PDF, EPUB e arquivos intermediários não são distribuídos pelo ESPDocs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 space-y-7">
|
||||
<CodeSample
|
||||
label="Ponto de partida · ESP-IDF"
|
||||
language="C"
|
||||
code={ESP_IDF}
|
||||
note="Pisca o GPIO 2 usando a API atual do ESP-IDF e uma task do FreeRTOS."
|
||||
/>
|
||||
<CodeSample
|
||||
label="Conectividade · Arduino"
|
||||
language="C++"
|
||||
code={ARDUINO}
|
||||
note="Conexão Wi-Fi mínima, com espera ativa e impressão do IP obtido."
|
||||
/>
|
||||
<CodeSample
|
||||
label="Energia · deep sleep"
|
||||
language="C · ESP-IDF"
|
||||
code={DEEP_SLEEP}
|
||||
note="Timer de 30 segundos e entrada no modo de menor consumo disponível."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell section pb-20 md:pb-28">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<p className="eyebrow">Para quem é</p>
|
||||
<h2 className="display mt-4 text-[clamp(2rem,4.5vw,3.2rem)]">
|
||||
Para quem já cansou de juntar pedaços de datasheet.
|
||||
</h2>
|
||||
<p className="lede mx-auto mt-4 max-w-2xl">
|
||||
Estudantes, makers e engenheiros de firmware que querem uma visão coerente do
|
||||
ecossistema — sem abandonar a fonte oficial.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||||
<a
|
||||
href={SITE.livroAmazon}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-primary w-full sm:w-auto"
|
||||
>
|
||||
<BookOpen className="h-4 w-4" aria-hidden="true" />
|
||||
Comprar digital
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
<a
|
||||
href={SITE.livroUiclap}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-ghost w-full sm:w-auto"
|
||||
>
|
||||
Comprar impresso
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, BookOpen, Compass, Scale, SlidersHorizontal } from "lucide-react";
|
||||
|
||||
export const metadata = {
|
||||
title: "Página não encontrada",
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
const CAMINHOS = [
|
||||
{ href: "/series", icon: Compass, titulo: "Séries ESP32", texto: "As 12 famílias, com pinagem e especificações." },
|
||||
{ href: "/seletor", icon: SlidersHorizontal, titulo: "Seletor de chip", texto: "Responda a oito perguntas e receba os modelos indicados." },
|
||||
{ href: "/comparacao", icon: Scale, titulo: "Comparador", texto: "Até quatro séries lado a lado." },
|
||||
{ href: "/glossario", icon: BookOpen, titulo: "Glossário", texto: "Termos de hardware explicados em português." },
|
||||
];
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main id="conteudo" className="shell section">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<p className="eyebrow justify-center">Erro 404</p>
|
||||
<h1 className="page-title mt-4">Essa página não existe.</h1>
|
||||
<p className="lede mx-auto mt-5 max-w-xl">
|
||||
O endereço pode ter mudado ou estar digitado errado. Escolha um destes caminhos para continuar.
|
||||
</p>
|
||||
<div className="mt-8 flex justify-center">
|
||||
<Link href="/" className="btn btn-primary">
|
||||
Voltar ao início
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="mx-auto mt-14 grid max-w-4xl gap-3 sm:grid-cols-2">
|
||||
{CAMINHOS.map(({ href, icon: Icon, titulo, texto }) => (
|
||||
<li key={href}>
|
||||
<Link href={href} className="card card-interactive group flex h-full items-start gap-4 p-5">
|
||||
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-accent-soft text-accent">
|
||||
<Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
<span>
|
||||
<span className="block font-semibold text-ink">{titulo}</span>
|
||||
<span className="mt-0.5 block text-sm text-muted">{texto}</span>
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import BookCover from "@/components/BookCover";
|
||||
import Link from "next/link";
|
||||
import seriesData from "@/public/series.json";
|
||||
import frameworksData from "@/public/frameworks.json";
|
||||
import { SITE } from "@/lib/seo";
|
||||
import { getLocalizedHref } from "@/components/LanguageSelector";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
BookOpen,
|
||||
Box,
|
||||
Cpu,
|
||||
Scale,
|
||||
SlidersHorizontal,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
|
||||
const FEATURED = ["ESP32", "ESP32-S3", "ESP32-C6", "ESP32-C3", "ESP32-P4", "ESP32-H2"];
|
||||
|
||||
const TOOLS = [
|
||||
{
|
||||
href: "/seletor",
|
||||
title: "Seletor inteligente",
|
||||
desc: "Responda oito perguntas sobre o projeto e receba os chips mais compatíveis, com justificativa técnica.",
|
||||
icon: SlidersHorizontal,
|
||||
action: "Encontrar meu chip",
|
||||
},
|
||||
{
|
||||
href: "/comparacao",
|
||||
title: "Comparador de séries",
|
||||
desc: "Coloque até quatro modelos lado a lado e compare núcleos, rádio, memória, periféricos e segurança.",
|
||||
icon: Scale,
|
||||
action: "Comparar agora",
|
||||
},
|
||||
{
|
||||
href: "/diagnostico",
|
||||
title: "Diagnóstico de falhas",
|
||||
desc: "Causas e correções para brownout, bootloop, Guru Meditation, watchdog e falhas de flash.",
|
||||
icon: AlertTriangle,
|
||||
action: "Abrir diagnóstico",
|
||||
},
|
||||
{
|
||||
href: "/componentes",
|
||||
title: "Component Registry",
|
||||
desc: "Drivers, bibliotecas e BSPs do registro oficial da Espressif, com manifesto pronto para o projeto.",
|
||||
icon: Box,
|
||||
action: "Explorar componentes",
|
||||
},
|
||||
{
|
||||
href: "/frameworks",
|
||||
title: "Frameworks e SDKs",
|
||||
desc: "ESP-IDF, Arduino, Rust, Matter, MicroPython e as ferramentas que ficam em volta do chip.",
|
||||
icon: Terminal,
|
||||
action: "Ver frameworks",
|
||||
},
|
||||
{
|
||||
href: "/catalogo",
|
||||
title: "Catálogo de placas",
|
||||
desc: "DevKits e módulos reais, com interface USB, sensores integrados e link para a loja oficial.",
|
||||
icon: Cpu,
|
||||
action: "Ver placas",
|
||||
},
|
||||
];
|
||||
|
||||
const ERRORS = [
|
||||
{
|
||||
code: "Brownout detector was triggered",
|
||||
cause: "Queda de tensão na inicialização do rádio.",
|
||||
fix: "Capacitor de 10–100 µF junto ao chip e fonte de 5 V com corrente suficiente.",
|
||||
},
|
||||
{
|
||||
code: "Guru Meditation Error",
|
||||
cause: "Ponteiro inválido, stack overflow ou uso incorreto de interrupção.",
|
||||
fix: "Leia o backtrace com addr2line e dimensione a stack da task no FreeRTOS.",
|
||||
},
|
||||
{
|
||||
code: "ADC2 indisponível com Wi-Fi",
|
||||
cause: "O controlador de rádio reserva o ADC2 enquanto está ativo.",
|
||||
fix: "Use os pinos do ADC1 para sensores analógicos que convivem com o Wi-Fi.",
|
||||
},
|
||||
{
|
||||
code: "Flash Read err / bootloader",
|
||||
cause: "Tensão ou modo de flash incorretos, ou pino de strapping preso no boot.",
|
||||
fix: "Confira 3,3 V, pull-ups de boot e a configuração de flash no esptool.",
|
||||
},
|
||||
];
|
||||
|
||||
export default async function Home({ params }) {
|
||||
const { locale } = await params;
|
||||
const isPt = locale === "pt";
|
||||
const getLinkHref = (href) => getLocalizedHref(`/${locale}${href}`, locale);
|
||||
|
||||
const seriesCount = Object.keys(seriesData).length;
|
||||
const frameworkCount = Object.keys(frameworksData).length;
|
||||
const featured = FEATURED.map((key) => [key, seriesData[key]]).filter(([, serie]) => Boolean(serie));
|
||||
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
<section className="shell pb-10 pt-14 sm:pt-20 lg:pb-14 lg:pt-24">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<span className="chip chip-accent">
|
||||
<Cpu className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Documentação aberta do ESP32
|
||||
</span>
|
||||
<h1 className="display mt-6 text-[clamp(2.9rem,7.4vw,5.4rem)]">
|
||||
O ESP32,
|
||||
<br className="sm:hidden" /> sem ruído.
|
||||
</h1>
|
||||
<p className="lede mx-auto mt-6 max-w-2xl">
|
||||
Especificações, pinagem, comparações e diagnóstico em uma leitura só.
|
||||
Menos tradução de datasheet, mais tempo com o protótipo na bancada.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||||
<Link href={getLinkHref("/seletor")} className="btn btn-primary w-full sm:w-auto">
|
||||
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
|
||||
Escolher meu chip
|
||||
</Link>
|
||||
<Link href={getLinkHref("/series")} className="btn btn-ghost w-full sm:w-auto">
|
||||
Explorar as {seriesCount} séries
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<p className="mt-7 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-sm text-muted">
|
||||
<span><strong className="font-semibold text-ink">{seriesCount}</strong> séries</span>
|
||||
<span aria-hidden="true" className="text-line-strong">·</span>
|
||||
<span><strong className="font-semibold text-ink">{frameworkCount}</strong> frameworks</span>
|
||||
{isPt && (
|
||||
<>
|
||||
<span aria-hidden="true" className="text-line-strong">·</span>
|
||||
<span><strong className="font-semibold text-ink">78</strong> capítulos no livro</span>
|
||||
<span aria-hidden="true" className="text-line-strong">·</span>
|
||||
<span>100% em português</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="panel mt-12 overflow-hidden sm:mt-16">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 px-5 py-4 sm:px-7">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-ink">Séries em destaque</h2>
|
||||
<p className="mt-0.5 text-xs text-muted">O essencial de cada família, antes de abrir o datasheet.</p>
|
||||
</div>
|
||||
<Link href="/series" className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent transition-colors hover:text-accent-strong">
|
||||
Índice completo
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-px border-t border-line bg-line sm:grid-cols-2 lg:grid-cols-3">
|
||||
{featured.map(([key, serie]) => (
|
||||
<Link
|
||||
key={key}
|
||||
href={`/series/${key}`}
|
||||
className="group flex min-w-0 flex-col bg-surface p-5 transition-colors duration-300 hover:bg-surface-2 sm:p-6"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-display text-xl font-semibold tracking-[-0.035em] text-ink">{key}</h3>
|
||||
<p className="mt-1 truncate text-xs text-muted">{serie.arquitetura}</p>
|
||||
</div>
|
||||
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-accent-soft text-accent transition-transform duration-300 group-hover:scale-105">
|
||||
<Cpu className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-5 line-clamp-2 text-sm leading-relaxed text-muted">{serie.descricao}</p>
|
||||
<div className="mt-5 flex items-center justify-between gap-3 border-t border-line pt-3.5">
|
||||
<span className="truncate font-mono text-[0.6875rem] text-ink-soft">{serie.frequencia}</span>
|
||||
<span className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent">
|
||||
Ficha
|
||||
<ArrowUpRight className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{isPt && (
|
||||
<section className="shell section pt-8 sm:pt-12" aria-labelledby="livro-titulo">
|
||||
<div className="relative isolate overflow-hidden rounded-[2rem] bg-[#0b0d10] ring-1 ring-white/10 dark:bg-[#05070a] dark:ring-white/15 sm:rounded-[2.5rem]">
|
||||
<div
|
||||
className="pointer-events-none absolute -left-24 top-1/2 -z-10 h-80 w-80 -translate-y-1/2 rounded-full bg-[#2dd4bf]/15 blur-3xl"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="grid lg:grid-cols-[0.82fr_1.18fr]">
|
||||
<div className="order-1 flex flex-col justify-center p-7 sm:p-10 lg:order-2 lg:p-14">
|
||||
<span className="inline-flex w-fit items-center gap-2 rounded-full border border-white/15 bg-white/5 px-3 py-1 text-xs font-semibold text-[#75cdc1]">
|
||||
<BookOpen className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Livro novo
|
||||
</span>
|
||||
<h2 id="livro-titulo" className="mt-5 font-display text-[clamp(2rem,4.2vw,3.2rem)] font-semibold leading-[1.05] tracking-[-0.04em] text-[#f4f6f8]">
|
||||
{SITE.livroTitulo}
|
||||
</h2>
|
||||
<p className="mt-2 text-base text-[#a6afbd]">{SITE.livroSubtitulo}</p>
|
||||
<p className="mt-5 max-w-xl text-sm leading-relaxed text-[#c4cbd5]">
|
||||
Um percurso completo pelo hardware e pelo firmware do ESP32: da arquitetura do chip
|
||||
à implantação de produtos conectados. São 12 partes e 78 capítulos sobre Arduino,
|
||||
ESP-IDF, FreeRTOS, Wi-Fi, BLE, Thread e Matter, periféricos, depuração, baixo consumo
|
||||
e segurança.
|
||||
</p>
|
||||
|
||||
<p className="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-xs text-[#a6afbd]">
|
||||
<span>12 partes</span>
|
||||
<span aria-hidden="true" className="text-white/20">·</span>
|
||||
<span>78 capítulos</span>
|
||||
<span aria-hidden="true" className="text-white/20">·</span>
|
||||
<span>ISBN {SITE.livroIsbn}</span>
|
||||
</p>
|
||||
|
||||
<div className="mt-7 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
|
||||
<a
|
||||
href={SITE.livroAmazon}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex min-h-11 items-center justify-center gap-2 rounded-full bg-[#2dd4bf] px-5 text-sm font-semibold text-[#06201d] transition-colors duration-300 hover:bg-[#5eead4]"
|
||||
>
|
||||
Comprar digital
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
<a
|
||||
href={SITE.livroUiclap}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex min-h-11 items-center justify-center gap-2 rounded-full border border-white/20 px-5 text-sm font-semibold text-[#f4f6f8] transition-colors duration-300 hover:border-white/40 hover:bg-white/5"
|
||||
>
|
||||
Comprar impresso
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
<Link href="/pt/livro" className="inline-flex min-h-11 items-center justify-center gap-1.5 rounded-full px-3 text-sm font-medium text-[#a6afbd] transition-colors duration-300 hover:text-[#f4f6f8]">
|
||||
Ficha completa
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="order-2 flex items-center justify-center border-t border-white/10 p-8 sm:p-12 lg:order-1 lg:border-r lg:border-t-0 lg:p-14">
|
||||
<div className="relative w-[min(15.5rem,68%)] lg:w-[min(17rem,100%)]">
|
||||
<div className="absolute -inset-10 -z-10 rounded-full bg-[#2dd4bf]/10 blur-3xl" aria-hidden="true" />
|
||||
<BookCover />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="shell section" aria-labelledby="ferramentas-titulo">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<p className="eyebrow">Ferramentas</p>
|
||||
<h2 id="ferramentas-titulo" className="display mt-4 text-[clamp(2rem,4.8vw,3.4rem)]">
|
||||
Comece pelo problema.
|
||||
</h2>
|
||||
<p className="lede mt-4">
|
||||
Cada ferramenta resolve uma etapa concreta do projeto, sem cadastro e sem rodeio.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{TOOLS.map((tool) => {
|
||||
const Icon = tool.icon;
|
||||
return (
|
||||
<Link key={tool.href} href={tool.href} className="card card-interactive group flex flex-col p-5 sm:p-6">
|
||||
<span className="grid h-11 w-11 place-items-center rounded-2xl bg-accent-soft text-accent">
|
||||
<Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
<h3 className="mt-5 font-display text-lg font-semibold tracking-[-0.025em] text-ink">{tool.title}</h3>
|
||||
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted">{tool.desc}</p>
|
||||
<span className="mt-5 inline-flex items-center gap-1.5 text-sm font-medium text-accent">
|
||||
{tool.action}
|
||||
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-0.5" aria-hidden="true" />
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell section pt-0" aria-labelledby="diagnostico-titulo">
|
||||
<div className="grid items-start gap-10 lg:grid-cols-[0.78fr_1.22fr] lg:gap-16">
|
||||
<div className="lg:sticky lg:top-28">
|
||||
<p className="eyebrow">Diagnóstico</p>
|
||||
<h2 id="diagnostico-titulo" className="display mt-4 text-[clamp(2rem,4.8vw,3.4rem)]">
|
||||
O primeiro boot não precisa virar noite.
|
||||
</h2>
|
||||
<p className="lede mt-4">
|
||||
Os erros mais comuns têm causa conhecida. O guia parte da mensagem no terminal e chega ao ajuste de hardware ou firmware.
|
||||
</p>
|
||||
<Link href="/diagnostico" className="btn btn-accent mt-7">
|
||||
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
|
||||
Abrir os 30+ diagnósticos
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="panel-dark min-w-0 overflow-hidden rounded-[1.75rem]">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-white/10 px-5 py-4 sm:px-6">
|
||||
<span className="inline-flex items-center gap-2 font-mono text-xs text-[#75cdc1]">
|
||||
<Terminal className="h-4 w-4" aria-hidden="true" />
|
||||
boot.log
|
||||
</span>
|
||||
<span className="text-xs text-[#a6afbd]">4 exemplos de 30+</span>
|
||||
</div>
|
||||
<div className="divide-y divide-white/10">
|
||||
{ERRORS.map((item) => (
|
||||
<Link key={item.code} href="/diagnostico" className="group grid gap-2 px-5 py-5 transition-colors duration-300 hover:bg-white/[0.035] sm:px-6">
|
||||
<span className="break-words font-mono text-[0.75rem] font-medium leading-relaxed text-[#ffb39b]">{item.code}</span>
|
||||
<span className="text-sm font-medium text-[#f4f6f8]">{item.cause}</span>
|
||||
<span className="text-xs leading-relaxed text-[#a6afbd]">{item.fix}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="shell pb-20 pt-4 md:pb-28">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<p className="eyebrow">Curadoria independente</p>
|
||||
<h2 className="display mt-4 text-[clamp(2rem,4.5vw,3.2rem)]">
|
||||
Feito para consultar no meio do projeto.
|
||||
</h2>
|
||||
<p className="lede mx-auto mt-4 max-w-2xl">
|
||||
O ESPDocs organiza dados públicos da Espressif e aponta sempre para a fonte oficial.
|
||||
Para decisões críticas, o datasheet e o Technical Reference Manual continuam sendo a palavra final.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||||
<Link href="/glossario" className="btn btn-ghost w-full sm:w-auto">
|
||||
<BookOpen className="h-4 w-4" aria-hidden="true" />
|
||||
Glossário
|
||||
</Link>
|
||||
<Link href="/sobre" className="btn btn-primary w-full sm:w-auto">
|
||||
Sobre o projeto
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { paginaMeta, jsonLd } from "@/lib/seo";
|
||||
|
||||
export const metadata = paginaMeta({
|
||||
titulo: "Qual ESP32 escolher",
|
||||
descricao: "Responda a perguntas simples sobre conectividade (Wi-Fi 6, 5GHz, Matter), energia, periféricos e segurança ou use o filtro paramétrico para encontrar o ESP32 perfeito.",
|
||||
caminho: "/seletor",
|
||||
keywords: ["qual ESP32 escolher", "qual ESP32 usar", "escolher microcontrolador", "ESP32 para meu projeto", "comparar ESP32"],
|
||||
});
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
pergunta: "Qual ESP32 escolher para iniciantes?",
|
||||
resposta: "Para quem está começando no mundo Maker e IoT, o ESP32 clássico (NodeMCU/DevKitC) ou o ESP32-C3 são as melhores opções devido à vasta quantidade de bibliotecas, exemplos prontos no Arduino IDE e farta documentação em português.",
|
||||
},
|
||||
{
|
||||
pergunta: "Qual ESP32 tem suporte a Wi-Fi 6, Bluetooth 6 e Thread/Zigbee/Matter?",
|
||||
resposta: "O ESP32-C6 e o ESP32-C5 são as melhores séries para redes modernas. O ESP32-C6 oferece Wi-Fi 6 (2.4 GHz) e rádio 802.15.4 (Zigbee 3.0 / Thread / Matter). Já o ESP32-C5 é o primeiro a oferecer suporte a Wi-Fi 6 Dual-Band operando em 2.4 GHz e 5 GHz simultaneamente.",
|
||||
},
|
||||
{
|
||||
pergunta: "Qual o ESP32 mais potente para Inteligência Artificial (TinyML) e Displays?",
|
||||
resposta: "O ESP32-P4 (dual-core RISC-V a 400 MHz com extensões vetoriais de IA, interfaces MIPI-CSI/DSI para câmeras/telas e até 64 MB de PSRAM) e o ESP32-S3 (dual-core Xtensa a 240 MHz com aceleradores vetoriais para redes neurais e telas LCD RGB) são os mais velozes para IA na borda e interfaces gráficas ricas.",
|
||||
},
|
||||
{
|
||||
pergunta: "Qual ESP32 consome menos energia para projetos a bateria?",
|
||||
resposta: "Microcontroladores com arquitetura RISC-V compacta e coprocessador ULP, como o ESP32-C2, ESP32-C3 e ESP32-H2 (focado em 802.15.4 sem rádio Wi-Fi), oferecem correntes de deep sleep extremamente baixas (na faixa de 5 µA a 10 µA).",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.aplicacaoWeb({
|
||||
nome: "Seletor Inteligente de ESP32",
|
||||
descricao: "Ferramenta interativa que analisa conectividade, consumo, periféricos e segurança para indicar a série ideal de ESP32.",
|
||||
caminho: "/seletor",
|
||||
})
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Seletor de ESP32", caminho: "/seletor" },
|
||||
])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(jsonLd.perguntas(faqs)),
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,488 @@
|
||||
import { paginaMeta, jsonLd } from "@/lib/seo";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import { notFound } from "next/navigation";
|
||||
import seriesData from "@/public/series.json";
|
||||
import ConnectionsDiagram from "@/components/ConnectionsDiagram";
|
||||
import SeriesTabMenu from "@/components/SeriesTabMenu";
|
||||
import PinRestrictions from "@/components/PinRestrictions";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
Battery,
|
||||
BookOpen,
|
||||
Code,
|
||||
Cpu,
|
||||
Database,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
Lightbulb,
|
||||
Package,
|
||||
Scale,
|
||||
ShieldCheck,
|
||||
Sliders,
|
||||
Thermometer,
|
||||
Wifi,
|
||||
} from "lucide-react";
|
||||
|
||||
export default async function SerieDetail({ params }) {
|
||||
const { key } = await params;
|
||||
const serie = seriesData[key];
|
||||
|
||||
if (!serie) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
let conexoes = [];
|
||||
try {
|
||||
conexoes = require(`@/public/conexoes/${key}.json`);
|
||||
} catch (error) {
|
||||
console.log(`Arquivo de conexões não encontrado para ${key}`);
|
||||
}
|
||||
|
||||
const seriesKeys = Object.keys(seriesData);
|
||||
const currentIndex = seriesKeys.indexOf(key);
|
||||
const previousKey = currentIndex > 0 ? seriesKeys[currentIndex - 1] : null;
|
||||
const nextKey = currentIndex < seriesKeys.length - 1 ? seriesKeys[currentIndex + 1] : null;
|
||||
|
||||
const processadorSpecs = [
|
||||
{ label: "Arquitetura", value: serie.arquitetura },
|
||||
{ label: "Núcleos", value: serie.nucleos },
|
||||
{ label: "Frequência", value: serie.frequencia },
|
||||
{ label: "Coprocessador ULP", value: serie.coprocessador_ulp || "Não" },
|
||||
{ label: "Aceleradores IA", value: serie.aceleradores_ia || "Não" },
|
||||
];
|
||||
|
||||
const seg = serie.seguranca || {};
|
||||
const segurancaSpecs = [
|
||||
{ label: "AES", value: seg.aes || "Não" },
|
||||
{ label: "SHA", value: seg.sha || "Não" },
|
||||
{ label: "RSA / MPI", value: seg.rsa || "Não" },
|
||||
{ label: "ECC (curvas elípticas)", value: seg.ecc || "Não" },
|
||||
{ label: "ECDSA em hardware", value: seg.ecdsa || "Não" },
|
||||
{ label: "HMAC", value: seg.hmac || "Não" },
|
||||
{ label: "Assinatura Digital (DS)", value: seg.assinatura_digital || "Não" },
|
||||
{ label: "Key Manager", value: seg.key_manager || "Não" },
|
||||
{ label: "Gerador aleatório (RNG)", value: seg.rng || "Não" },
|
||||
{ label: "Criptografia de flash", value: seg.criptografia_flash || "Não" },
|
||||
{ label: "Secure Boot", value: seg.secure_boot || "Não" },
|
||||
{ label: "Proteção contra DPA", value: seg.protecao_dpa || "Não" },
|
||||
];
|
||||
|
||||
const ferramentasSpecs = [
|
||||
{ label: "ESP-IDF mínimo", value: serie.esp_idf_minimo || "Não informado" },
|
||||
{ label: "Core Arduino", value: serie.arduino_core || "Não informado" },
|
||||
{ label: "Depuração", value: serie.depuracao || "Não informado" },
|
||||
{ label: "Encapsulamento", value: serie.encapsulamento || "Não informado" },
|
||||
{ label: "Módulos disponíveis", value: serie.modulos || "Não informado" },
|
||||
];
|
||||
|
||||
const conectividadeSpecs = [
|
||||
{ label: "Wi-Fi", value: serie.wifi || "Não" },
|
||||
{ label: "Bluetooth", value: serie.bluetooth || "Não" },
|
||||
{ label: "Zigbee/Thread", value: serie.zigbee_thread || "Não" },
|
||||
{ label: "Matter", value: serie.matter || "Não" },
|
||||
{ label: "Ethernet", value: serie.ethernet || serie.ethernet_mac || "Não" },
|
||||
{ label: "CAN", value: serie.can || "Não" },
|
||||
];
|
||||
|
||||
const memoriaSpecs = [
|
||||
{ label: "SRAM", value: serie.memoria_sram },
|
||||
{ label: "SRAM RTC", value: serie.memoria_sram_rtc },
|
||||
{ label: "ROM", value: serie.memoria_rom },
|
||||
{ label: "Flash Externa", value: serie.flash_externa },
|
||||
{ label: "PSRAM Externa", value: serie.psram_externa || "Não" },
|
||||
];
|
||||
|
||||
const perifericos = [
|
||||
{ label: "GPIO", value: serie.gpio },
|
||||
{ label: "ADC", value: serie.adc },
|
||||
{ label: "DAC", value: serie.dac || "Não" },
|
||||
{ label: "Touch", value: serie.touch || "Não" },
|
||||
{ label: "UART", value: serie.uart },
|
||||
{ label: "SPI", value: serie.spi },
|
||||
{ label: "I2C", value: serie.i2c },
|
||||
{ label: "I2S", value: serie.i2s },
|
||||
{ label: "PWM", value: serie.pwm },
|
||||
{ label: "USB", value: serie.usb || "Não" },
|
||||
];
|
||||
|
||||
const interfacesEspeciais = [];
|
||||
if (serie.lcd) interfacesEspeciais.push({ label: "LCD", value: serie.lcd });
|
||||
if (serie.camera) interfacesEspeciais.push({ label: "Câmera", value: serie.camera });
|
||||
if (serie.mipi_csi) interfacesEspeciais.push({ label: "MIPI CSI", value: serie.mipi_csi });
|
||||
if (serie.mipi_dsi) interfacesEspeciais.push({ label: "MIPI DSI", value: serie.mipi_dsi });
|
||||
if (serie.sdio) interfacesEspeciais.push({ label: "SDIO", value: serie.sdio });
|
||||
if (serie.sensor_hall) interfacesEspeciais.push({ label: "Sensor Hall", value: serie.sensor_hall });
|
||||
if (serie.sensor_temperatura) interfacesEspeciais.push({ label: "Sensor Temperatura", value: serie.sensor_temperatura });
|
||||
|
||||
// Consumo de Energia
|
||||
const consumoEnergia = [];
|
||||
if (serie.consumo_energia) {
|
||||
Object.entries(serie.consumo_energia).forEach(([modo, consumo]) => {
|
||||
const modoLabels = {
|
||||
'active': 'Modo Ativo',
|
||||
'active_cpu_240mhz': 'Ativo (CPU 240MHz)',
|
||||
'active_cpu_160mhz': 'Ativo (CPU 160MHz)',
|
||||
'active_wifi_tx': 'Ativo (Wi-Fi TX)',
|
||||
'active_wifi_tx_peak': 'Ativo (Wi-Fi TX Pico)',
|
||||
'active_wifi': 'Ativo (Wi-Fi)',
|
||||
'active_wifi6': 'Ativo (Wi-Fi 6)',
|
||||
'modem_sleep': 'Modem Sleep',
|
||||
'modem_sleep_cpu_160mhz': 'Modem Sleep (CPU 160MHz)',
|
||||
'light_sleep': 'Light Sleep',
|
||||
'deep_sleep': 'Deep Sleep',
|
||||
'hibernation': 'Hibernação'
|
||||
};
|
||||
|
||||
consumoEnergia.push({
|
||||
label: modoLabels[modo] || modo.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
||||
value: consumo
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: "devboard",
|
||||
label: "Placa de desenvolvimento",
|
||||
available: serie.placa,
|
||||
content: (
|
||||
<div className="card p-5 sm:p-7">
|
||||
<h2 className="text-lg font-semibold text-ink">Kit de desenvolvimento oficial</h2>
|
||||
<div className="mt-5 overflow-hidden rounded-xl border border-line bg-surface-2 p-4">
|
||||
<Image
|
||||
src={serie.placa}
|
||||
alt={`Placa de desenvolvimento ${key}`}
|
||||
width={1200}
|
||||
height={800}
|
||||
className="mx-auto h-auto max-h-[28rem] w-full rounded-lg object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-muted">Placa padrão da Espressif para a série {key}.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "connections",
|
||||
label: "Diagrama de conexões",
|
||||
available: conexoes.length > 0,
|
||||
content: (
|
||||
<div className="card p-5 sm:p-7">
|
||||
<div className="callout callout-warn flex items-start gap-3">
|
||||
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
|
||||
<div>
|
||||
<p className="font-semibold text-ink">Este é o diagrama do chip, não da placa</p>
|
||||
<p className="mt-1">
|
||||
O mapa mostra as conexões do silício do <strong>{key}</strong>. Cada placa de desenvolvimento
|
||||
expõe só parte delas, ou as roteia de outra forma.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<ConnectionsDiagram connections={conexoes} serie={serie} />
|
||||
</div>
|
||||
|
||||
<div className="callout mt-8">
|
||||
<p className="flex items-center gap-2 font-semibold text-ink">
|
||||
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
|
||||
Quase todo pino é reconfigurável
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
A matriz de IO do {key} permite mover SPI, I2C, UART, PWM e barramentos de câmera ou display
|
||||
para a maioria dos pinos digitais. Para as restrições de strapping e dos pinos analógicos,
|
||||
consulte o{" "}
|
||||
<a
|
||||
href={`https://docs.espressif.com/projects/esp-idf/en/latest/${String(key).toLowerCase().replace("-", "")}/api-reference/peripherals/gpio.html`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 font-medium text-accent underline decoration-accent/30 underline-offset-2 hover:decoration-accent"
|
||||
>
|
||||
guia oficial de GPIO
|
||||
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const secoes = [
|
||||
{ id: "processador", titulo: "Processador", icon: Cpu, specs: processadorSpecs },
|
||||
{ id: "conectividade", titulo: "Conectividade", icon: Wifi, specs: conectividadeSpecs },
|
||||
{ id: "ferramentas", titulo: "Ferramentas e disponibilidade", icon: Package, specs: ferramentasSpecs },
|
||||
{ id: "seguranca", titulo: "Segurança", icon: ShieldCheck, specs: segurancaSpecs },
|
||||
{ id: "memoria", titulo: "Memória", icon: Database, specs: memoriaSpecs },
|
||||
...(consumoEnergia.length > 0
|
||||
? [{
|
||||
id: "energia",
|
||||
titulo: "Consumo de energia",
|
||||
icon: Battery,
|
||||
specs: consumoEnergia,
|
||||
nota: "Valores típicos a 3,3 V e 25 °C. O consumo real depende do ciclo de trabalho do rádio e da frequência de clock configurada.",
|
||||
}]
|
||||
: []),
|
||||
{ id: "perifericos", titulo: "Periféricos", icon: Sliders, specs: perifericos },
|
||||
...(interfacesEspeciais.length > 0
|
||||
? [{ id: "interfaces", titulo: "Interfaces especiais", icon: Sliders, specs: interfacesEspeciais }]
|
||||
: []),
|
||||
{
|
||||
id: "temperatura",
|
||||
titulo: "Limites de temperatura",
|
||||
icon: Thermometer,
|
||||
specs: [
|
||||
{ label: "Operação", value: serie.temperatura_operacao },
|
||||
{ label: "Armazenamento", value: serie.temperatura_armazenamento },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fatos = [
|
||||
{ label: "Processador", value: String(serie.nucleos).includes("2") ? "Dual-core" : "Single-core", detail: serie.frequencia },
|
||||
{ label: "SRAM", value: serie.memoria_sram, detail: `+ ${serie.memoria_sram_rtc} de SRAM RTC` },
|
||||
{ label: "GPIO", value: serie.gpio, detail: "pinos programáveis" },
|
||||
{ label: "Arquitetura", value: String(serie.arquitetura).includes("RISC-V") ? "RISC-V" : "Xtensa", detail: serie.arquitetura },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.serie(key, serie)) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Séries", caminho: "/series" },
|
||||
{ nome: serie.nome_completo, caminho: `/series/${key}` },
|
||||
])),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
<PageHero
|
||||
trail={[{ label: "Séries", href: "/series" }, { label: key }]}
|
||||
icon={Cpu}
|
||||
label={serie.arquitetura}
|
||||
title={key}
|
||||
lede={serie.descricao}
|
||||
>
|
||||
{serie.datasheet ? (
|
||||
<a href={serie.datasheet} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
|
||||
<FileText className="h-4 w-4" aria-hidden="true" />
|
||||
Datasheet oficial
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
) : (
|
||||
<span className="btn btn-ghost cursor-not-allowed opacity-70" aria-disabled="true">
|
||||
<FileText className="h-4 w-4" aria-hidden="true" />
|
||||
Datasheet ainda não publicado
|
||||
</span>
|
||||
)}
|
||||
{serie.guia_de_programacao && (
|
||||
<a href={serie.guia_de_programacao} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
|
||||
<Code className="h-4 w-4" aria-hidden="true" />
|
||||
Guia de programação
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
{serie.manual_tecnico && (
|
||||
<a href={serie.manual_tecnico} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
|
||||
<BookOpen className="h-4 w-4" aria-hidden="true" />
|
||||
Manual técnico
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
</PageHero>
|
||||
|
||||
<section className="shell mt-10" aria-label="Resumo">
|
||||
<dl className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
{fatos.map((fato) => (
|
||||
<div key={fato.label} className="tile">
|
||||
<dt className="text-xs font-medium text-muted">{fato.label}</dt>
|
||||
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{fato.value}</dd>
|
||||
<dd className="mt-0.5 text-xs text-muted">{fato.detail}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
|
||||
{serie.status_documentacao && (
|
||||
<div className="callout callout-warn mt-4 flex items-start gap-3">
|
||||
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
|
||||
<div>
|
||||
<p className="font-semibold text-ink">Documentação preliminar</p>
|
||||
<p className="mt-1">{serie.status_documentacao}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className="shell mt-10">
|
||||
<SeriesTabMenu tabs={tabs} />
|
||||
<PinRestrictions conexoes={conexoes} serieKey={key} />
|
||||
</div>
|
||||
|
||||
<div className="shell mt-12">
|
||||
<nav
|
||||
aria-label="Seções da ficha"
|
||||
className="nav-pill no-scrollbar sticky top-20 z-30 flex gap-1 overflow-x-auto rounded-full p-1.5"
|
||||
>
|
||||
{secoes.map((secao) => (
|
||||
<a key={secao.id} href={`#${secao.id}`} className="nav-link shrink-0">
|
||||
{secao.titulo}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="mt-8 space-y-5">
|
||||
{secoes.map((secao) => (
|
||||
<SpecSection key={secao.id} {...secao} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">Pronto para prototipar o {key}?</h2>
|
||||
<p className="mt-3 text-base text-muted">
|
||||
Veja as placas de desenvolvimento disponíveis para a série, com USB, sensores integrados e link para a loja oficial.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/catalogo" className="btn btn-primary shrink-0">
|
||||
Ver placas
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav aria-label="Outras séries" className="mt-6 grid gap-3 sm:grid-cols-[1fr_auto_1fr] sm:items-center">
|
||||
{previousKey ? (
|
||||
<Link href={`/series/${previousKey}`} className="card card-interactive group flex items-center gap-3 px-5 py-4">
|
||||
<ArrowLeft className="h-4 w-4 text-muted transition-transform group-hover:-translate-x-1" aria-hidden="true" />
|
||||
<span>
|
||||
<span className="block text-xs text-muted">Série anterior</span>
|
||||
<span className="block font-display text-lg font-semibold text-ink">{previousKey}</span>
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span className="hidden sm:block" />
|
||||
)}
|
||||
<Link href="/comparacao" className="btn btn-ghost justify-self-center">
|
||||
<Scale className="h-4 w-4" aria-hidden="true" />
|
||||
Comparar séries
|
||||
</Link>
|
||||
{nextKey ? (
|
||||
<Link href={`/series/${nextKey}`} className="card card-interactive group flex items-center justify-end gap-3 px-5 py-4 text-right">
|
||||
<span>
|
||||
<span className="block text-xs text-muted">Próxima série</span>
|
||||
<span className="block font-display text-lg font-semibold text-ink">{nextKey}</span>
|
||||
</span>
|
||||
<ArrowRight className="h-4 w-4 text-muted transition-transform group-hover:translate-x-1" aria-hidden="true" />
|
||||
</Link>
|
||||
) : (
|
||||
<span className="hidden sm:block" />
|
||||
)}
|
||||
</nav>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SpecSection({ id, titulo, icon: Icon, specs, nota }) {
|
||||
return (
|
||||
<section id={id} aria-labelledby={`${id}-titulo`} className="card scroll-mt-36 p-5 sm:p-7">
|
||||
<h2 id={`${id}-titulo`} className="flex items-center gap-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
||||
<span className="grid h-9 w-9 place-items-center rounded-full bg-accent-soft text-accent">
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
{titulo}
|
||||
</h2>
|
||||
{nota && <p className="mt-3 text-sm text-muted">{nota}</p>}
|
||||
<dl className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-3">
|
||||
{specs.map((spec) => {
|
||||
const negativo = /^n[ãa]o\b/i.test(String(spec.value));
|
||||
return (
|
||||
<div key={spec.label} className="tile">
|
||||
<dt className="text-xs font-medium text-muted">{spec.label}</dt>
|
||||
<dd className={`mt-1 text-sm font-semibold leading-snug ${negativo ? "text-muted" : "text-ink"}`}>{spec.value}</dd>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</dl>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { key } = await params;
|
||||
const serie = seriesData[key];
|
||||
|
||||
if (!serie) {
|
||||
return { title: "Série não encontrada", robots: { index: false, follow: false } };
|
||||
}
|
||||
|
||||
// A descrição usa as especificações reais em vez de texto genérico: é o que
|
||||
// aparece no resultado de busca e o que faz alguém clicar.
|
||||
const radio = [
|
||||
String(serie.wifi).startsWith("Não") ? null : serie.wifi.split(" (")[0],
|
||||
String(serie.bluetooth).startsWith("Não") ? null : serie.bluetooth.split(" (")[0],
|
||||
serie.zigbee_thread && !String(serie.zigbee_thread).startsWith("Não") ? "802.15.4" : null,
|
||||
].filter(Boolean).join(", ");
|
||||
|
||||
const descricao =
|
||||
`${serie.nome_completo}: ${serie.arquitetura}, ${serie.frequencia.replace("Até ", "até ")}, ` +
|
||||
`${serie.memoria_sram} de SRAM e ${serie.gpio} GPIOs` +
|
||||
(radio ? `. Rádio: ${radio}` : "") +
|
||||
`. Pinagem completa, periféricos, segurança em hardware e pinos com restrição, em português.`;
|
||||
|
||||
// A pinagem da placa é a melhor prévia possível para uma série: quem
|
||||
// compartilha o link já mostra o diagrama. Séries sem placa publicada caem
|
||||
// na imagem padrão do site.
|
||||
// Definir openGraph explicitamente impede a herança automática da imagem do
|
||||
// site, então séries sem placa publicada (o H4) precisam do fallback aqui.
|
||||
const imagem = serie.placa
|
||||
? [{ url: serie.placa, width: 1600, height: 1091, alt: `Pinagem do ${serie.nome_completo}` }]
|
||||
: [{ url: "/opengraph-image.png", width: 1200, height: 630, alt: "ESPDocs" }];
|
||||
|
||||
const meta = {
|
||||
...paginaMeta({
|
||||
titulo: `${serie.nome_completo}: especificações e pinagem`,
|
||||
descricao,
|
||||
caminho: `/series/${key}`,
|
||||
tipo: "article",
|
||||
keywords: [
|
||||
serie.nome_completo, `${serie.nome_completo} pinout`,
|
||||
`${serie.nome_completo} datasheet`, `${serie.nome_completo} especificações`,
|
||||
`${serie.nome_completo} português`, "ESP32", "Espressif", "microcontrolador",
|
||||
],
|
||||
}),
|
||||
other: { "article:section": "Séries ESP32" },
|
||||
};
|
||||
|
||||
meta.openGraph = { ...meta.openGraph, images: imagem };
|
||||
meta.twitter = { ...meta.twitter, images: imagem };
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(seriesData).map((key) => ({
|
||||
key: key,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { paginaMeta, jsonLd } from "@/lib/seo";
|
||||
|
||||
export const metadata = paginaMeta({
|
||||
titulo: "Séries ESP32",
|
||||
descricao: "As 12 séries de ESP32 lado a lado: ESP32, S2, S3, S31, C2, C3, C5, C6, C61, P4, H2 e H4. Arquitetura, memória, rádio, periféricos e pinagem de cada uma, em português.",
|
||||
caminho: "/series",
|
||||
keywords: ["séries ESP32", "modelos de ESP32", "ESP32 S3", "ESP32 C6", "ESP32 P4", "diferença entre ESP32"],
|
||||
});
|
||||
|
||||
metadata.title = { default: "Séries ESP32", template: "%s · ESPDocs" };
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
pergunta: "Quantas séries de ESP32 existem atualmente?",
|
||||
resposta: "Atualmente o ecossistema Espressif conta com 12 séries principais: ESP32 clássico, ESP32-S2, ESP32-S3, ESP32-S31, ESP32-C2, ESP32-C3, ESP32-C5, ESP32-C6, ESP32-C61, ESP32-P4, ESP32-H2 e ESP32-H4.",
|
||||
},
|
||||
{
|
||||
pergunta: "Quais são as séries mais recentes da Espressif?",
|
||||
resposta: "As séries mais recentes incluem o ESP32-P4 (focado em alto desempenho com dual-core RISC-V a 400 MHz), o ESP32-C5 (primeiro chip com Wi-Fi dual-band 2.4/5 GHz da Espressif) e o ESP32-C61 (Wi-Fi 6 e Bluetooth 6 de baixo custo).",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Séries ESP32", caminho: "/series" },
|
||||
])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(jsonLd.perguntas(faqs)),
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { paginaMeta, jsonLd, SITE } from "@/lib/seo";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import Image from "next/image";
|
||||
import GiteaMark from "@/components/GiteaMark";
|
||||
import { ArrowUpRight, Code, Globe, GitCompare, Info, Languages } from "lucide-react";
|
||||
|
||||
const REPOSITORIO = SITE.repositorio;
|
||||
|
||||
|
||||
export const metadata = paginaMeta({
|
||||
titulo: "Sobre o projeto",
|
||||
descricao: "O ESPDocs é uma plataforma independente que reúne a documentação do ecossistema ESP32 em português brasileiro. Saiba como o projeto nasceu e como contribuir.",
|
||||
caminho: "/sobre",
|
||||
keywords: ["ESPDocs", "documentação ESP32 português", "projeto open source ESP32"],
|
||||
});
|
||||
|
||||
export default function Sobre() {
|
||||
const features = [
|
||||
{ icon: Languages, title: "Em português", desc: "Dados técnicos organizados e apresentados em português do Brasil." },
|
||||
{ icon: GitCompare, title: "Comparações visuais", desc: "Tabelas e diagramas para escolher a série certa sem abrir dez datasheets." },
|
||||
{ icon: Globe, title: "Acesso centralizado", desc: "Links diretos para a documentação oficial, repositórios e exemplos." },
|
||||
{ icon: Code, title: "Código aberto", desc: "Contribuições são bem-vindas no Gitea." },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="site-canvas min-h-screen">
|
||||
{/* Metadados E-E-A-T de autoria e organização para o Google */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.paginaSobre()) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Sobre o Projeto", caminho: "/sobre" },
|
||||
])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Header />
|
||||
|
||||
<main id="conteudo">
|
||||
<PageHero
|
||||
trail={[{ label: "Sobre" }]}
|
||||
icon={Info}
|
||||
label="Projeto independente"
|
||||
title="Sobre o ESPDocs"
|
||||
lede="Uma plataforma não oficial, em português, que organiza as informações técnicas do ESP32 e leva você à documentação oficial da Espressif. Feita para a comunidade de hardware livre."
|
||||
>
|
||||
<a href={REPOSITORIO} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
|
||||
<GiteaMark className="h-4 w-4" />
|
||||
Ver o repositório
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
</PageHero>
|
||||
|
||||
<div className="shell section grid grid-cols-1 gap-10 pt-12 sm:pt-16 lg:grid-cols-[1fr_22rem] lg:gap-14">
|
||||
<div className="min-w-0 max-w-3xl space-y-12">
|
||||
<section aria-labelledby="missao">
|
||||
<h2 id="missao" className="section-title">Missão</h2>
|
||||
<p className="mt-4 text-base leading-relaxed text-muted">
|
||||
Facilitar o acesso e a compreensão do ecossistema ESP32 com uma interface em português que organiza
|
||||
especificações, compara séries, apresenta frameworks e centraliza os links da documentação oficial.
|
||||
O objetivo é economizar o seu tempo no desenvolvimento de firmware e IoT.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="porque">
|
||||
<h2 id="porque" className="section-title">Por que o ESPDocs existe</h2>
|
||||
<p className="mt-4 text-base leading-relaxed text-muted">
|
||||
As informações sobre os ESP32 costumam estar espalhadas entre datasheets densos, manuais de referência
|
||||
(TRM) e repositórios secundários, tudo em inglês. O ESPDocs condensa e estrutura esses dados para a
|
||||
triagem rápida, antes de você aprofundar na engenharia oficial.
|
||||
</p>
|
||||
<ul className="mt-6 grid gap-3 sm:grid-cols-2">
|
||||
{features.map(({ icon: Icon, title, desc }) => (
|
||||
<li key={title} className="card flex items-start gap-4 p-5">
|
||||
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-accent-soft text-accent">
|
||||
<Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
<div>
|
||||
<h3 className="font-semibold text-ink">{title}</h3>
|
||||
<p className="mt-1 text-sm leading-relaxed text-muted">{desc}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="contribuir">
|
||||
<h2 id="contribuir" className="section-title">Como contribuir</h2>
|
||||
<p className="mt-4 text-base leading-relaxed text-muted">
|
||||
Correções de tradução, ajustes em tabelas e novos exemplos de código são muito bem-vindos. O projeto
|
||||
cresce com a comunidade.
|
||||
</p>
|
||||
<ol className="mt-6 space-y-3">
|
||||
{[
|
||||
"Faça o fork do repositório no Gitea.",
|
||||
"Implemente as melhorias e confira as referências técnicas oficiais.",
|
||||
"Abra um Pull Request descrevendo os ajustes feitos.",
|
||||
].map((passo, i) => (
|
||||
<li key={passo} className="card flex items-center gap-4 px-5 py-4">
|
||||
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-ink text-sm font-semibold text-canvas" aria-hidden="true">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="text-sm text-ink-soft sm:text-base">{passo}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="acessibilidade">
|
||||
<h2 id="acessibilidade" className="section-title">Acessibilidade</h2>
|
||||
<p className="mt-4 text-base leading-relaxed text-muted">
|
||||
O ESPDocs prioriza a legibilidade, o contraste alto (nível AAA da WCAG) e a navegação completa por
|
||||
teclado. Se encontrar alguma barreira de leitura ou navegação, abra uma issue no repositório.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside className="space-y-5 lg:sticky lg:top-28 lg:self-start" aria-label="Autor e aviso legal">
|
||||
<div className="panel p-6 sm:p-7">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="grid h-16 w-16 shrink-0 place-items-center rounded-2xl border border-line bg-white">
|
||||
<Image src="/logo.png" alt="" aria-hidden="true" width={48} height={48} className="h-12 w-12 object-contain" />
|
||||
</span>
|
||||
<div>
|
||||
<h2 className="font-display text-lg font-semibold text-ink">Lucas Rayan Guerra</h2>
|
||||
<p className="text-sm text-muted">Criador do Ciência Embarcada</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 space-y-3 text-sm leading-relaxed text-muted">
|
||||
<p>
|
||||
Técnico em Desenvolvimento de Sistemas, estudante de Ciência da Computação na UFRPE e pesquisador de
|
||||
Blockchain e Criptografia.
|
||||
</p>
|
||||
<p>
|
||||
Diretor de Software na Semine AgriTech e palestrante em eventos como o REC'n'Play, compartilha
|
||||
conteúdo sobre hardware, IoT e cibersegurança.
|
||||
</p>
|
||||
</div>
|
||||
<a href="https://cienciaembarcada.com.br" target="_blank" rel="noopener noreferrer" className="btn btn-ghost mt-6 w-full">
|
||||
Visitar o Ciência Embarcada
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="callout callout-warn">
|
||||
<p className="font-semibold text-ink">Aviso legal</p>
|
||||
<p className="mt-1">
|
||||
O ESPDocs é um agregador e organizador. Os manuais e repositórios referenciados pertencem à Espressif
|
||||
Systems. Em projetos críticos ou comerciais, consulte sempre a documentação oficial.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user