CI / Lint e auditorias (push) Successful in 1m10s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m39s
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 18s
Release / Cria a release no Gitea (push) Successful in 5s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
731 lines
38 KiB
React
731 lines
38 KiB
React
"use client";
|
||
|
||
import { useState, useMemo, useRef } from "react";
|
||
import { usePathname } from "next/navigation";
|
||
import Header from "@/components/Header";
|
||
import Footer from "@/components/Footer";
|
||
import PageHero from "@/components/PageHero";
|
||
import * as diagnosticoPt from "@/lib/diagnosticoData";
|
||
import * as diagnosticoEn from "@/lib/data/en/diagnostico";
|
||
import * as diagnosticoEs from "@/lib/data/es/diagnostico";
|
||
import * as diagnosticoFr from "@/lib/data/fr/diagnostico";
|
||
import * as diagnosticoDe from "@/lib/data/de/diagnostico";
|
||
import * as diagnosticoZh from "@/lib/data/zh-CN/diagnostico";
|
||
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_PT = [
|
||
["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 EXEMPLOS_LOG_EN = [
|
||
["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"],
|
||
["TLS Handshake", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
|
||
];
|
||
|
||
const EXEMPLOS_LOG_ES = [
|
||
["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 EXEMPLOS_LOG_FR = [
|
||
["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 EXEMPLOS_LOG_DE = [
|
||
["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"],
|
||
["TLS-Handshake", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
|
||
];
|
||
const EXEMPLOS_LOG_ZH = [
|
||
["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 got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
|
||
["TLS 握手", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
|
||
];
|
||
|
||
const GRAVIDADE = {
|
||
"Crítica": "text-danger",
|
||
"Critical": "text-danger",
|
||
"Critique": "text-danger",
|
||
"Kritisch": "text-danger",
|
||
Alta: "text-gold",
|
||
High: "text-gold",
|
||
"Élevée": "text-gold",
|
||
Hoch: "text-gold",
|
||
"Média": "text-accent",
|
||
Medium: "text-accent",
|
||
Media: "text-accent",
|
||
Moyenne: "text-accent",
|
||
Mittel: "text-accent",
|
||
"严重": "text-danger",
|
||
"高": "text-gold",
|
||
"中": "text-accent",
|
||
};
|
||
|
||
const CHECKLISTS_PT = [
|
||
{
|
||
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.",
|
||
],
|
||
},
|
||
];
|
||
|
||
const CHECKLISTS_EN = [
|
||
{
|
||
icon: Zap,
|
||
titulo: "Hardware and Power Supply",
|
||
itens: [
|
||
"10 µF to 100 µF low-ESR capacitor on VCC, plus a 100 nF ceramic capacitor close to the chip.",
|
||
"LDO regulator sized for 600 mA to 1 A current spikes, with adequate thermal margin.",
|
||
"No external peripherals forcing logic levels on strapping pins during reset and boot.",
|
||
"10 µF or 1 µF capacitor on the EN (RST) pin for stable auto-reset flashing.",
|
||
],
|
||
},
|
||
{
|
||
icon: Layers,
|
||
titulo: "Firmware and FreeRTOS",
|
||
itens: [
|
||
"Every FreeRTOS task yields CPU time with vTaskDelay, queues, or semaphores.",
|
||
"Every ISR marked with IRAM_ATTR with zero blocking calls.",
|
||
"Avoid continuous heap reallocations using Arduino Strings in tight loops.",
|
||
"Clock synchronized via SNTP before attempting TLS/HTTPS handshakes.",
|
||
],
|
||
},
|
||
];
|
||
|
||
const CHECKLISTS_ES = [
|
||
{
|
||
icon: Zap,
|
||
titulo: "Hardware y alimentación",
|
||
itens: [
|
||
"Condensador de 10 µF a 100 µF de bajo ESR en VCC, más uno cerámico de 100 nF junto al chip.",
|
||
"Regulador LDO dimensionado para picos de 600 mA a 1 A, con margen térmico.",
|
||
"Ningún periférico forzando niveles lógicos en los pines de strapping durante el arranque.",
|
||
"Condensador de 10 µF o 1 µF en el pin EN (RST) para auto-reset estable durante la programación.",
|
||
],
|
||
},
|
||
{
|
||
icon: Layers,
|
||
titulo: "Firmware y FreeRTOS",
|
||
itens: [
|
||
"Cada tarea de FreeRTOS cede la CPU con vTaskDelay, colas o semáforos.",
|
||
"Cada ISR marcada con IRAM_ATTR y sin llamadas bloqueantes.",
|
||
"Sin uso continuo de String de Arduino en bucles para evitar la fragmentación del heap.",
|
||
"Hora sincronizada mediante SNTP antes de abrir conexiones TLS/HTTPS.",
|
||
],
|
||
},
|
||
];
|
||
|
||
const CHECKLISTS_FR = [
|
||
{
|
||
icon: Zap,
|
||
titulo: "Matériel et alimentation",
|
||
itens: [
|
||
"Condensateur de 10 µF à 100 µF à faible ESR sur VCC, plus un céramique de 100 nF près de la puce.",
|
||
"Régulateur LDO dimensionné pour des pointes de 600 mA à 1 A, avec marge thermique.",
|
||
"Aucun périphérique ne forçant de niveau logique sur les broches de strapping pendant le boot.",
|
||
"Condensateur de 10 µF ou 1 µF sur la broche EN (RST) pour un auto-reset stable au flashage.",
|
||
],
|
||
},
|
||
{
|
||
icon: Layers,
|
||
titulo: "Firmware et FreeRTOS",
|
||
itens: [
|
||
"Chaque tâche FreeRTOS cède la CPU avec vTaskDelay, des files ou des sémaphores.",
|
||
"Chaque ISR marquée avec IRAM_ATTR et exempte d'appels bloquants.",
|
||
"Pas d'usage continu de String d'Arduino dans les boucles, afin d'éviter la fragmentation du tas.",
|
||
"Horloge synchronisée par SNTP avant d'ouvrir des connexions TLS/HTTPS.",
|
||
],
|
||
},
|
||
];
|
||
|
||
const CHECKLISTS_DE = [
|
||
{
|
||
icon: Zap,
|
||
titulo: "Hardware und Spannungsversorgung",
|
||
itens: [
|
||
"10 µF bis 100 µF Low-ESR-Kondensator an VCC, plus ein 100-nF-Keramikkondensator nah am Chip.",
|
||
"LDO-Regler ausgelegt für 600 mA bis 1 A Stromspitzen mit thermischer Reserve.",
|
||
"Keine Peripheriegeräte, die beim Booten Logikpegel an Strapping-Pins erzwingen.",
|
||
"10 µF- oder 1 µF-Kondensator am EN-Pin (RST) für zuverlässigen Auto-Reset beim Flashen.",
|
||
],
|
||
},
|
||
{
|
||
icon: Layers,
|
||
titulo: "Firmware und FreeRTOS",
|
||
itens: [
|
||
"Jeder FreeRTOS-Task gibt CPU-Zeit mit vTaskDelay, Queues oder Semaphoren ab.",
|
||
"Jede ISR mit IRAM_ATTR versehen und frei von blockierenden Aufrufen.",
|
||
"Keine fortlaufende Verwendung von Arduino-Strings in Schleifen zur Vermeidung von Heap-Fragmentierung.",
|
||
"Uhrzeit vor dem Aufbau von TLS/HTTPS-Verbindungen per SNTP synchronisiert.",
|
||
],
|
||
},
|
||
];
|
||
const CHECKLISTS_ZH = [
|
||
{
|
||
icon: Zap,
|
||
titulo: "硬件与供电",
|
||
itens: [
|
||
"VCC 上接 10 µF 到 100 µF 的低 ESR 电容,并在靠近芯片处再加一个 100 nF 陶瓷电容。",
|
||
"LDO 稳压器按 600 mA 到 1 A 的电流峰值选型,并留有足够的散热余量。",
|
||
"复位和启动期间,没有外部外设在启动配置引脚上强制逻辑电平。",
|
||
"EN(RST)引脚上接 10 µF 或 1 µF 电容,使自动复位烧录更稳定。",
|
||
],
|
||
},
|
||
{
|
||
icon: Layers,
|
||
titulo: "固件与 FreeRTOS",
|
||
itens: [
|
||
"每个 FreeRTOS 任务都通过 vTaskDelay、队列或信号量让出 CPU 时间。",
|
||
"每个 ISR 都标记了 IRAM_ATTR,且不包含任何阻塞调用。",
|
||
"避免在紧凑循环中使用 Arduino String 造成持续的堆重新分配。",
|
||
"在尝试 TLS/HTTPS 握手之前,先通过 SNTP 同步时钟。",
|
||
],
|
||
},
|
||
];
|
||
|
||
export default function DiagnosticoPage() {
|
||
const pathname = usePathname();
|
||
const isEn = pathname?.startsWith("/en");
|
||
const isEs = pathname?.startsWith("/es");
|
||
const isFr = pathname?.startsWith("/fr");
|
||
const isDe = pathname?.startsWith("/de");
|
||
const isZh = pathname?.startsWith("/zh-CN");
|
||
const { categoriasDiagnostico, sintomasRapidos, errosCatalogo, regrasDecodificadorLog } = isEs ? diagnosticoEs : isEn ? diagnosticoEn : isFr ? diagnosticoFr : isZh ? diagnosticoZh : isDe ? diagnosticoDe : diagnosticoPt;
|
||
const exemplosLog = isEs ? EXEMPLOS_LOG_ES : isEn ? EXEMPLOS_LOG_EN : isFr ? EXEMPLOS_LOG_FR : isZh ? EXEMPLOS_LOG_ZH : isDe ? EXEMPLOS_LOG_DE : EXEMPLOS_LOG_PT;
|
||
const checklists = isEs ? CHECKLISTS_ES : isEn ? CHECKLISTS_EN : isFr ? CHECKLISTS_FR : isZh ? CHECKLISTS_ZH : isDe ? CHECKLISTS_DE : CHECKLISTS_PT;
|
||
|
||
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(
|
||
isEs
|
||
? {
|
||
titulo: "Patrón de error no categorizado automáticamente",
|
||
tipo: "Desconocido",
|
||
explicacao:
|
||
"No identificamos una firma clásica de Guru Meditation o Brownout en este fragmento. Verifique que la velocidad de baudios (ej: 115200) sea correcta y si el encabezado 'rst:' o 'panic' está presente.",
|
||
acaoRecomendada:
|
||
"Busque palabras clave como 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' o 'panic' en la consola serie.",
|
||
}
|
||
: isEn
|
||
? {
|
||
titulo: "Error pattern not automatically classified",
|
||
tipo: "Unknown",
|
||
explicacao:
|
||
"We could not identify a classic Guru Meditation or Brownout signature in this snippet. Verify that the baud rate (e.g. 115200) is correct and whether the 'rst:' or 'panic' header is present.",
|
||
acaoRecomendada:
|
||
"Look for keywords such as 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog', or 'panic' in the serial output.",
|
||
}
|
||
: isFr
|
||
? {
|
||
titulo: "Modèle d'erreur non catégorisé automatiquement",
|
||
tipo: "Inconnu",
|
||
explicacao:
|
||
"Nous n'avons pas identifié de signature classique de Guru Meditation ou Brownout dans cet extrait. Vérifiez que la vitesse en bauds (ex. : 115200) est correcte et si l'en-tête 'rst:' ou 'panic' est présent.",
|
||
acaoRecomendada:
|
||
"Recherchez des mots-clés tels que 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' ou 'panic' dans la console série.",
|
||
}
|
||
: isZh ? {
|
||
titulo: "未能自动归类的错误模式",
|
||
tipo: "未知",
|
||
explicacao: "在这段内容中没有识别出典型的 Guru Meditation 或 Brownout 特征。请检查波特率(例如 115200)是否正确,以及是否包含以 'rst:' 或 'panic' 开头的信息。",
|
||
acaoRecomendada: "在串口控制台中查找 'Guru Meditation'、'rst:'、'LoadProhibited'、'watchdog' 或 'panic' 等关键字。",
|
||
} : isDe
|
||
? {
|
||
titulo: "Fehlermuster nicht automatisch kategorisiert",
|
||
tipo: "Unbekannt",
|
||
explicacao:
|
||
"Wir konnten in diesem Ausschnitt keine klassische Guru-Meditation- oder Brownout-Signatur erkennen. Prüfen Sie, ob die Baudrate (z. B. 115200) korrekt ist und ob der Header 'rst:' oder 'panic' vorhanden ist.",
|
||
acaoRecomendada:
|
||
"Suchen Sie in der seriellen Konsole nach Begriffen wie 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' oder 'panic'.",
|
||
}
|
||
: {
|
||
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, errosCatalogo]);
|
||
|
||
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
|
||
isEn={isEn}
|
||
isEs={isEs}
|
||
isFr={isFr}
|
||
isDe={isDe} isZh={isZh}
|
||
trail={[{ label: isEs ? "Diagnóstico" : isEn ? "Diagnostics" : isFr ? "Diagnostic" : isZh ? "诊断" : isDe ? "Diagnose" : "Diagnóstico" }]}
|
||
icon={AlertTriangle}
|
||
label={isEs ? "Depuración de firmware" : isEn ? "Firmware Debugging" : isFr ? "Débogage du firmware" : isZh ? "固件调试" : isDe ? "Firmware-Debugging" : "Depuração de firmware"}
|
||
title={isEs ? "Diagnóstico de fallas del ESP32" : isEn ? "ESP32 Diagnostics and Crash Decoder" : isFr ? "Diagnostic des pannes de l'ESP32" : isZh ? "ESP32 故障诊断" : isDe ? "ESP32-Fehlerdiagnose und Crash-Decoder" : "Diagnóstico de falhas do ESP32"}
|
||
lede={
|
||
isEs
|
||
? "Comprenda la causa raíz de las fallas por Brownout, Guru Meditation, Watchdog y Stack Overflow, y aplique la corrección de ingeniería en lugar de soluciones improvisadas."
|
||
: isEn
|
||
? "Understand the root cause of Brownout, Guru Meditation, Watchdog, and Stack Overflow crashes, and apply engineering solutions instead of workarounds."
|
||
: isFr
|
||
? "Comprenez la cause racine des pannes de Brownout, Guru Meditation, Watchdog et Stack Overflow, et appliquez la correction d'ingénierie plutôt qu'un bricolage."
|
||
: isZh ? "弄清 Brownout、Guru Meditation、Watchdog 和栈溢出的成因,并采用工程上的正确修复,而不是临时补救。" : isDe
|
||
? "Verstehen Sie die Ursache von Brownout, Guru Meditation, Watchdog und Stack Overflow und wenden Sie saubere Ingenieurlösungen statt Notbehelfen an."
|
||
: "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"
|
||
>
|
||
{isEs ? "Basado en el artículo de Ciência Embarcada" : isEn ? "Based on article by Ciência Embarcada" : isFr ? "D'après l'article de Ciência Embarcada" : isZh ? "基于 Ciência Embarcada 的文章" : isDe ? "Basierend auf dem Artikel von Ciência Embarcada" : "Baseado no artigo do Ciência Embarcada"}
|
||
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||
<span className="sr-only"> {isEs ? "(abre en una nueva pestaña)" : isEn ? "(opens in a new tab)" : isFr ? "(ouvre un nouvel onglet)" : isZh ? "(在新标签页打开)" : isDe ? "(öffnet in neuem Tab)" : "(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">
|
||
{isEs ? "Pegue el registro y descubra la causa" : isEn ? "Paste the log to see the cause" : isFr ? "Collez le journal et découvrez la cause" : isZh ? "粘贴日志,查看原因" : isDe ? "Log einfügen und Ursache ermitteln" : "Cole o log e veja a causa"}
|
||
</h2>
|
||
<p className="mt-2 text-sm text-muted">
|
||
{isEs ? "Copie el fragmento del monitor serie. El diagnóstico aparece al instante." : isEn ? "Copy the serial monitor output snippet. Diagnostics appear instantly." : isFr ? "Copiez l'extrait du moniteur série. Le diagnostic apparaît immédiatement." : isZh ? "复制串口监视器中的内容,诊断结果立即显示。" : isDe ? "Kopieren Sie den Ausschnitt aus dem seriellen Monitor. Die Diagnose erscheint sofort." : "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">{isEs ? "Probar con" : isEn ? "Test with" : isFr ? "Tester avec" : isZh ? "试试这些" : isDe ? "Testen mit" : "Testar com"}</span>
|
||
{exemplosLog.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">
|
||
{isEs ? "Registro del monitor serie" : isEn ? "Serial monitor log" : isFr ? "Journal du moniteur série" : isZh ? "串口监视器日志" : isDe ? "Serieller Monitor-Log" : "Log do monitor serial"}
|
||
</label>
|
||
<textarea
|
||
id="log-serial"
|
||
rows={4}
|
||
value={logInput}
|
||
onChange={(e) => analisarLog(e.target.value)}
|
||
placeholder={isEs ? "Pegue el registro del monitor serie aquí" : isEn ? "Paste serial monitor log here" : isFr ? "Collez le journal du moniteur série ici" : isZh ? "在此粘贴串口监视器日志" : isDe ? "Seriellen Monitor-Log hier einfügen" : "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" />
|
||
{isEs ? "Limpiar" : isEn ? "Clear" : isFr ? "Effacer" : isZh ? "清除" : isDe ? "Löschen" : "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">{isEs ? "Causa probable:" : isEn ? "Likely cause:" : isFr ? "Cause probable :" : isZh ? "可能的原因:" : isDe ? "Wahrscheinliche Ursache :" : "Causa provável:"}</strong> {resultadoLog.explicacao}</p>
|
||
<p><strong className="font-semibold text-ink">{isEs ? "Qué hacer:" : isEn ? "What to do:" : isFr ? "Que faire :" : isZh ? "该怎么做:" : isDe ? "Was zu tun ist :" : "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">
|
||
{isEs ? "Catálogo de fallas" : isEn ? "Fault catalog" : isFr ? "Catalogue de pannes" : isZh ? "故障目录" : isDe ? "Fehlerkatalog" : "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">
|
||
{isEs ? "Buscar por error, síntoma o mensaje serie" : isEn ? "Search by error, symptom, or serial message" : isFr ? "Rechercher par erreur, symptôme ou message série" : isZh ? "按错误、症状或串口信息搜索" : isDe ? "Nach Fehler, Symptom oder serieller Meldung suchen" : "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={isEs ? "Buscar error, síntoma o mensaje" : isEn ? "Search error, symptom, or message" : isFr ? "Rechercher une erreur, un symptôme ou un message" : isZh ? "搜索错误、症状或信息" : isDe ? "Fehler, Symptom oder Meldung suchen" : "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">{isEs ? "Síntoma" : isEn ? "Symptom" : isFr ? "Symptôme" : isZh ? "症状" : isDe ? "Symptom" : "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">{isEs ? "Área" : isEn ? "Area" : isFr ? "Domaine" : isZh ? "领域" : isDe ? "Bereich" : "Á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" />
|
||
{isEs ? "Todas" : isEn ? "All" : isFr ? "Tous" : isZh ? "全部" : isDe ? "Alle" : "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 ? (isEs ? "falla" : isEn ? "fault" : isFr ? "panne" : isZh ? "个故障" : isDe ? "Fehler" : "falha") : (isEs ? "fallas" : isEn ? "faults" : isFr ? "pannes" : isZh ? "个故障" : isDe ? "Fehler" : "falhas")}
|
||
{totalFiltros > 0 && <span className="font-normal text-muted">{isEs ? " con los filtros actuales" : isEn ? " with current filters" : isFr ? " avec les filtres actuels" : isZh ? "(当前筛选下)" : isDe ? " mit aktuellen Filtern" : " 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" />
|
||
{isEs ? "Restablecer filtros" : isEn ? "Reset filters" : isFr ? "Réinitialiser les filtres" : isZh ? "重置筛选" : isDe ? "Filter zurücksetzen" : "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">
|
||
{isEs ? "Ninguna falla coincide con estos filtros" : isEn ? "No faults matching these filters" : isFr ? "Aucune panne ne correspond à ces filtres" : isZh ? "没有符合这些筛选条件的故障" : isDe ? "Keine Fehler entsprechen diesen Filtern" : "Nenhuma falha com esses filtros"}
|
||
</h3>
|
||
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
|
||
{isEs ? "Busque por un término más general o elimine el síntoma y el área seleccionados." : isEn ? "Search for a more general term or remove selected symptom and area." : isFr ? "Recherchez un terme plus général ou retirez le symptôme et le domaine sélectionnés." : isZh ? "请搜索更通用的词,或去掉已选的症状和领域。" : isDe ? "Suchen Sie nach einem allgemeineren Begriff oder entfernen Sie gewähltes Symptom und Bereich." : "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">
|
||
{isEs ? "Limpiar filtros" : isEn ? "Clear filters" : isFr ? "Effacer les filtres" : isZh ? "清除筛选" : isDe ? "Filter zurücksetzen" : "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 || (isEs ? "# Ejemplo no aplicable" : isEn ? "# Example not applicable" : isFr ? "# Exemple non applicable" : isZh ? "# 示例不适用" : isDe ? "# Beispiel nicht anwendbar" : "# 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"}`}>
|
||
{isEs ? (
|
||
<>Severidad {erro.gravidade.toLowerCase()}</>
|
||
) : isEn ? (
|
||
<>Severity: {erro.gravidade.toLowerCase()}</>
|
||
) : isFr ? (
|
||
<>Gravité : {erro.gravidade.toLowerCase()}</>
|
||
) : isZh ? ( <>严重程度:{erro.gravidade}</> ) : isDe ? (
|
||
<>Schweregrad: {erro.gravidade.toLowerCase()}</>
|
||
) : (
|
||
<>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" />
|
||
{isEs ? "Cómo aparece en el monitor serie" : isEn ? "How it appears in serial monitor" : isFr ? "Apparence dans le moniteur série" : isZh ? "在串口监视器中的样子" : isDe ? "Erscheinungsbild im seriellen Monitor" : "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}` ? (isEs ? "Copiado" : isEn ? "Copied" : isFr ? "Copié" : isZh ? "已复制" : isDe ? "Kopiert" : "Copiado") : (isEs ? "Copiar registro" : isEn ? "Copy log" : isFr ? "Copier le log" : isZh ? "复制日志" : isDe ? "Log kopieren" : "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" />
|
||
{isEs ? "Causa raíz" : isEn ? "Root cause" : isFr ? "Cause racine" : isZh ? "根本原因" : isDe ? "Ursache" : "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" />
|
||
{isEs ? "La solución improvisada común" : isEn ? "The common workaround" : isFr ? "Le bricolage courant" : isZh ? "常见的临时补救" : isDe ? "Der verbreitete Notbehelf" : "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">{isEs ? "Consecuencia:" : isEn ? "Consequence:" : isFr ? "Conséquence :" : isZh ? "后果:" : isDe ? "Folge :" : "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" />
|
||
{isEs ? "La solución de ingeniería" : isEn ? "The engineering solution" : isFr ? "La solution d'ingénierie" : isZh ? "工程上的解决方案" : isDe ? "Die Ingenieurlösung" : "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={isEs ? "Lenguaje del ejemplo" : isEn ? "Example language" : isFr ? "Langage de l'exemple" : isZh ? "示例语言" : isDe ? "Sprache des Beispiels" : "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}` ? (isEs ? "Copiado" : isEn ? "Copied" : isFr ? "Copié" : isZh ? "已复制" : isDe ? "Kopiert" : "Copiado") : (isEs ? "Copiar código" : isEn ? "Copy code" : isFr ? "Copier le code" : isZh ? "复制代码" : isDe ? "Code kopieren" : "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">{isEs ? "Consejo de ingeniería:" : isEn ? "Engineering tip:" : isFr ? "Conseil d'ingénierie :" : isZh ? "工程提示:" : isDe ? "Praxistipp :" : "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">
|
||
{isEs ? "Lista de verificación previa a campo" : isEn ? "Pre-deployment checklist" : isFr ? "Checklist avant déploiement" : isZh ? "上线前的检查清单" : isDe ? "Checkliste vor dem Feldeinsatz" : "Checklist antes de ir para campo"}
|
||
</h2>
|
||
<p className="mt-3 max-w-2xl text-base text-muted">
|
||
{isEs ? "Elementos esenciales para validar en hardware y firmware antes de entregar el producto." : isEn ? "Essential items to validate on hardware and firmware before shipping a product." : isFr ? "Éléments essentiels à valider sur le matériel et le firmware avant de livrer le produit." : isZh ? "在交付产品之前,需要在硬件和固件上验证的关键项目。" : isDe ? "Wesentliche Punkte zur Prüfung von Hardware und Firmware vor der Auslieferung." : "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>
|
||
);
|
||
}
|