- Translate content to English across messages/en.json, data/en, lib/data/en, and public/exemplos/en - Implement localized routing, slugs, and language negotiation with fallback to en - Build dedicated search index for English with strict book exclusion - Update components and pages to dynamically render localized strings and datasets - Verify full structural parity and exact 1:1 match on Portuguese pages via comparator Co-Authored-By: Antigravity <noreply@google.com>
1907 lines
78 KiB
React
1907 lines
78 KiB
React
"use client";
|
|
|
|
import { useState, useMemo } from "react";
|
|
import { usePathname } from "next/navigation";
|
|
import Header from "@/components/Header";
|
|
import Footer from "@/components/Footer";
|
|
import Link from "next/link";
|
|
import { getLocalizedHref } from "@/i18n/links";
|
|
import seriesDataPt from "@/public/series.json";
|
|
import seriesDataEn from "@/data/en/series.json";
|
|
import PageHero from "@/components/PageHero";
|
|
import {
|
|
Activity,
|
|
AlertTriangle,
|
|
ArrowRight,
|
|
ArrowUpRight,
|
|
Award,
|
|
Ban,
|
|
Battery,
|
|
BookOpen,
|
|
Bot,
|
|
Cable,
|
|
Check,
|
|
CheckCircle,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Cpu,
|
|
Edit2,
|
|
Filter,
|
|
HardDrive,
|
|
HelpCircle,
|
|
Home as HomeIcon,
|
|
KeyRound,
|
|
Layers,
|
|
Lock,
|
|
Monitor,
|
|
Network,
|
|
Radio,
|
|
RotateCcw,
|
|
ShieldCheck,
|
|
ShoppingBag,
|
|
Sliders,
|
|
SlidersHorizontal,
|
|
Sparkles,
|
|
Volume2,
|
|
Wifi,
|
|
Zap,
|
|
} from "lucide-react";
|
|
|
|
// Dynamic icon mapper for options and presets
|
|
const iconMap = {
|
|
// Categorias
|
|
"category_smarthome": <HomeIcon className="h-5 w-5" aria-hidden="true" />,
|
|
"category_multimedia": <Monitor className="h-5 w-5" aria-hidden="true" />,
|
|
"category_ai_edge": <Bot className="h-5 w-5" aria-hidden="true" />,
|
|
"category_wearable": <Battery className="h-5 w-5" aria-hidden="true" />,
|
|
"category_industrial": <Cpu className="h-5 w-5" aria-hidden="true" />,
|
|
"category_basic_iot": <Sparkles className="h-5 w-5" aria-hidden="true" />,
|
|
|
|
// Conectividade
|
|
"connectivity_wifi6_dualband": <Wifi className="h-5 w-5" aria-hidden="true" />,
|
|
"connectivity_wifi6_mesh": <Network className="h-5 w-5" aria-hidden="true" />,
|
|
"connectivity_mesh_only": <Radio className="h-5 w-5" aria-hidden="true" />,
|
|
"connectivity_wifi_ble": <Wifi className="h-5 w-5" aria-hidden="true" />,
|
|
"connectivity_bt_classic_audio": <Volume2 className="h-5 w-5" aria-hidden="true" />,
|
|
"connectivity_ethernet": <Cable className="h-5 w-5" aria-hidden="true" />,
|
|
"connectivity_none_local": <Ban className="h-5 w-5" aria-hidden="true" />,
|
|
|
|
// Alimentação
|
|
"power_battery_critical": <Battery className="h-5 w-5" aria-hidden="true" />,
|
|
"power_ulp_coprocessor": <Activity className="h-5 w-5" aria-hidden="true" />,
|
|
"power_always_on": <Zap className="h-5 w-5" aria-hidden="true" />,
|
|
|
|
// Periféricos / Hardware
|
|
"hardware_mipi_multimedia": <Monitor className="h-5 w-5" aria-hidden="true" />,
|
|
"hardware_usb_otg": <Cable className="h-5 w-5" aria-hidden="true" />,
|
|
"hardware_can_twai": <Sliders className="h-5 w-5" aria-hidden="true" />,
|
|
"hardware_dac_audio": <Volume2 className="h-5 w-5" aria-hidden="true" />,
|
|
"hardware_many_gpios": <SlidersHorizontal className="h-5 w-5" aria-hidden="true" />,
|
|
"hardware_standard_sensors": <Cpu className="h-5 w-5" aria-hidden="true" />,
|
|
|
|
// Memória
|
|
"memory_heavy_psram": <HardDrive className="h-5 w-5" aria-hidden="true" />,
|
|
"memory_moderate_sram": <Layers className="h-5 w-5" aria-hidden="true" />,
|
|
"memory_minimal_cost": <Sparkles className="h-5 w-5" aria-hidden="true" />,
|
|
|
|
// IA
|
|
"ai_ai_vector": <Bot className="h-5 w-5" aria-hidden="true" />,
|
|
"ai_standard": <Ban className="h-5 w-5" aria-hidden="true" />,
|
|
|
|
// Segurança
|
|
"security_key_manager_dpa": <KeyRound className="h-5 w-5" aria-hidden="true" />,
|
|
"security_hardware_ecc_ecdsa": <ShieldCheck className="h-5 w-5" aria-hidden="true" />,
|
|
"security_matter_ecdsa": <ShieldCheck className="h-5 w-5" aria-hidden="true" />,
|
|
"security_secure_boot": <Lock className="h-5 w-5" aria-hidden="true" />,
|
|
"security_standard_tls": <HelpCircle className="h-5 w-5" aria-hidden="true" />,
|
|
|
|
// Ecossistema
|
|
"ecosystem_arduino_ready": <CheckCircle className="h-5 w-5" aria-hidden="true" />,
|
|
"ecosystem_advanced_idf": <Cpu className="h-5 w-5" aria-hidden="true" />,
|
|
};
|
|
|
|
const getOptionIcon = (questionId, value) => {
|
|
const key = `${questionId}_${value}`;
|
|
return iconMap[key] || <HelpCircle className="h-5 w-5" aria-hidden="true" />;
|
|
};
|
|
|
|
// 7 Preset Configurations for 1-click recommendations
|
|
const PRESETS_PT = [
|
|
{
|
|
id: "preset_smarthome",
|
|
title: "Smart Home & Matter",
|
|
badge: "Automação 802.15.4",
|
|
description: "Wi-Fi 6, Thread/Zigbee e baixo consumo",
|
|
answers: {
|
|
category: "smarthome",
|
|
connectivity: "wifi6_mesh",
|
|
power: "ulp_coprocessor",
|
|
hardware: "standard_sensors",
|
|
memory: "moderate_sram",
|
|
ai: "standard",
|
|
security: "hardware_ecc_ecdsa",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_hmi",
|
|
title: "Painel Touch & Telas",
|
|
badge: "Display / Câmera",
|
|
description: "Display LCD/MIPI, PSRAM e alta taxa de quadros",
|
|
answers: {
|
|
category: "multimedia",
|
|
connectivity: "wifi_ble",
|
|
power: "always_on",
|
|
hardware: "mipi_multimedia",
|
|
memory: "heavy_psram",
|
|
ai: "standard",
|
|
security: "secure_boot",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_ai",
|
|
title: "Edge AI & Visão",
|
|
badge: "Aceleração Vetorial",
|
|
description: "Acelerador neural, câmera e alta memória",
|
|
answers: {
|
|
category: "ai_edge",
|
|
connectivity: "wifi_ble",
|
|
power: "always_on",
|
|
hardware: "mipi_multimedia",
|
|
memory: "heavy_psram",
|
|
ai: "ai_vector",
|
|
security: "secure_boot",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_wearable",
|
|
title: "Wearable a Bateria",
|
|
badge: "Deep Sleep <10µA",
|
|
description: "Eficiência extrema e rádio Bluetooth LE",
|
|
answers: {
|
|
category: "wearable",
|
|
connectivity: "wifi_ble",
|
|
power: "battery_critical",
|
|
hardware: "standard_sensors",
|
|
memory: "minimal_cost",
|
|
ai: "standard",
|
|
security: "standard_tls",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_wifi6_5ghz",
|
|
title: "Wi-Fi 6 Dual-Band (5GHz)",
|
|
badge: "2.4 & 5 GHz",
|
|
description: "Banda de 5 GHz com menor interferência de RF",
|
|
answers: {
|
|
category: "smarthome",
|
|
connectivity: "wifi6_dualband",
|
|
power: "always_on",
|
|
hardware: "standard_sensors",
|
|
memory: "moderate_sram",
|
|
ai: "standard",
|
|
security: "key_manager_dpa",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_industrial",
|
|
title: "Industrial / CAN & Rede",
|
|
badge: "Ethernet & TWAI",
|
|
description: "Barramento CAN/TWAI, Ethernet e alta robustez",
|
|
answers: {
|
|
category: "industrial",
|
|
connectivity: "ethernet",
|
|
power: "always_on",
|
|
hardware: "can_twai",
|
|
memory: "moderate_sram",
|
|
ai: "standard",
|
|
security: "secure_boot",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_budget",
|
|
title: "IoT Básico de Baixo Custo",
|
|
badge: "Ultra-Econômico",
|
|
description: "Menor custo por unidade para sensores simples",
|
|
answers: {
|
|
category: "basic_iot",
|
|
connectivity: "wifi_ble",
|
|
power: "battery_critical",
|
|
hardware: "standard_sensors",
|
|
memory: "minimal_cost",
|
|
ai: "standard",
|
|
security: "standard_tls",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
}
|
|
];
|
|
|
|
const PRESETS_EN = [
|
|
{
|
|
id: "preset_smarthome",
|
|
title: "Smart Home & Matter",
|
|
badge: "802.15.4 Automation",
|
|
description: "Wi-Fi 6, Thread/Zigbee, and low power",
|
|
answers: {
|
|
category: "smarthome",
|
|
connectivity: "wifi6_mesh",
|
|
power: "ulp_coprocessor",
|
|
hardware: "standard_sensors",
|
|
memory: "moderate_sram",
|
|
ai: "standard",
|
|
security: "hardware_ecc_ecdsa",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_hmi",
|
|
title: "Touch Panel & Displays",
|
|
badge: "Display / Camera",
|
|
description: "LCD/MIPI display, PSRAM, and high frame rates",
|
|
answers: {
|
|
category: "multimedia",
|
|
connectivity: "wifi_ble",
|
|
power: "always_on",
|
|
hardware: "mipi_multimedia",
|
|
memory: "heavy_psram",
|
|
ai: "standard",
|
|
security: "secure_boot",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_ai",
|
|
title: "Edge AI & Vision",
|
|
badge: "Vector Acceleration",
|
|
description: "Neural accelerator, camera, and large memory",
|
|
answers: {
|
|
category: "ai_edge",
|
|
connectivity: "wifi_ble",
|
|
power: "always_on",
|
|
hardware: "mipi_multimedia",
|
|
memory: "heavy_psram",
|
|
ai: "ai_vector",
|
|
security: "secure_boot",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_wearable",
|
|
title: "Battery-Powered Wearable",
|
|
badge: "Deep Sleep <10µA",
|
|
description: "Extreme efficiency and Bluetooth LE radio",
|
|
answers: {
|
|
category: "wearable",
|
|
connectivity: "wifi_ble",
|
|
power: "battery_critical",
|
|
hardware: "standard_sensors",
|
|
memory: "minimal_cost",
|
|
ai: "standard",
|
|
security: "standard_tls",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_wifi6_5ghz",
|
|
title: "Dual-Band Wi-Fi 6 (5GHz)",
|
|
badge: "2.4 & 5 GHz",
|
|
description: "5 GHz band with reduced RF interference",
|
|
answers: {
|
|
category: "smarthome",
|
|
connectivity: "wifi6_dualband",
|
|
power: "always_on",
|
|
hardware: "standard_sensors",
|
|
memory: "moderate_sram",
|
|
ai: "standard",
|
|
security: "key_manager_dpa",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_industrial",
|
|
title: "Industrial / CAN & Wired Net",
|
|
badge: "Ethernet & TWAI",
|
|
description: "CAN/TWAI bus, Ethernet, and high robustness",
|
|
answers: {
|
|
category: "industrial",
|
|
connectivity: "ethernet",
|
|
power: "always_on",
|
|
hardware: "can_twai",
|
|
memory: "moderate_sram",
|
|
ai: "standard",
|
|
security: "secure_boot",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
},
|
|
{
|
|
id: "preset_budget",
|
|
title: "Entry-Level Low-Cost IoT",
|
|
badge: "Ultra-Budget",
|
|
description: "Lowest unit cost for simple connected sensors",
|
|
answers: {
|
|
category: "basic_iot",
|
|
connectivity: "wifi_ble",
|
|
power: "battery_critical",
|
|
hardware: "standard_sensors",
|
|
memory: "minimal_cost",
|
|
ai: "standard",
|
|
security: "standard_tls",
|
|
ecosystem: "arduino_ready",
|
|
}
|
|
}
|
|
];
|
|
|
|
const QUESTIONS_PT = [
|
|
{
|
|
id: "category",
|
|
question: "1. Qual é o foco principal ou aplicação do seu projeto?",
|
|
description: "Categorizar sua aplicação ajuda a equilibrar formato físico, custo por unidade, barramentos de mídia e poder computacional.",
|
|
options: [
|
|
{ value: "smarthome", label: "Automação / Smart Home / Dispositivos Conectados", icon: "category_smarthome" },
|
|
{ value: "multimedia", label: "Interfaces Gráficas (Telas LCD/MIPI / Câmeras / IHM)", icon: "category_multimedia" },
|
|
{ value: "ai_edge", label: "Inteligência Artificial na Borda (Visão / Áudio / TinyML)", icon: "category_ai_edge" },
|
|
{ value: "wearable", label: "Dispositivo Portátil / Vestível (Alimentado a Bateria)", icon: "category_wearable" },
|
|
{ value: "industrial", label: "Industrial / Robótica / Redes de Campo e Automação", icon: "category_industrial" },
|
|
{ value: "basic_iot", label: "IoT Básico / Sensor Conectado de Ultra-Baixo Custo", icon: "category_basic_iot" },
|
|
]
|
|
},
|
|
{
|
|
id: "connectivity",
|
|
question: "2. Qual tecnologia de rede sem fio ou cabeada você precisa?",
|
|
description: "O ecossistema ESP32 oferece desde rádios Wi-Fi 6 Dual-Band (2.4/5GHz), Bluetooth 6 e Mesh 802.15.4 até Ethernet cabeada e Bluetooth Clássico.",
|
|
options: [
|
|
{ value: "wifi6_dualband", label: "Wi-Fi 6 Dual-Band (2.4 GHz e 5 GHz) + Bluetooth 6.0", icon: "connectivity_wifi6_dualband" },
|
|
{ value: "wifi6_mesh", label: "Wi-Fi 6 (2.4 GHz) + Matter / Zigbee 3.0 / Thread (802.15.4)", icon: "connectivity_wifi6_mesh" },
|
|
{ value: "mesh_only", label: "Apenas Rádio Mesh (Zigbee / Thread / Matter) + BLE (Sem Wi-Fi)", icon: "connectivity_mesh_only" },
|
|
{ value: "wifi_ble", label: "Wi-Fi 4 Tradicional (2.4 GHz) + Bluetooth LE", icon: "connectivity_wifi_ble" },
|
|
{ value: "bt_classic_audio", label: "Bluetooth Clássico BR/EDR (Áudio A2DP / SPP Serial)", icon: "connectivity_bt_classic_audio" },
|
|
{ value: "ethernet", label: "Rede Cabeada Industrial (Ethernet MAC integrado)", icon: "connectivity_ethernet" },
|
|
{ value: "none_local", label: "Apenas Processamento Local / Sem Conexão Sem Fio", icon: "connectivity_none_local" },
|
|
]
|
|
},
|
|
{
|
|
id: "power",
|
|
question: "3. Como o dispositivo será alimentado eletricamente?",
|
|
description: "A escolha da fonte define a importância de modos de Deep Sleep ultra-baixos (<10µA) e coprocessadores ULP para monitoramento contínuo.",
|
|
options: [
|
|
{ value: "battery_critical", label: "Bateria / Solar (Consumo Ultra-Baixo em Deep Sleep <10µA)", icon: "power_battery_critical" },
|
|
{ value: "ulp_coprocessor", label: "Misto / Monitorar sensores em background via Coprocessador ULP", icon: "power_ulp_coprocessor" },
|
|
{ value: "always_on", label: "Tomada, Fonte Externa ou USB (Sempre Ativo / Alto Desempenho)", icon: "power_always_on" },
|
|
]
|
|
},
|
|
{
|
|
id: "hardware",
|
|
question: "4. Quais periféricos ou barramentos físicos especiais você vai usar?",
|
|
description: "Algumas funcionalidades exigem blocos de silício dedicados, como interfaces de vídeo MIPI/RGB, barramento CAN industrial ou USB nativo.",
|
|
options: [
|
|
{ value: "mipi_multimedia", label: "Display Gráfico LCD (RGB/MIPI DSI) ou Câmeras (DVP/MIPI CSI)", icon: "hardware_mipi_multimedia" },
|
|
{ value: "usb_otg", label: "USB Nativo OTG / Host (Emular teclado/mouse HID, CDC ou modo Host)", icon: "hardware_usb_otg" },
|
|
{ value: "can_twai", label: "Barramento Automotivo / Industrial CAN (TWAI ou CAN-FD)", icon: "hardware_can_twai" },
|
|
{ value: "dac_audio", label: "Áudio Analógico com DAC integrado (saída direta de som)", icon: "hardware_dac_audio" },
|
|
{ value: "many_gpios", label: "Muitos pinos livres de expansão (Mais de 35 GPIOs físicos)", icon: "hardware_many_gpios" },
|
|
{ value: "standard_sensors", label: "Sensores comuns com barramentos tradicionais (I2C, SPI, UART, PWM)", icon: "hardware_standard_sensors" },
|
|
]
|
|
},
|
|
{
|
|
id: "memory",
|
|
question: "5. Qual o volume de memória RAM / PSRAM necessário?",
|
|
description: "Projetos gráficos (LVGL), visão computacional, áudio ou pilhas pesadas exigem suporte a PSRAM externa ou integrada de alta densidade.",
|
|
options: [
|
|
{ value: "heavy_psram", label: "Alta Densidade de PSRAM (>4 MB a 64 MB para buffers de vídeo, IA ou LVGL)", icon: "memory_heavy_psram" },
|
|
{ value: "moderate_sram", label: "SRAM interna equilibrada (320 KB a 512 KB) com ou sem PSRAM leve", icon: "memory_moderate_sram" },
|
|
{ value: "minimal_cost", label: "Memória compacta e econômica (firmware enxuto de baixo custo)", icon: "memory_minimal_cost" },
|
|
]
|
|
},
|
|
{
|
|
id: "ai",
|
|
question: "6. Seu projeto usará Inteligência Artificial local (Edge AI / TinyML)?",
|
|
description: "Instruções vetoriais e aceleradores neurais dedicados em hardware reduzem o tempo de inferência de redes neurais de segundos para milissegundos.",
|
|
options: [
|
|
{ value: "ai_vector", label: "Sim, preciso de acelerador vetorial de IA em hardware (TinyML / Visão)", icon: "ai_ai_vector" },
|
|
{ value: "standard", label: "Não, processamento lógico e matemático comum é suficiente", icon: "ai_standard" },
|
|
]
|
|
},
|
|
{
|
|
id: "security",
|
|
question: "7. Qual nível de segurança e criptografia por hardware o produto exige?",
|
|
description: "As séries diferem em suporte a Key Manager, proteção contra ataques de canal lateral (DPA), curvas elípticas (ECC/ECDSA) e aceleradores AES.",
|
|
options: [
|
|
{ value: "key_manager_dpa", label: "Proteção Máxima: Key Manager em hardware (chave isolada da CPU) e Proteção DPA", icon: "security_key_manager_dpa" },
|
|
{ value: "hardware_ecc_ecdsa", label: "Acelerador de Curvas Elípticas (ECC) e Assinatura Digital ECDSA por hardware", icon: "security_hardware_ecc_ecdsa" },
|
|
{ value: "secure_boot", label: "Boot Seguro V2 e Criptografia de Flash (XTS-AES) contra cópia do firmware", icon: "security_secure_boot" },
|
|
{ value: "standard_tls", label: "Comunicação TLS/HTTPS comum (Aceleradores AES e SHA por hardware)", icon: "security_standard_tls" },
|
|
]
|
|
},
|
|
{
|
|
id: "ecosystem",
|
|
question: "8. Qual é a sua preferência de ambiente de desenvolvimento?",
|
|
description: "Chips mais maduros têm amplo suporte oficial no Arduino Core com centenas de bibliotecas prontas, enquanto chips preliminares utilizam ESP-IDF.",
|
|
options: [
|
|
{ value: "arduino_ready", label: "Pronto para Arduino IDE (suporte maduro com vasta coleção de bibliotecas)", icon: "ecosystem_arduino_ready" },
|
|
{ value: "advanced_idf", label: "ESP-IDF profissional ou aberto a silícios de última geração (S31, H4, etc.)", icon: "ecosystem_advanced_idf" },
|
|
]
|
|
}
|
|
];
|
|
|
|
const QUESTIONS_EN = [
|
|
{
|
|
id: "category",
|
|
question: "1. What is the main focus or application of your project?",
|
|
description: "Categorizing your application helps balance physical form factor, unit cost, media buses, and compute performance.",
|
|
options: [
|
|
{ value: "smarthome", label: "Automation / Smart Home / Connected Devices", icon: "category_smarthome" },
|
|
{ value: "multimedia", label: "Graphical Interfaces (LCD/MIPI Displays / Cameras / HMI)", icon: "category_multimedia" },
|
|
{ value: "ai_edge", label: "Edge Artificial Intelligence (Vision / Audio / TinyML)", icon: "category_ai_edge" },
|
|
{ value: "wearable", label: "Portable / Wearable Device (Battery-Powered)", icon: "category_wearable" },
|
|
{ value: "industrial", label: "Industrial / Robotics / Fieldbuses and Automation", icon: "category_industrial" },
|
|
{ value: "basic_iot", label: "Basic IoT / Ultra-Low-Cost Connected Sensor", icon: "category_basic_iot" },
|
|
]
|
|
},
|
|
{
|
|
id: "connectivity",
|
|
question: "2. Which wireless or wired networking technology do you need?",
|
|
description: "The ESP32 ecosystem offers everything from Dual-Band Wi-Fi 6 (2.4/5GHz), Bluetooth 6, and 802.15.4 Mesh to wired Ethernet and Bluetooth Classic.",
|
|
options: [
|
|
{ value: "wifi6_dualband", label: "Dual-Band Wi-Fi 6 (2.4 GHz & 5 GHz) + Bluetooth 6.0", icon: "connectivity_wifi6_dualband" },
|
|
{ value: "wifi6_mesh", label: "Wi-Fi 6 (2.4 GHz) + Matter / Zigbee 3.0 / Thread (802.15.4)", icon: "connectivity_wifi6_mesh" },
|
|
{ value: "mesh_only", label: "Mesh Radio Only (Zigbee / Thread / Matter) + BLE (No Wi-Fi)", icon: "connectivity_mesh_only" },
|
|
{ value: "wifi_ble", label: "Traditional Wi-Fi 4 (2.4 GHz) + Bluetooth LE", icon: "connectivity_wifi_ble" },
|
|
{ value: "bt_classic_audio", label: "Bluetooth Classic BR/EDR (A2DP Audio / SPP Serial)", icon: "connectivity_bt_classic_audio" },
|
|
{ value: "ethernet", label: "Industrial Wired Network (Integrated Ethernet MAC)", icon: "connectivity_ethernet" },
|
|
{ value: "none_local", label: "Local Processing Only / No Wireless Connection", icon: "connectivity_none_local" },
|
|
]
|
|
},
|
|
{
|
|
id: "power",
|
|
question: "3. How will the device be electrically powered?",
|
|
description: "Power source choices determine the need for ultra-low Deep Sleep modes (<10µA) and ULP coprocessors for continuous monitoring.",
|
|
options: [
|
|
{ value: "battery_critical", label: "Battery / Solar (Ultra-Low Deep Sleep Current <10µA)", icon: "power_battery_critical" },
|
|
{ value: "ulp_coprocessor", label: "Mixed / Background Sensor Monitoring via ULP Coprocessor", icon: "power_ulp_coprocessor" },
|
|
{ value: "always_on", label: "Mains, External Power Supply, or USB (Always-On / High Performance)", icon: "power_always_on" },
|
|
]
|
|
},
|
|
{
|
|
id: "hardware",
|
|
question: "4. Which specialized peripherals or physical buses will you use?",
|
|
description: "Certain features require dedicated silicon blocks, such as MIPI/RGB video interfaces, industrial CAN bus, or native USB.",
|
|
options: [
|
|
{ value: "mipi_multimedia", label: "Graphic LCD Display (RGB/MIPI DSI) or Cameras (DVP/MIPI CSI)", icon: "hardware_mipi_multimedia" },
|
|
{ value: "usb_otg", label: "Native USB OTG / Host (Emulate HID keyboard/mouse, CDC, or Host mode)", icon: "hardware_usb_otg" },
|
|
{ value: "can_twai", label: "Automotive / Industrial CAN Bus (TWAI or CAN-FD)", icon: "hardware_can_twai" },
|
|
{ value: "dac_audio", label: "Analog Audio with Integrated DAC (Direct Sound Output)", icon: "hardware_dac_audio" },
|
|
{ value: "many_gpios", label: "Many Expansion Pins (More than 35 Physical GPIOs)", icon: "hardware_many_gpios" },
|
|
{ value: "standard_sensors", label: "Standard Sensors with Common Buses (I2C, SPI, UART, PWM)", icon: "hardware_standard_sensors" },
|
|
]
|
|
},
|
|
{
|
|
id: "memory",
|
|
question: "5. What volume of RAM / PSRAM is required?",
|
|
description: "Graphic UI projects (LVGL), computer vision, audio, or heavy stacks require high-density external or integrated PSRAM.",
|
|
options: [
|
|
{ value: "heavy_psram", label: "High PSRAM Density (>4 MB to 64 MB for video buffers, AI, or LVGL)", icon: "memory_heavy_psram" },
|
|
{ value: "moderate_sram", label: "Balanced Internal SRAM (320 KB to 512 KB) with or without light PSRAM", icon: "memory_moderate_sram" },
|
|
{ value: "minimal_cost", label: "Compact and Economical Memory (Lean, low-cost firmware)", icon: "memory_minimal_cost" },
|
|
]
|
|
},
|
|
{
|
|
id: "ai",
|
|
question: "6. Will your project use local Artificial Intelligence (Edge AI / TinyML)?",
|
|
description: "Hardware vector instructions and dedicated neural accelerators cut neural network inference times from seconds to milliseconds.",
|
|
options: [
|
|
{ value: "ai_vector", label: "Yes, I need hardware AI vector acceleration (TinyML / Vision)", icon: "ai_ai_vector" },
|
|
{ value: "standard", label: "No, standard logic and mathematical processing is sufficient", icon: "ai_standard" },
|
|
]
|
|
},
|
|
{
|
|
id: "security",
|
|
question: "7. What level of hardware security and cryptography does the product require?",
|
|
description: "Series differ in Key Manager support, side-channel attack protection (DPA), elliptic curves (ECC/ECDSA), and AES accelerators.",
|
|
options: [
|
|
{ value: "key_manager_dpa", label: "Maximum Protection: Hardware Key Manager (isolated from CPU) and DPA Protection", icon: "security_key_manager_dpa" },
|
|
{ value: "hardware_ecc_ecdsa", label: "Hardware Elliptic Curve Accelerator (ECC) and ECDSA Digital Signature", icon: "security_hardware_ecc_ecdsa" },
|
|
{ value: "secure_boot", label: "Secure Boot V2 and Flash Encryption (XTS-AES) against firmware cloning", icon: "security_secure_boot" },
|
|
{ value: "standard_tls", label: "Standard TLS/HTTPS Communication (Hardware AES and SHA accelerators)", icon: "security_standard_tls" },
|
|
]
|
|
},
|
|
{
|
|
id: "ecosystem",
|
|
question: "8. What is your development environment preference?",
|
|
description: "Mature chips offer broad official support in Arduino Core with hundreds of ready libraries, while newer silicon starts in ESP-IDF.",
|
|
options: [
|
|
{ value: "arduino_ready", label: "Arduino IDE Ready (mature support with vast library ecosystem)", icon: "ecosystem_arduino_ready" },
|
|
{ value: "advanced_idf", label: "Professional ESP-IDF or open to next-gen silicon (S31, H4, etc.)", icon: "ecosystem_advanced_idf" },
|
|
]
|
|
}
|
|
];
|
|
|
|
const FILTROS_PT = [
|
|
{ key: "wifi6", label: "Wi-Fi 6 (802.11ax)" },
|
|
{ key: "wifi5ghz", label: "Wi-Fi dual-band (5 GHz)" },
|
|
{ key: "bluetoothClassic", label: "Bluetooth Clássico (A2DP)" },
|
|
{ key: "leAudio", label: "Bluetooth LE Audio" },
|
|
{ key: "matter", label: "Matter / 802.15.4" },
|
|
{ key: "ethernet", label: "Ethernet MAC" },
|
|
{ key: "aiAccel", label: "Aceleração vetorial de IA" },
|
|
{ key: "psram", label: "Suporte a PSRAM" },
|
|
{ key: "mipiDisplayCamera", label: "Telas e câmeras (MIPI/RGB)" },
|
|
{ key: "usbOTG", label: "USB nativo OTG" },
|
|
{ key: "canBus", label: "Barramento CAN / TWAI" },
|
|
{ key: "dac", label: "Conversor DAC" },
|
|
{ key: "touch", label: "Touch capacitivo" },
|
|
{ key: "arduinoReady", label: "Suporte ao Arduino Core" },
|
|
{ key: "eccHardware", label: "Curvas elípticas (ECC)" },
|
|
{ key: "ecdsaHardware", label: "Assinatura ECDSA em hardware" },
|
|
{ key: "keyManager", label: "Key Manager em hardware" },
|
|
{ key: "secureBootV2", label: "Secure Boot V2" },
|
|
];
|
|
|
|
const FILTROS_EN = [
|
|
{ key: "wifi6", label: "Wi-Fi 6 (802.11ax)" },
|
|
{ key: "wifi5ghz", label: "Dual-band Wi-Fi (5 GHz)" },
|
|
{ key: "bluetoothClassic", label: "Bluetooth Classic (A2DP)" },
|
|
{ key: "leAudio", label: "Bluetooth LE Audio" },
|
|
{ key: "matter", label: "Matter / 802.15.4" },
|
|
{ key: "ethernet", label: "Ethernet MAC" },
|
|
{ key: "aiAccel", label: "AI vector acceleration" },
|
|
{ key: "psram", label: "PSRAM support" },
|
|
{ key: "mipiDisplayCamera", label: "Displays and cameras (MIPI/RGB)" },
|
|
{ key: "usbOTG", label: "Native USB OTG" },
|
|
{ key: "canBus", label: "CAN / TWAI bus" },
|
|
{ key: "dac", label: "DAC converter" },
|
|
{ key: "touch", label: "Capacitive touch" },
|
|
{ key: "arduinoReady", label: "Arduino Core support" },
|
|
{ key: "eccHardware", label: "Elliptic curves (ECC)" },
|
|
{ key: "ecdsaHardware", label: "Hardware ECDSA signature" },
|
|
{ key: "keyManager", label: "Hardware Key Manager" },
|
|
{ key: "secureBootV2", label: "Secure Boot V2" },
|
|
];
|
|
|
|
const ROTULOS_ETAPA_PT = {
|
|
category: "Aplicação",
|
|
connectivity: "Conectividade",
|
|
power: "Alimentação",
|
|
hardware: "Periféricos",
|
|
memory: "Memória",
|
|
ai: "Inteligência artificial",
|
|
security: "Segurança",
|
|
ecosystem: "Ambiente de desenvolvimento",
|
|
};
|
|
|
|
const ROTULOS_ETAPA_EN = {
|
|
category: "Application",
|
|
connectivity: "Connectivity",
|
|
power: "Power Supply",
|
|
hardware: "Peripherals",
|
|
memory: "Memory",
|
|
ai: "Artificial Intelligence",
|
|
security: "Security",
|
|
ecosystem: "Development Environment",
|
|
};
|
|
|
|
export default function Seletor() {
|
|
const pathname = usePathname();
|
|
const isEn = pathname?.startsWith("/en");
|
|
const currentLocale = isEn ? "en" : "pt";
|
|
|
|
const seriesData = isEn ? seriesDataEn : seriesDataPt;
|
|
const PRESETS = isEn ? PRESETS_EN : PRESETS_PT;
|
|
const questions = isEn ? QUESTIONS_EN : QUESTIONS_PT;
|
|
const FILTROS = isEn ? FILTROS_EN : FILTROS_PT;
|
|
const ROTULOS_ETAPA = isEn ? ROTULOS_ETAPA_EN : ROTULOS_ETAPA_PT;
|
|
|
|
const [mode, setMode] = useState("wizard"); // "wizard" | "filter"
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
const [currentAnswer, setCurrentAnswer] = useState(null);
|
|
const [activePreset, setActivePreset] = useState(null);
|
|
|
|
const [answers, setAnswers] = useState({
|
|
category: null,
|
|
connectivity: null,
|
|
power: null,
|
|
hardware: null,
|
|
memory: null,
|
|
ai: null,
|
|
security: null,
|
|
ecosystem: null,
|
|
});
|
|
|
|
const [showResults, setShowResults] = useState(false);
|
|
const [showSummary, setShowSummary] = useState(false);
|
|
|
|
// Direct Parametric Filter State (for "filter" mode)
|
|
const [filters, setFilters] = useState({
|
|
wifi6: false,
|
|
wifi5ghz: false,
|
|
bluetoothClassic: false,
|
|
leAudio: false,
|
|
matter: false,
|
|
ethernet: false,
|
|
aiAccel: false,
|
|
psram: false,
|
|
mipiDisplayCamera: false,
|
|
usbOTG: false,
|
|
canBus: false,
|
|
dac: false,
|
|
touch: false,
|
|
arduinoReady: false,
|
|
keyManager: false,
|
|
eccHardware: false,
|
|
ecdsaHardware: false,
|
|
secureBootV2: false,
|
|
});
|
|
|
|
const handleAnswer = (questionId, value) => {
|
|
setCurrentAnswer(value);
|
|
setAnswers(prev => ({ ...prev, [questionId]: value }));
|
|
setActivePreset(null);
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (currentAnswer !== null) {
|
|
if (currentStep < questions.length - 1) {
|
|
const nextStep = currentStep + 1;
|
|
setCurrentStep(nextStep);
|
|
setCurrentAnswer(answers[questions[nextStep].id] || null);
|
|
} else {
|
|
setShowSummary(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
const goBack = () => {
|
|
if (currentStep > 0) {
|
|
const prevStep = currentStep - 1;
|
|
setCurrentStep(prevStep);
|
|
setCurrentAnswer(answers[questions[prevStep].id] || null);
|
|
}
|
|
};
|
|
|
|
const resetQuiz = () => {
|
|
setCurrentStep(0);
|
|
setCurrentAnswer(null);
|
|
setShowResults(false);
|
|
setShowSummary(false);
|
|
setActivePreset(null);
|
|
setAnswers({
|
|
category: null,
|
|
connectivity: null,
|
|
power: null,
|
|
hardware: null,
|
|
memory: null,
|
|
ai: null,
|
|
security: null,
|
|
ecosystem: null,
|
|
});
|
|
};
|
|
|
|
const applyPreset = (preset) => {
|
|
setAnswers(preset.answers);
|
|
setActivePreset(preset.id);
|
|
setCurrentStep(questions.length - 1);
|
|
setCurrentAnswer(preset.answers.ecosystem);
|
|
setShowSummary(false);
|
|
setShowResults(true);
|
|
};
|
|
|
|
// Comprehensive Multi-Criteria Scoring Engine based on series data
|
|
const calculateRecommendations = () => {
|
|
const scores = {};
|
|
const reasons = {};
|
|
const alerts = {};
|
|
const r = (pt, en) => isEn ? en : pt;
|
|
|
|
Object.keys(seriesData).forEach(key => {
|
|
scores[key] = 0;
|
|
reasons[key] = [];
|
|
alerts[key] = [];
|
|
const serie = seriesData[key];
|
|
const seg = serie.seguranca || {};
|
|
|
|
const hasMatter = serie.matter === "Sim" || serie.matter === "Yes";
|
|
const hasNoWifi = !serie.wifi || serie.wifi === "Não" || serie.wifi === "No";
|
|
const hasNoBluetooth = !serie.bluetooth || serie.bluetooth === "Não" || serie.bluetooth === "No";
|
|
|
|
// 1. CATEGORIA (category)
|
|
if (answers.category === "smarthome") {
|
|
if (hasMatter) {
|
|
scores[key] += 25;
|
|
reasons[key].push(r(
|
|
"Suporte nativo a Matter para interoperabilidade residencial inteligente",
|
|
"Native Matter support for smart home interoperability"
|
|
));
|
|
}
|
|
if (serie.zigbee_thread && serie.zigbee_thread !== "Não" && serie.zigbee_thread !== "No") {
|
|
scores[key] += 20;
|
|
reasons[key].push(r(
|
|
"Rádio 802.15.4 integrado compatível com Zigbee 3.0 e Thread",
|
|
"Integrated 802.15.4 radio compatible with Zigbee 3.0 and Thread"
|
|
));
|
|
}
|
|
if (!hasNoWifi) {
|
|
scores[key] += 10;
|
|
reasons[key].push(r(
|
|
"Rádio Wi-Fi integrado para conexão direta ao roteador",
|
|
"Integrated Wi-Fi radio for direct router connection"
|
|
));
|
|
}
|
|
} else if (answers.category === "multimedia") {
|
|
if (serie.mipi_dsi || serie.mipi_csi) {
|
|
scores[key] += 35;
|
|
reasons[key].push(r(
|
|
"Barramentos avançados de alta velocidade MIPI DSI (telas) e MIPI CSI (câmeras)",
|
|
"High-speed MIPI DSI (displays) and MIPI CSI (cameras) buses"
|
|
));
|
|
} else if (serie.lcd || serie.camera) {
|
|
scores[key] += 25;
|
|
reasons[key].push(r(
|
|
"Interfaces dedicadas em hardware para telas LCD RGB e câmeras DVP",
|
|
"Dedicated hardware interfaces for RGB LCD displays and DVP cameras"
|
|
));
|
|
} else {
|
|
scores[key] -= 40;
|
|
alerts[key].push(r(
|
|
"Sem barramentos dedicados para telas coloridas ou câmeras (apenas SPI/I2C simples)",
|
|
"No dedicated buses for color displays or cameras (SPI/I2C only)"
|
|
));
|
|
}
|
|
|
|
if (serie.psram_externa && serie.psram_externa !== "Não" && serie.psram_externa !== "No") {
|
|
scores[key] += 20;
|
|
reasons[key].push(r(
|
|
"Suporte a PSRAM de alta capacidade para framebuffers gráficos e processamento de imagem",
|
|
"High-capacity PSRAM support for graphics framebuffers and image processing"
|
|
));
|
|
}
|
|
} else if (answers.category === "ai_edge") {
|
|
if (serie.aceleradores_ia) {
|
|
scores[key] += 40;
|
|
reasons[key].push(r(
|
|
`Aceleração neural e instruções vetoriais dedicadas (${serie.aceleradores_ia})`,
|
|
`Dedicated neural acceleration and vector instructions (${serie.aceleradores_ia})`
|
|
));
|
|
} else {
|
|
scores[key] -= 50;
|
|
alerts[key].push(r(
|
|
"Sem aceleradores neurais dedicados em hardware",
|
|
"No dedicated hardware neural accelerators"
|
|
));
|
|
}
|
|
if (serie.nucleos && serie.nucleos.includes("2")) {
|
|
scores[key] += 10;
|
|
reasons[key].push(r(
|
|
"Processador Dual-Core para pipeline paralelo de captura e inferência",
|
|
"Dual-core processor for parallel capture and inference pipeline"
|
|
));
|
|
}
|
|
} else if (answers.category === "wearable") {
|
|
const sleep = serie.consumo_energia?.deep_sleep;
|
|
if (sleep) {
|
|
const uaMatch = sleep.match(/(\d+)\s*µA/);
|
|
if (uaMatch && parseInt(uaMatch[1]) <= 10) {
|
|
scores[key] += 25;
|
|
reasons[key].push(r(
|
|
`Consumo em Deep Sleep ultrabaixo de apenas ${sleep} preserva baterias compactas`,
|
|
`Ultra-low Deep Sleep current of just ${sleep} preserves compact batteries`
|
|
));
|
|
}
|
|
}
|
|
if (!hasNoBluetooth) {
|
|
scores[key] += 15;
|
|
reasons[key].push(r(
|
|
"Rádio Bluetooth de alta eficiência energética para pareamento contínuo",
|
|
"Energy-efficient Bluetooth radio for continuous pairing"
|
|
));
|
|
}
|
|
if (key === "ESP32-C2" || key === "ESP32-C3" || key === "ESP32-H2") {
|
|
scores[key] += 10;
|
|
reasons[key].push(r(
|
|
"Encapsulamento compacto QFN reduz o footprint na placa",
|
|
"Compact QFN package reduces board footprint"
|
|
));
|
|
}
|
|
} else if (answers.category === "industrial") {
|
|
if (serie.ethernet || serie.ethernet_mac) {
|
|
scores[key] += 30;
|
|
reasons[key].push(r(
|
|
"Controlador MAC Ethernet integrado para conexões cabeadas industriais imunes a ruídos de RF",
|
|
"Integrated Ethernet MAC controller for industrial wired connections immune to RF noise"
|
|
));
|
|
}
|
|
if (serie.can) {
|
|
scores[key] += 25;
|
|
reasons[key].push(r(
|
|
`Interface de barramento industrial ${serie.can}`,
|
|
`Industrial bus interface ${serie.can}`
|
|
));
|
|
}
|
|
if (parseInt(serie.gpio) >= 30) {
|
|
scores[key] += 10;
|
|
reasons[key].push(r(
|
|
`Ampla contagem de portas digitais (${serie.gpio} GPIOs) para relés e atuadores`,
|
|
`Large count of digital ports (${serie.gpio} GPIOs) for relays and actuators`
|
|
));
|
|
}
|
|
} else if (answers.category === "basic_iot") {
|
|
if (key === "ESP32-C2" || key === "ESP32-C3" || key === "ESP32-C61") {
|
|
scores[key] += 30;
|
|
reasons[key].push(r(
|
|
"Excelente relação custo-benefício para sensores e atuadores conectados em escala",
|
|
"Cost-effective for connected sensors and actuators at scale"
|
|
));
|
|
}
|
|
if (!hasNoWifi && !hasNoBluetooth) {
|
|
scores[key] += 15;
|
|
reasons[key].push(r(
|
|
"Wi-Fi + BLE integrados em módulo econômico",
|
|
"Integrated Wi-Fi + BLE in an economical module"
|
|
));
|
|
}
|
|
}
|
|
|
|
// 2. CONECTIVIDADE (connectivity)
|
|
if (answers.connectivity === "wifi6_dualband") {
|
|
if (String(serie.wifi).includes("5 GHz") || String(serie.wifi).includes("dual-band")) {
|
|
scores[key] += 50;
|
|
reasons[key].push(r(
|
|
"Exclusivo Wi-Fi 6 Dual-Band (2.4 GHz e 5 GHz) para ambientes com alto ruído de RF",
|
|
"Exclusive Dual-Band Wi-Fi 6 (2.4 GHz and 5 GHz) for high-noise RF environments"
|
|
));
|
|
} else {
|
|
scores[key] -= 250;
|
|
alerts[key].push(r(
|
|
"Incompatível: Não possui rádio Wi-Fi na faixa de 5 GHz",
|
|
"Incompatible: No 5 GHz Wi-Fi radio"
|
|
));
|
|
}
|
|
} else if (answers.connectivity === "wifi6_mesh") {
|
|
if (String(serie.wifi).includes("Wi-Fi 6") && hasMatter) {
|
|
scores[key] += 40;
|
|
reasons[key].push(r(
|
|
"Wi-Fi 6 (802.11ax) somado a rádio 802.15.4 para Thread/Zigbee/Matter",
|
|
"Wi-Fi 6 (802.11ax) plus 802.15.4 radio for Thread/Zigbee/Matter"
|
|
));
|
|
} else if (hasMatter) {
|
|
scores[key] += 15;
|
|
reasons[key].push(r(
|
|
"Suporte a Matter/Thread, porém sem Wi-Fi 6",
|
|
"Matter/Thread support, but without Wi-Fi 6"
|
|
));
|
|
} else {
|
|
scores[key] -= 150;
|
|
alerts[key].push(r(
|
|
"Incompatível: Sem rádio 802.15.4 integrado para Thread/Zigbee",
|
|
"Incompatible: No integrated 802.15.4 radio for Thread/Zigbee"
|
|
));
|
|
}
|
|
} else if (answers.connectivity === "mesh_only") {
|
|
if (hasNoWifi && hasMatter) {
|
|
scores[key] += 45;
|
|
reasons[key].push(r(
|
|
"Foco 100% em 802.15.4 (Zigbee/Thread/Matter) com baixo consumo e sem rádio Wi-Fi",
|
|
"100% focus on 802.15.4 (Zigbee/Thread/Matter) with low power and no Wi-Fi radio"
|
|
));
|
|
} else if (hasMatter) {
|
|
scores[key] += 20;
|
|
reasons[key].push(r(
|
|
"Suporta redes Mesh (Zigbee/Thread), mas inclui rádio Wi-Fi adicional",
|
|
"Supports Mesh networks (Zigbee/Thread), but includes additional Wi-Fi radio"
|
|
));
|
|
} else {
|
|
scores[key] -= 180;
|
|
alerts[key].push(r(
|
|
"Incompatível: Sem rádio 802.15.4 para protocolo Thread ou Zigbee",
|
|
"Incompatible: No 802.15.4 radio for Thread or Zigbee"
|
|
));
|
|
}
|
|
} else if (answers.connectivity === "wifi_ble") {
|
|
if (!hasNoWifi && !hasNoBluetooth) {
|
|
scores[key] += 30;
|
|
reasons[key].push(r(
|
|
`Pilha completa com Wi-Fi (${serie.wifi}) e Bluetooth (${serie.bluetooth})`,
|
|
`Full stack with Wi-Fi (${serie.wifi}) and Bluetooth (${serie.bluetooth})`
|
|
));
|
|
} else if (!hasNoWifi) {
|
|
scores[key] += 10;
|
|
alerts[key].push(r(
|
|
"Possui Wi-Fi, mas não possui rádio Bluetooth integrado",
|
|
"Has Wi-Fi, but lacks integrated Bluetooth"
|
|
));
|
|
} else {
|
|
scores[key] -= 200;
|
|
alerts[key].push(r(
|
|
"Incompatível: Não possui rádio Wi-Fi integrado",
|
|
"Incompatible: No integrated Wi-Fi radio"
|
|
));
|
|
}
|
|
} else if (answers.connectivity === "bt_classic_audio") {
|
|
if (String(serie.bluetooth).includes("Classic") || String(serie.bluetooth).includes("Clássico")) {
|
|
scores[key] += 50;
|
|
reasons[key].push(r(
|
|
"Suporte a Bluetooth Clássico (BR/EDR) com perfis A2DP de áudio e SPP serial",
|
|
"Bluetooth Classic (BR/EDR) support with A2DP audio and serial SPP profiles"
|
|
));
|
|
} else {
|
|
scores[key] -= 250;
|
|
alerts[key].push(r(
|
|
"Incompatível: Não possui Bluetooth Clássico (apenas BLE, incompatível com áudio A2DP legado)",
|
|
"Incompatible: No Bluetooth Classic (BLE only, incompatible with legacy A2DP audio)"
|
|
));
|
|
}
|
|
} else if (answers.connectivity === "ethernet") {
|
|
if (serie.ethernet || serie.ethernet_mac) {
|
|
scores[key] += 45;
|
|
reasons[key].push(r(
|
|
"Controlador MAC Ethernet nativo integrado (requer apenas transceiver PHY)",
|
|
"Native integrated Ethernet MAC controller (requires only PHY transceiver)"
|
|
));
|
|
} else {
|
|
scores[key] -= 180;
|
|
alerts[key].push(r(
|
|
"Sem MAC Ethernet interno (necessita de módulo SPI Ethernet externo como W5500)",
|
|
"No internal Ethernet MAC (requires external SPI Ethernet module like W5500)"
|
|
));
|
|
}
|
|
} else if (answers.connectivity === "none_local") {
|
|
if (hasNoWifi || hasNoBluetooth || key === "ESP32-P4") {
|
|
scores[key] += 35;
|
|
reasons[key].push(r(
|
|
"Arquitetura orientada a processamento local dedicado sem custo de transceivers de rádio",
|
|
"Architecture geared towards dedicated local processing without radio transceiver cost"
|
|
));
|
|
} else {
|
|
scores[key] += 10;
|
|
}
|
|
}
|
|
|
|
// 3. ALIMENTAÇÃO (power)
|
|
if (answers.power === "battery_critical") {
|
|
const sleep = serie.consumo_energia?.deep_sleep;
|
|
if (sleep) {
|
|
const uaMatch = sleep.match(/(\d+)\s*µA/);
|
|
if (uaMatch && parseInt(uaMatch[1]) <= 10) {
|
|
scores[key] += 25;
|
|
reasons[key].push(r(
|
|
`Consumo de suspensão de ${sleep} permite operação prolongada por bateria`,
|
|
`Sleep current of ${sleep} enables extended battery operation`
|
|
));
|
|
} else {
|
|
scores[key] += 10;
|
|
reasons[key].push(r(
|
|
`Modo de baixo consumo de ${sleep}`,
|
|
`Low power mode of ${sleep}`
|
|
));
|
|
}
|
|
}
|
|
if (serie.coprocessador_ulp && serie.coprocessador_ulp !== "Não" && serie.coprocessador_ulp !== "No") {
|
|
scores[key] += 10;
|
|
reasons[key].push(r(
|
|
"Coprocessador ULP permite checar pinos sem despertar a CPU principal",
|
|
"ULP coprocessor monitors pins without waking the main CPU"
|
|
));
|
|
}
|
|
} else if (answers.power === "ulp_coprocessor") {
|
|
if (serie.coprocessador_ulp && serie.coprocessador_ulp !== "Não" && serie.coprocessador_ulp !== "No") {
|
|
scores[key] += 35;
|
|
reasons[key].push(r(
|
|
`Coprocessador ULP / CPU de Baixo Consumo (${serie.coprocessador_ulp})`,
|
|
`ULP coprocessor / Low-power core (${serie.coprocessador_ulp})`
|
|
));
|
|
} else {
|
|
scores[key] -= 30;
|
|
alerts[key].push(r(
|
|
"Sem coprocessador ULP dedicado",
|
|
"No dedicated ULP coprocessor"
|
|
));
|
|
}
|
|
} else if (answers.power === "always_on") {
|
|
if (serie.nucleos && serie.nucleos.includes("2")) {
|
|
scores[key] += 15;
|
|
reasons[key].push(r(
|
|
"Processador Dual-Core para multitarefa paralela contínua",
|
|
"Dual-Core processor for continuous parallel multitasking"
|
|
));
|
|
}
|
|
const freqMatch = serie.frequencia?.match(/(\d+)\s*MHz/);
|
|
if (freqMatch && parseInt(freqMatch[1]) >= 240) {
|
|
scores[key] += 15;
|
|
reasons[key].push(r(
|
|
`Frequência de clock de alto desempenho (${serie.frequencia})`,
|
|
`High-performance clock frequency (${serie.frequencia})`
|
|
));
|
|
}
|
|
}
|
|
|
|
// 4. HARDWARE & PERIFÉRICOS (hardware)
|
|
if (answers.hardware === "mipi_multimedia") {
|
|
if (serie.mipi_dsi || serie.mipi_csi) {
|
|
scores[key] += 40;
|
|
reasons[key].push(r(
|
|
"Interface MIPI DSI (telas) e MIPI CSI (câmeras) de altíssima taxa de transferência",
|
|
"High-throughput MIPI DSI (displays) and MIPI CSI (cameras) interface"
|
|
));
|
|
} else if (serie.lcd || serie.camera) {
|
|
scores[key] += 25;
|
|
reasons[key].push(r(
|
|
"Interfaces paralelas para LCD RGB e câmeras DVP de 8/16 bits",
|
|
"Parallel interfaces for RGB LCD and 8/16-bit DVP cameras"
|
|
));
|
|
} else {
|
|
scores[key] -= 180;
|
|
alerts[key].push(r(
|
|
"Incompatível: Sem interfaces dedicadas de tela/câmera (limitado a barramento SPI/I2C)",
|
|
"Incompatible: No dedicated display/camera interfaces (limited to SPI/I2C bus)"
|
|
));
|
|
}
|
|
} else if (answers.hardware === "usb_otg") {
|
|
if (serie.usb && String(serie.usb).includes("OTG 2.0")) {
|
|
scores[key] += 40;
|
|
reasons[key].push(r(
|
|
`Porta USB 2.0 High-Speed OTG (${serie.usb}) para emulação HID, CDC e modo Host`,
|
|
`USB 2.0 High-Speed OTG port (${serie.usb}) for HID emulation, CDC and Host mode`
|
|
));
|
|
} else if (serie.usb && String(serie.usb).includes("OTG")) {
|
|
scores[key] += 30;
|
|
reasons[key].push(r(
|
|
`Interface USB OTG 1.1 integrada (${serie.usb}) para emulação HID e Host`,
|
|
`Integrated USB OTG 1.1 interface (${serie.usb}) for HID emulation and Host mode`
|
|
));
|
|
} else {
|
|
scores[key] -= 160;
|
|
alerts[key].push(r(
|
|
"Incompatível: Sem USB OTG nativo (USB Serial/JTAG não suporta modo Host ou HID customizado)",
|
|
"Incompatible: No native USB OTG (USB Serial/JTAG does not support Host mode or custom HID)"
|
|
));
|
|
}
|
|
} else if (answers.hardware === "can_twai") {
|
|
if (serie.can && String(serie.can).includes("CAN FD")) {
|
|
scores[key] += 40;
|
|
reasons[key].push(r(
|
|
"Controlador avançado CAN-FD (Flexible Data-Rate) para alta largura de banda automotiva",
|
|
"Advanced CAN-FD (Flexible Data-Rate) controller for automotive high bandwidth"
|
|
));
|
|
} else if (serie.can) {
|
|
scores[key] += 30;
|
|
reasons[key].push(r(
|
|
`Controlador de barramento industrial ${serie.can}`,
|
|
`Industrial bus controller ${serie.can}`
|
|
));
|
|
} else {
|
|
scores[key] -= 150;
|
|
alerts[key].push(r(
|
|
"Incompatível: Sem controlador de barramento CAN / TWAI nativo",
|
|
"Incompatible: No native CAN / TWAI bus controller"
|
|
));
|
|
}
|
|
} else if (answers.hardware === "dac_audio") {
|
|
if (serie.dac && serie.dac !== "Não" && serie.dac !== "No") {
|
|
scores[key] += 45;
|
|
reasons[key].push(r(
|
|
`Conversores Digital-Analógico embutidos (${serie.dac}) para saída de áudio direta sem DAC externo`,
|
|
`Built-in Digital-to-Analog converters (${serie.dac}) for direct audio output without external DAC`
|
|
));
|
|
} else {
|
|
scores[key] -= 150;
|
|
alerts[key].push(r(
|
|
"Incompatível: Não possui conversor DAC interno de áudio (requer codec I2S externo)",
|
|
"Incompatible: No internal audio DAC converter (requires external I2S codec)"
|
|
));
|
|
}
|
|
} else if (answers.hardware === "many_gpios") {
|
|
const gpios = parseInt(serie.gpio) || 0;
|
|
if (gpios >= 40) {
|
|
scores[key] += 35;
|
|
reasons[key].push(r(
|
|
`Excepcional número de pinos disponíveis (${gpios} GPIOs)`,
|
|
`Exceptional number of available pins (${gpios} GPIOs)`
|
|
));
|
|
} else if (gpios >= 28) {
|
|
scores[key] += 15;
|
|
reasons[key].push(r(
|
|
`Quantidade moderada de portas (${gpios} GPIOs)`,
|
|
`Moderate pin count (${gpios} GPIOs)`
|
|
));
|
|
} else {
|
|
scores[key] -= 60;
|
|
alerts[key].push(r(
|
|
`Contagem restrita de pinos (${gpios} GPIOs)`,
|
|
`Restricted pin count (${gpios} GPIOs)`
|
|
));
|
|
}
|
|
} else if (answers.hardware === "standard_sensors") {
|
|
scores[key] += 10;
|
|
}
|
|
|
|
// 5. MEMÓRIA & PSRAM (memory)
|
|
if (answers.memory === "heavy_psram") {
|
|
if (serie.psram_externa && (String(serie.psram_externa).includes("64 MB") || String(serie.psram_externa).includes("1 GB"))) {
|
|
scores[key] += 40;
|
|
reasons[key].push(r(
|
|
`Expansão massiva de memória PSRAM (${serie.psram_externa})`,
|
|
`Massive PSRAM memory expansion (${serie.psram_externa})`
|
|
));
|
|
} else if (serie.psram_externa && serie.psram_externa !== "Não" && serie.psram_externa !== "No") {
|
|
scores[key] += 25;
|
|
reasons[key].push(r(
|
|
`Suporte a memória PSRAM externa (${serie.psram_externa})`,
|
|
`Support for external PSRAM memory (${serie.psram_externa})`
|
|
));
|
|
} else {
|
|
scores[key] -= 180;
|
|
alerts[key].push(r(
|
|
"Incompatível: Sem suporte a memória PSRAM externa ou integrada",
|
|
"Incompatible: No support for external or integrated PSRAM"
|
|
));
|
|
}
|
|
} else if (answers.memory === "moderate_sram") {
|
|
const sramMatch = serie.memoria_sram?.match(/(\d+)\s*KB/);
|
|
if (sramMatch && parseInt(sramMatch[1]) >= 380) {
|
|
scores[key] += 15;
|
|
reasons[key].push(r(
|
|
`SRAM interna confortável (${serie.memoria_sram})`,
|
|
`Comfortable internal SRAM (${serie.memoria_sram})`
|
|
));
|
|
} else {
|
|
scores[key] += 8;
|
|
}
|
|
} else if (answers.memory === "minimal_cost") {
|
|
if (key === "ESP32-C2" || key === "ESP32-C3" || key === "ESP32-H2") {
|
|
scores[key] += 25;
|
|
reasons[key].push(r(
|
|
"Memória enxuta e silício compacto para menor custo unitário",
|
|
"Lean memory and compact silicon for lower unit cost"
|
|
));
|
|
} else {
|
|
scores[key] += 5;
|
|
}
|
|
}
|
|
|
|
// 6. INTELIGÊNCIA ARTIFICIAL (ai)
|
|
if (answers.ai === "ai_vector") {
|
|
if (serie.aceleradores_ia) {
|
|
scores[key] += 45;
|
|
reasons[key].push(r(
|
|
"Acelerador neural de silício e instruções vetoriais para inferência veloz",
|
|
"Silicon neural accelerator and vector instructions for fast inference"
|
|
));
|
|
} else {
|
|
scores[key] -= 180;
|
|
alerts[key].push(r(
|
|
"Incompatível: Sem aceleração vetorial por hardware para IA na borda",
|
|
"Incompatible: No hardware vector acceleration for edge AI"
|
|
));
|
|
}
|
|
} else if (answers.ai === "standard") {
|
|
scores[key] += 5;
|
|
}
|
|
|
|
// 7. SEGURANÇA (security)
|
|
const temECDSA = String(seg.ecdsa).startsWith("Sim") || String(seg.ecdsa).startsWith("Yes");
|
|
const temECC = String(seg.ecc).startsWith("Sim") || String(seg.ecc).startsWith("Yes");
|
|
const temAES = String(seg.aes).startsWith("AES");
|
|
const temKeyManager = seg.key_manager === "Sim" || seg.key_manager === "Yes";
|
|
const temDPA = seg.protecao_dpa === "Sim" || seg.protecao_dpa === "Yes";
|
|
|
|
if (answers.security === "key_manager_dpa") {
|
|
if (temKeyManager && temDPA) {
|
|
scores[key] += 45;
|
|
reasons[key].push(r(
|
|
"Key Manager em silício (chaves isoladas da CPU) e proteção contra análise de consumo DPA",
|
|
"Silicon Key Manager (keys isolated from CPU) and DPA side-channel protection"
|
|
));
|
|
} else if (temKeyManager) {
|
|
scores[key] += 35;
|
|
reasons[key].push(r(
|
|
"Periférico Key Manager integrado para isolamento total de chaves privadas",
|
|
"Integrated Key Manager peripheral for complete private key isolation"
|
|
));
|
|
} else if (temDPA) {
|
|
scores[key] += 20;
|
|
reasons[key].push(r(
|
|
"Proteção física DPA em hardware contra ataques de canal lateral",
|
|
"Hardware DPA physical protection against side-channel attacks"
|
|
));
|
|
} else {
|
|
scores[key] -= 160;
|
|
alerts[key].push(r(
|
|
"Incompatível: Sem Key Manager nem proteção física contra DPA",
|
|
"Incompatible: No Key Manager or physical DPA protection"
|
|
));
|
|
}
|
|
} else if (answers.security === "hardware_ecc_ecdsa") {
|
|
if (temECDSA) {
|
|
scores[key] += 40;
|
|
reasons[key].push(r(
|
|
`Acelerador de assinatura digital ECDSA determinístico em hardware (${seg.ecc})`,
|
|
`Deterministic hardware ECDSA digital signature accelerator (${seg.ecc})`
|
|
));
|
|
} else if (temECC) {
|
|
scores[key] += 30;
|
|
reasons[key].push(r(
|
|
`Coprocessador de curvas elípticas ECC em hardware (${seg.ecc})`,
|
|
`Hardware ECC elliptic curve coprocessor (${seg.ecc})`
|
|
));
|
|
} else {
|
|
scores[key] -= 220;
|
|
alerts[key].push(r(
|
|
"Incompatível: Não possui acelerador de hardware para curvas elípticas (ECC/ECDSA)",
|
|
"Incompatible: No hardware accelerator for elliptic curves (ECC/ECDSA)"
|
|
));
|
|
}
|
|
} else if (answers.security === "secure_boot") {
|
|
if (String(seg.secure_boot).includes("V2")) {
|
|
scores[key] += 25;
|
|
reasons[key].push(r(
|
|
`Secure Boot ${seg.secure_boot} com criptografia de flash ${seg.criptografia_flash}`,
|
|
`Secure Boot ${seg.secure_boot} with flash encryption ${seg.criptografia_flash}`
|
|
));
|
|
} else if (String(seg.secure_boot).includes("V1")) {
|
|
scores[key] += 5;
|
|
alerts[key].push(r(
|
|
"Secure Boot em esquema V1 (legado com limitações de segurança)",
|
|
"Secure Boot V1 scheme (legacy with security limitations)"
|
|
));
|
|
} else {
|
|
scores[key] -= 80;
|
|
alerts[key].push(r(
|
|
"Sem Secure Boot V2 estável",
|
|
"No stable Secure Boot V2"
|
|
));
|
|
}
|
|
} else if (answers.security === "standard_tls") {
|
|
if (temAES) {
|
|
scores[key] += 15;
|
|
reasons[key].push(r(
|
|
`Criptografia de tráfego TLS acelerada por hardware (${seg.aes})`,
|
|
`Hardware-accelerated TLS traffic encryption (${seg.aes})`
|
|
));
|
|
} else {
|
|
scores[key] += 5;
|
|
}
|
|
}
|
|
|
|
// 8. ECOSSISTEMA (ecosystem)
|
|
if (answers.ecosystem === "arduino_ready") {
|
|
if (String(serie.arduino_core).startsWith("Sim") || String(serie.arduino_core).startsWith("Yes")) {
|
|
const countMatch = serie.arduino_core.match(/(\d+)\s*(definições|board|definitions)/);
|
|
const count = countMatch ? parseInt(countMatch[1]) : 1;
|
|
if (count >= 20) {
|
|
scores[key] += 25;
|
|
reasons[key].push(r(
|
|
`Excelente maturidade no core oficial Arduino (${count} variantes de placas)`,
|
|
`Excellent maturity in the official Arduino core (${count} board variants)`
|
|
));
|
|
} else {
|
|
scores[key] += 15;
|
|
reasons[key].push(r(
|
|
`Suporte no core oficial Arduino (${count} placa registrada)`,
|
|
`Support in the official Arduino core (${count} registered board)`
|
|
));
|
|
}
|
|
} else {
|
|
scores[key] -= 200;
|
|
alerts[key].push(r(
|
|
"Incompatível: Ainda não suportado no core oficial do Arduino (desenvolvimento restrito ao ESP-IDF)",
|
|
"Incompatible: Not yet supported in the official Arduino core (development restricted to ESP-IDF)"
|
|
));
|
|
}
|
|
} else if (answers.ecosystem === "advanced_idf") {
|
|
scores[key] += 15;
|
|
}
|
|
});
|
|
|
|
return { scores, reasons, alerts };
|
|
};
|
|
|
|
// Parametric Direct Filter Matcher
|
|
const filteredChips = useMemo(() => {
|
|
if (mode !== "filter") return [];
|
|
|
|
return Object.entries(seriesData).filter(([_, serie]) => {
|
|
const seg = serie.seguranca || {};
|
|
const hasMatter = serie.matter === "Sim" || serie.matter === "Yes";
|
|
|
|
if (filters.wifi6 && !String(serie.wifi).includes("Wi-Fi 6") && !String(serie.wifi).includes("802.11ax")) return false;
|
|
if (filters.wifi5ghz && (!serie.wifi || !String(serie.wifi).includes("5 GHz"))) return false;
|
|
if (filters.bluetoothClassic && (!serie.bluetooth || (!String(serie.bluetooth).includes("Classic") && !String(serie.bluetooth).includes("Clássico")))) return false;
|
|
if (filters.leAudio && (!serie.bluetooth || !String(serie.bluetooth).includes("LE Audio"))) return false;
|
|
if (filters.matter && !hasMatter) return false;
|
|
if (filters.ethernet && !serie.ethernet && !serie.ethernet_mac) return false;
|
|
if (filters.aiAccel && !serie.aceleradores_ia) return false;
|
|
if (filters.psram && (!serie.psram_externa || serie.psram_externa === "Não" || serie.psram_externa === "No")) return false;
|
|
if (filters.mipiDisplayCamera && !serie.mipi_dsi && !serie.mipi_csi && !serie.lcd && !serie.camera) return false;
|
|
if (filters.usbOTG && (!serie.usb || !String(serie.usb).includes("OTG"))) return false;
|
|
if (filters.canBus && !serie.can) return false;
|
|
if (filters.dac && (!serie.dac || serie.dac === "Não" || serie.dac === "No")) return false;
|
|
if (filters.touch && (!serie.touch || serie.touch === "Não" || serie.touch === "No")) return false;
|
|
if (filters.arduinoReady && (!serie.arduino_core || (!String(serie.arduino_core).startsWith("Sim") && !String(serie.arduino_core).startsWith("Yes")))) return false;
|
|
if (filters.keyManager && seg.key_manager !== "Sim" && seg.key_manager !== "Yes") return false;
|
|
if (filters.eccHardware && (!seg.ecc || seg.ecc === "Não" || seg.ecc === "No")) return false;
|
|
if (filters.ecdsaHardware && (!seg.ecdsa || (!String(seg.ecdsa).startsWith("Sim") && !String(seg.ecdsa).startsWith("Yes")))) return false;
|
|
if (filters.secureBootV2 && (!seg.secure_boot || !String(seg.secure_boot).includes("V2"))) return false;
|
|
|
|
return true;
|
|
});
|
|
}, [mode, filters, seriesData]);
|
|
|
|
const { scores: allScores, reasons: allReasons, alerts: allAlerts } = useMemo(() => {
|
|
if (showResults && Object.values(answers).every(a => a !== null)) {
|
|
return calculateRecommendations();
|
|
}
|
|
return { scores: {}, reasons: {}, alerts: {} };
|
|
}, [showResults, answers, seriesData]);
|
|
|
|
const recommendations = useMemo(() => {
|
|
const entries = Object.entries(allScores)
|
|
.filter(([_, score]) => score > 0)
|
|
.sort(([, a], [, b]) => b - a);
|
|
|
|
const maxScore = entries.length > 0 ? entries[0][1] : 1;
|
|
|
|
return entries.map(([key, score]) => ({
|
|
key,
|
|
score,
|
|
percentage: Math.min(100, Math.round((score / maxScore) * 100)),
|
|
}));
|
|
}, [allScores]);
|
|
|
|
const topThree = recommendations.slice(0, 3);
|
|
const otherRecommendations = recommendations.slice(3);
|
|
|
|
const progress = ((currentStep + 1) / questions.length) * 100;
|
|
|
|
const filtrosAtivos = FILTROS.filter(({ key }) => filters[key]).length;
|
|
const limparFiltros = () => setFilters(Object.fromEntries(FILTROS.map(({ key }) => [key, false])));
|
|
|
|
// O texto das perguntas vem numerado ("1. Qual é..."); a interface já mostra a etapa.
|
|
const semNumero = (texto) => texto.replace(/^\d+\.\s*/, "");
|
|
|
|
const pergunta = questions[currentStep];
|
|
|
|
return (
|
|
<div className="site-canvas min-h-screen">
|
|
<Header />
|
|
|
|
<main id="conteudo">
|
|
<PageHero
|
|
trail={isEn ? [{ label: "Chip selector" }] : [{ label: "Seletor de chip" }]}
|
|
icon={Sparkles}
|
|
label={isEn ? "Smart selector" : "Seletor inteligente"}
|
|
title={isEn ? "Which ESP32 to choose?" : "Qual ESP32 escolher?"}
|
|
lede={isEn
|
|
? "Answer eight questions about your project, or select required features, to find the best matching models with technical rationale."
|
|
: "Responda a oito perguntas sobre o projeto, ou marque os recursos obrigatórios, e veja os modelos mais compatíveis com a justificativa técnica."}
|
|
>
|
|
<div role="group" aria-label={isEn ? "Selection mode" : "Modo de seleção"} className="flex flex-wrap gap-2">
|
|
<button type="button" onClick={() => setMode("wizard")} aria-pressed={mode === "wizard"} className="chip-btn">
|
|
<Sparkles className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "Guided wizard" : "Assistente guiado"}
|
|
</button>
|
|
<button type="button" onClick={() => setMode("filter")} aria-pressed={mode === "filter"} className="chip-btn">
|
|
<Filter className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "Feature filter" : "Filtro por recursos"}
|
|
</button>
|
|
</div>
|
|
</PageHero>
|
|
|
|
{/* Assistente: pergunta */}
|
|
{mode === "wizard" && !showResults && !showSummary && (
|
|
<section className="shell pb-6 pt-10" aria-labelledby="pergunta-titulo">
|
|
<div className="panel mx-auto max-w-3xl p-5 sm:p-9">
|
|
<div className="flex items-center justify-between gap-4 text-sm">
|
|
<p className="font-medium text-ink">
|
|
{isEn ? (
|
|
<>Step {currentStep + 1} of {questions.length}</>
|
|
) : (
|
|
<>Etapa {currentStep + 1} de {questions.length}</>
|
|
)}
|
|
<span className="text-muted"> · {ROTULOS_ETAPA[pergunta.id]}</span>
|
|
</p>
|
|
<span className="tabular-nums text-muted">{Math.round(progress)}%</span>
|
|
</div>
|
|
<div
|
|
role="progressbar"
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
aria-valuenow={Math.round(progress)}
|
|
aria-label={isEn ? "Quiz progress" : "Progresso do questionário"}
|
|
className="mt-3 h-1.5 overflow-hidden rounded-full bg-line"
|
|
>
|
|
<div className="h-full rounded-full bg-accent transition-[width] duration-500 ease-out" style={{ width: `${progress}%` }} />
|
|
</div>
|
|
|
|
<h2 id="pergunta-titulo" className="section-title mt-8">{semNumero(pergunta.question)}</h2>
|
|
<p className="mt-3 text-sm leading-relaxed text-muted sm:text-base">{pergunta.description}</p>
|
|
|
|
<div role="radiogroup" aria-labelledby="pergunta-titulo" className="mt-7 grid gap-2.5">
|
|
{pergunta.options.map((option) => {
|
|
const isSelected = currentAnswer === option.value;
|
|
return (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
role="radio"
|
|
aria-checked={isSelected}
|
|
onClick={() => handleAnswer(pergunta.id, option.value)}
|
|
className={`flex min-h-14 items-center gap-4 rounded-xl border p-3.5 text-left transition-colors sm:p-4 ${
|
|
isSelected ? "border-accent bg-accent-soft" : "border-line-strong bg-surface hover:bg-surface-2"
|
|
}`}
|
|
>
|
|
<span className={`grid h-10 w-10 shrink-0 place-items-center rounded-full ${isSelected ? "bg-accent text-accent-contrast" : "bg-surface-2 text-accent"}`}>
|
|
{getOptionIcon(pergunta.id, option.value)}
|
|
</span>
|
|
<span className="grow text-sm font-medium leading-snug text-ink sm:text-base">{option.label}</span>
|
|
{isSelected && <Check className="h-5 w-5 shrink-0 text-accent" aria-hidden="true" />}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="mt-8 flex flex-col-reverse items-stretch gap-3 border-t border-line pt-6 sm:flex-row sm:items-center sm:justify-between">
|
|
<button type="button" onClick={goBack} disabled={currentStep === 0} className="btn btn-ghost disabled:cursor-not-allowed disabled:opacity-40">
|
|
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "Back" : "Voltar"}
|
|
</button>
|
|
<button type="button" onClick={resetQuiz} className="inline-flex min-h-11 items-center justify-center gap-1.5 text-sm font-medium text-muted hover:text-ink">
|
|
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "Restart" : "Recomeçar"}
|
|
</button>
|
|
<button type="button" onClick={handleNext} disabled={currentAnswer === null} className="btn btn-primary disabled:cursor-not-allowed disabled:opacity-40">
|
|
{currentStep === questions.length - 1
|
|
? (isEn ? "Review answers" : "Revisar respostas")
|
|
: (isEn ? "Next" : "Avançar")}
|
|
<ChevronRight className="h-4 w-4" aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Atalhos por aplicação */}
|
|
{mode === "wizard" && !showResults && !showSummary && currentStep === 0 && (
|
|
<section className="shell pb-6 pt-10" aria-labelledby="atalhos-titulo">
|
|
<h2 id="atalhos-titulo" className="text-base font-semibold text-ink">
|
|
{isEn ? "Or start with a ready-made profile" : "Ou comece por um perfil pronto"}
|
|
</h2>
|
|
<p className="mt-1 text-sm text-muted">
|
|
{isEn
|
|
? "Applies all eight answers at once and displays recommendations. You can adjust them anytime."
|
|
: "Aplica as oito respostas de uma vez e mostra o resultado. Você pode ajustar depois."}
|
|
</p>
|
|
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
{PRESETS.map((preset) => (
|
|
<button
|
|
key={preset.id}
|
|
type="button"
|
|
onClick={() => applyPreset(preset)}
|
|
className="card card-interactive group flex items-start gap-4 p-4 text-left sm:p-5"
|
|
>
|
|
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-accent-soft text-accent">
|
|
{iconMap[`category_${preset.answers.category}`] || <Sparkles className="h-5 w-5" aria-hidden="true" />}
|
|
</span>
|
|
<span className="min-w-0">
|
|
<span className="block font-semibold text-ink">{preset.title}</span>
|
|
<span className="mt-0.5 block text-sm text-muted">{preset.description}</span>
|
|
<span className="mt-2 inline-flex items-center gap-1 text-sm font-medium text-accent">
|
|
{isEn ? "View recommendation" : "Ver recomendação"}
|
|
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" aria-hidden="true" />
|
|
</span>
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Assistente: revisão */}
|
|
{mode === "wizard" && showSummary && !showResults && (
|
|
<section className="shell pb-6 pt-10" aria-labelledby="resumo-titulo">
|
|
<div className="panel mx-auto max-w-4xl p-5 sm:p-9">
|
|
<h2 id="resumo-titulo" className="section-title">
|
|
{isEn ? "Review your answers" : "Revise suas respostas"}
|
|
</h2>
|
|
<p className="mt-3 text-sm text-muted sm:text-base">
|
|
{isEn
|
|
? "Review your choices before computing compatibility for each series."
|
|
: "Confira as escolhas antes de calcular a compatibilidade com cada série."}
|
|
</p>
|
|
|
|
<ul className="mt-7 grid gap-3 md:grid-cols-2">
|
|
{questions.map((question) => {
|
|
const answer = answers[question.id];
|
|
const selectedOption = question.options.find((opt) => opt.value === answer);
|
|
return (
|
|
<li key={question.id} className="tile flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<p className="text-xs font-medium text-muted">{ROTULOS_ETAPA[question.id]}</p>
|
|
{selectedOption && <p className="mt-1 text-sm font-medium leading-snug text-ink">{selectedOption.label}</p>}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setCurrentStep(questions.findIndex((q) => q.id === question.id));
|
|
setCurrentAnswer(answers[question.id]);
|
|
setShowSummary(false);
|
|
}}
|
|
className="grid h-11 w-11 shrink-0 place-items-center rounded-full text-muted transition-colors hover:bg-surface hover:text-accent"
|
|
aria-label={isEn ? `Edit answer: ${ROTULOS_ETAPA[question.id]}` : `Editar resposta: ${ROTULOS_ETAPA[question.id]}`}
|
|
>
|
|
<Edit2 className="h-4 w-4" aria-hidden="true" />
|
|
</button>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
|
|
<div className="mt-8 flex flex-col-reverse gap-3 border-t border-line pt-6 sm:flex-row sm:justify-between">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setCurrentStep(questions.length - 1);
|
|
setCurrentAnswer(answers[questions[questions.length - 1].id]);
|
|
setShowSummary(false);
|
|
}}
|
|
className="btn btn-ghost"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "Back" : "Voltar"}
|
|
</button>
|
|
<button type="button" onClick={() => setShowResults(true)} className="btn btn-primary">
|
|
<Sparkles className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "View recommendations" : "Ver recomendações"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Assistente: resultado */}
|
|
{mode === "wizard" && showResults && recommendations.length > 0 && (
|
|
<section className="shell pb-6 pt-10" aria-labelledby="resultado-titulo">
|
|
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-end">
|
|
<div>
|
|
<h2 id="resultado-titulo" className="section-title">
|
|
{isEn ? "Your recommendations" : "Suas recomendações"}
|
|
</h2>
|
|
<p role="status" className="mt-2 text-sm text-muted">
|
|
{isEn
|
|
? `${recommendations.length} matching models, ranked from most to least recommended.`
|
|
: `${recommendations.length} modelos compatíveis, do mais ao menos indicado.`}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
<button type="button" onClick={() => { setShowResults(false); setShowSummary(true); }} className="btn btn-ghost btn-sm">
|
|
<Edit2 className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "Adjust answers" : "Ajustar respostas"}
|
|
</button>
|
|
<button type="button" onClick={resetQuiz} className="btn btn-ghost btn-sm">
|
|
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "Restart" : "Recomeçar"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<ol className="mt-8 grid gap-4 lg:grid-cols-3">
|
|
{topThree.map((rec, index) => {
|
|
const seriesKey = rec.key;
|
|
const serie = seriesData[seriesKey];
|
|
const matchReasons = allReasons[seriesKey] || [];
|
|
const chipAlerts = allAlerts[seriesKey] || [];
|
|
const primeiro = index === 0;
|
|
const rankLabel = isEn
|
|
? `${index + 1}${index === 0 ? "st" : index === 1 ? "nd" : index === 2 ? "rd" : "th"} recommendation`
|
|
: `${index + 1}ª recomendação`;
|
|
return (
|
|
<li key={seriesKey} className={`card flex flex-col p-5 sm:p-6 ${primeiro ? "border-accent ring-2 ring-accent/20" : ""}`}>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className={`chip ${primeiro ? "chip-accent" : ""}`}>
|
|
{primeiro && <Award className="h-3.5 w-3.5" aria-hidden="true" />}
|
|
{rankLabel}
|
|
</span>
|
|
<span className="text-sm text-muted">
|
|
{isEn ? "Match" : "Afinidade"} <strong className="font-semibold text-ink">{rec.percentage}%</strong>
|
|
</span>
|
|
</div>
|
|
|
|
<h3 className="mt-4 font-display text-2xl font-semibold tracking-[-0.035em] text-ink">{seriesKey}</h3>
|
|
<p className="mt-1 line-clamp-2 text-sm text-muted">{serie.descricao}</p>
|
|
|
|
<div className="mt-5 h-1.5 overflow-hidden rounded-full bg-line" aria-hidden="true">
|
|
<div className="h-full rounded-full bg-accent" style={{ width: `${rec.percentage}%` }} />
|
|
</div>
|
|
|
|
<h4 className="mt-6 text-sm font-semibold text-ink">
|
|
{isEn ? "Why it fits" : "Por que combina"}
|
|
</h4>
|
|
<ul className="mt-2 space-y-2">
|
|
{matchReasons.slice(0, 3).map((reason) => (
|
|
<li key={reason} className="flex items-start gap-2 text-sm leading-relaxed text-ink-soft">
|
|
<Check className="mt-1 h-3.5 w-3.5 shrink-0 text-ok" aria-hidden="true" />
|
|
{reason}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
{chipAlerts.length > 0 && (
|
|
<div className="callout callout-warn mt-5">
|
|
<p className="flex items-center gap-1.5 font-semibold text-ink">
|
|
<AlertTriangle className="h-4 w-4 text-gold" aria-hidden="true" />
|
|
{isEn ? "Attention points" : "Pontos de atenção"}
|
|
</p>
|
|
<ul className="mt-1.5 list-disc space-y-1 pl-5">
|
|
{chipAlerts.slice(0, 2).map((alert) => (
|
|
<li key={alert}>{alert}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
<dl className="mt-5 grid grid-cols-2 gap-2">
|
|
{[
|
|
[isEn ? "Architecture" : "Arquitetura", serie.arquitetura.split(" ")[0]],
|
|
["Clock", serie.frequencia],
|
|
["GPIO", isEn ? `${serie.gpio} pins` : `${serie.gpio} pinos`],
|
|
["SRAM", serie.memoria_sram],
|
|
].map(([rotulo, valor]) => (
|
|
<div key={rotulo} className="tile !p-3">
|
|
<dt className="text-xs text-muted">{rotulo}</dt>
|
|
<dd className="mt-0.5 truncate text-sm font-medium text-ink">{valor}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
|
|
<Link href={getLocalizedHref(`/series/${seriesKey}`, currentLocale)} className={`btn mt-6 w-full ${primeiro ? "btn-primary" : "btn-ghost"}`}>
|
|
{isEn ? `Open ${seriesKey} datasheet` : `Abrir a ficha do ${seriesKey}`}
|
|
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
|
|
<div className="panel mt-10 p-5 sm:p-8">
|
|
<div className="flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
|
|
<div>
|
|
<h3 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
|
{isEn ? "The finalists side by side" : "Os finalistas lado a lado"}
|
|
</h3>
|
|
<p className="mt-1 text-sm text-muted">
|
|
{isEn
|
|
? "Key specifications of the top three recommendations."
|
|
: "As principais especificações das três primeiras recomendações."}
|
|
</p>
|
|
</div>
|
|
<Link href={getLocalizedHref(`/comparacao?chips=${topThree.map((t) => t.key).join(",")}`, currentLocale)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
|
{isEn ? "Open in full comparison" : "Abrir no comparador completo"}
|
|
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="mt-6 overflow-x-auto">
|
|
<table className="w-full min-w-[32rem] border-collapse text-left text-sm">
|
|
<caption className="sr-only">
|
|
{isEn ? "Comparison of the three recommended series" : "Comparação das três séries recomendadas"}
|
|
</caption>
|
|
<thead>
|
|
<tr className="border-b border-line">
|
|
<th scope="col" className="px-3 py-3 font-medium text-muted">
|
|
{isEn ? "Parameter" : "Parâmetro"}
|
|
</th>
|
|
{topThree.map((rec) => (
|
|
<th key={rec.key} scope="col" className="px-3 py-3 font-display text-base font-semibold text-ink">{rec.key}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-line text-ink-soft">
|
|
{[
|
|
[isEn ? "Architecture" : "Arquitetura", (c) => c.arquitetura],
|
|
["Clock", (c) => c.frequencia],
|
|
["Wi-Fi", (c) => c.wifi],
|
|
["Bluetooth", (c) => c.bluetooth],
|
|
["Matter / 802.15.4", (c) => ((c.matter === "Sim" || c.matter === "Yes") ? (isEn ? "Yes" : "Sim") : (isEn ? "No" : "Não"))],
|
|
[isEn ? "AI Acceleration" : "Aceleração de IA", (c) => (c.aceleradores_ia ? (isEn ? "Vector" : "Vetorial") : (isEn ? "No" : "Não"))],
|
|
[isEn ? "External PSRAM" : "PSRAM externa", (c) => (c.psram_externa && c.psram_externa !== "Não" && c.psram_externa !== "No" ? c.psram_externa : (isEn ? "No" : "Não"))],
|
|
["Arduino Core", (c) => ((String(c.arduino_core).startsWith("Sim") || String(c.arduino_core).startsWith("Yes")) ? (isEn ? "Supported" : "Suportado") : (isEn ? "ESP-IDF only" : "Só ESP-IDF"))],
|
|
].map(([rotulo, obter]) => (
|
|
<tr key={rotulo}>
|
|
<th scope="row" className="px-3 py-3 align-top font-medium text-muted">{rotulo}</th>
|
|
{topThree.map((rec) => (
|
|
<td key={rec.key} className="px-3 py-3 align-top">{obter(seriesData[rec.key])}</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{otherRecommendations.length > 0 && (
|
|
<div className="mt-10">
|
|
<h3 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
|
{isEn ? "Other evaluated alternatives" : "Outras alternativas avaliadas"}
|
|
</h3>
|
|
<ul className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
{otherRecommendations.map((rec) => (
|
|
<li key={rec.key}>
|
|
<Link href={getLocalizedHref(`/series/${rec.key}`, currentLocale)} className="card card-interactive flex items-center justify-between gap-3 px-4 py-3.5">
|
|
<span className="font-display text-lg font-semibold tracking-[-0.03em] text-ink">{rec.key}</span>
|
|
<span className="text-sm text-muted">{rec.percentage}%</span>
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{mode === "wizard" && showResults && recommendations.length === 0 && (
|
|
<section className="shell pb-6 pt-10">
|
|
<div className="card mx-auto max-w-2xl px-6 py-14 text-center">
|
|
<HelpCircle className="mx-auto h-10 w-10 text-gold" aria-hidden="true" />
|
|
<h2 className="mt-4 font-display text-2xl font-semibold text-ink">
|
|
{isEn ? "No chip meets all requirements" : "Nenhum chip atende a todos os requisitos"}
|
|
</h2>
|
|
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
|
|
{isEn
|
|
? "This combination has criteria that no series satisfies simultaneously. Try relaxing one of the answers."
|
|
: "Esta combinação tem critérios que nenhuma série cumpre ao mesmo tempo. Flexibilize uma das respostas e tente de novo."}
|
|
</p>
|
|
<div className="mt-6 flex flex-col justify-center gap-3 sm:flex-row">
|
|
<button type="button" onClick={() => { setShowResults(false); setShowSummary(true); }} className="btn btn-primary">
|
|
<Edit2 className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "Adjust answers" : "Ajustar respostas"}
|
|
</button>
|
|
<button type="button" onClick={resetQuiz} className="btn btn-ghost">
|
|
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
|
{isEn ? "Restart" : "Recomeçar"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Filtro por recursos */}
|
|
{mode === "filter" && (
|
|
<section className="shell pb-6 pt-10" aria-labelledby="filtro-titulo">
|
|
<div className="panel p-5 sm:p-8">
|
|
<div className="flex flex-col justify-between gap-3 sm:flex-row sm:items-end">
|
|
<div className="max-w-xl">
|
|
<h2 id="filtro-titulo" className="section-title">
|
|
{isEn ? "Check required features" : "Marque o que é obrigatório"}
|
|
</h2>
|
|
<p className="mt-2 text-sm text-muted">
|
|
{isEn
|
|
? "Only series that have all selected features are kept."
|
|
: "Só ficam as séries que têm todos os recursos marcados."}
|
|
</p>
|
|
</div>
|
|
{filtrosAtivos > 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">
|
|
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
|
{isEn
|
|
? `Clear ${filtrosAtivos} ${filtrosAtivos === 1 ? "filter" : "filters"}`
|
|
: `Limpar ${filtrosAtivos} ${filtrosAtivos === 1 ? "filtro" : "filtros"}`}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-6 flex flex-wrap gap-2" role="group" aria-label={isEn ? "Features" : "Recursos"}>
|
|
{FILTROS.map(({ key, label }) => (
|
|
<button key={key} type="button" onClick={() => setFilters((prev) => ({ ...prev, [key]: !prev[key] }))} aria-pressed={filters[key]} className="chip-btn">
|
|
{filters[key] && <Check className="h-4 w-4" aria-hidden="true" />}
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<h3 role="status" aria-live="polite" className="mt-8 text-base font-semibold text-ink">
|
|
{isEn
|
|
? `${filteredChips.length} of ${Object.keys(seriesData).length} series match`
|
|
: `${filteredChips.length} de ${Object.keys(seriesData).length} séries compatíveis`}
|
|
</h3>
|
|
|
|
{filteredChips.length > 0 ? (
|
|
<div className="mt-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{filteredChips.map(([seriesKey, serie]) => (
|
|
<Link key={seriesKey} href={getLocalizedHref(`/series/${seriesKey}`, currentLocale)} className="card card-interactive group flex flex-col p-5 sm:p-6">
|
|
<h4 className="font-display text-2xl font-semibold tracking-[-0.035em] text-ink">{seriesKey}</h4>
|
|
<p className="mt-1 line-clamp-2 min-h-[2.9rem] text-sm text-muted">{serie.descricao}</p>
|
|
<dl className="mt-4 divide-y divide-line border-y border-line text-sm">
|
|
{[
|
|
["Wi-Fi", serie.wifi],
|
|
["Matter / Thread", (serie.matter === "Sim" || serie.matter === "Yes") ? (isEn ? "Yes" : "Sim") : (isEn ? "No" : "Não")],
|
|
["Clock", serie.frequencia],
|
|
["GPIO", isEn ? `${serie.gpio} pins` : `${serie.gpio} pinos`],
|
|
["PSRAM", (serie.psram_externa && serie.psram_externa !== "Não" && serie.psram_externa !== "No") ? serie.psram_externa : (isEn ? "No" : "Não")],
|
|
].map(([rotulo, valor]) => (
|
|
<div key={rotulo} className="flex items-baseline justify-between gap-4 py-2">
|
|
<dt className="text-muted">{rotulo}</dt>
|
|
<dd className="truncate text-right font-medium text-ink">{valor}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
<span className="mt-4 inline-flex items-center gap-1 text-sm font-medium text-accent">
|
|
{isEn ? "Open datasheet" : "Abrir ficha"}
|
|
<ArrowUpRight className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
|
|
</span>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="card mt-4 px-6 py-14 text-center">
|
|
<HelpCircle className="mx-auto h-10 w-10 text-gold" aria-hidden="true" />
|
|
<h4 className="mt-4 font-display text-xl font-semibold text-ink">
|
|
{isEn ? "No series matches all filters" : "Nenhuma série atende a todos os filtros"}
|
|
</h4>
|
|
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
|
|
{isEn
|
|
? "Uncheck more restrictive features, like 5 GHz Wi-Fi together with Ethernet MAC."
|
|
: "Desmarque os recursos mais restritivos, como Wi-Fi 5 GHz junto com Ethernet MAC."}
|
|
</p>
|
|
<button type="button" onClick={limparFiltros} className="btn btn-primary mt-6">
|
|
{isEn ? "Clear filters" : "Limpar filtros"}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
<section className="shell section" aria-labelledby="proximos-titulo">
|
|
<h2 id="proximos-titulo" className="section-title">
|
|
{isEn ? "Next steps" : "Próximos passos"}
|
|
</h2>
|
|
<div className="mt-8 grid gap-4 md:grid-cols-3">
|
|
{[
|
|
{
|
|
href: "/catalogo",
|
|
icon: ShoppingBag,
|
|
titulo: isEn ? "Board catalog" : "Catálogo de placas",
|
|
texto: isEn
|
|
? "Real DevKits and modules, with pinout and official store links."
|
|
: "DevKits e módulos reais, com pinagem e link para a loja."
|
|
},
|
|
{
|
|
href: "/comparacao",
|
|
icon: SlidersHorizontal,
|
|
titulo: isEn ? "Side-by-side comparison" : "Comparador lado a lado",
|
|
texto: isEn
|
|
? "Frequency, GPIOs, ADC/DAC, power consumption, and security in one table."
|
|
: "Frequência, GPIOs, ADC/DAC, consumo e segurança em uma tabela."
|
|
},
|
|
{
|
|
href: "/glossario",
|
|
icon: BookOpen,
|
|
titulo: isEn ? "Technical glossary" : "Glossário técnico",
|
|
texto: isEn
|
|
? "Questions about ULP, RTC SRAM, JTAG or Matter? Check definitions."
|
|
: "Dúvida sobre ULP, SRAM RTC, JTAG ou Matter? Veja as definições."
|
|
},
|
|
].map(({ href, icon: Icon, titulo, texto }) => (
|
|
<Link key={href} href={getLocalizedHref(href, currentLocale)} className="card card-interactive group 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>
|
|
<h3 className="mt-4 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{titulo}</h3>
|
|
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted">{texto}</p>
|
|
<span className="mt-4 inline-flex items-center gap-1 text-sm font-medium text-accent">
|
|
{isEn ? "Open" : "Abrir"}
|
|
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" aria-hidden="true" />
|
|
</span>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|