diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d7d953b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + verificar: + name: Lint, design e build + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + # O Puppeteer só é usado em scripts locais (slides, capturas); não precisa do Chrome na CI. + PUPPETEER_SKIP_DOWNLOAD: "true" + NEXT_TELEMETRY_DISABLED: "1" + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - run: npm ci + + # eslint + auditoria do design system (tokens, contraste AAA) + auditoria de páginas + - run: npm run check + + - run: npm run build diff --git a/app/catalogo/page.jsx b/app/catalogo/page.jsx index f701f25..669df1c 100644 --- a/app/catalogo/page.jsx +++ b/app/catalogo/page.jsx @@ -3,17 +3,9 @@ import { useState, useEffect } from 'react'; import Header from "@/components/Header"; import Footer from "@/components/Footer"; +import PageHero from "@/components/PageHero"; import BoardCard from "@/components/BoardCard"; -import { - Search, - Filter, - ShoppingCart, - CheckCircle, - AlertTriangle, - RefreshCw, - Layers, - ChevronDown -} from 'lucide-react'; +import { AlertTriangle, RefreshCw, Search, ShoppingCart, SlidersHorizontal } from "lucide-react"; export default function PlacasPage() { const [boards, setBoards] = useState([]); @@ -24,11 +16,14 @@ export default function PlacasPage() { const [searchTerm, setSearchTerm] = useState(''); const [activeFilters, setActiveFilters] = useState({}); const [filterOptions, setFilterOptions] = useState({}); + const [filtrosAbertos, setFiltrosAbertos] = useState(false); - const excludedColumns = ['Nome', 'Link', 'Imagem', 'Loja Oficial']; + const excludedColumns = ['Nome', 'Descrição', 'Link', 'Imagem', 'Loja Oficial']; useEffect(() => { fetchBoards(); + // No desktop os filtros ficam sempre à vista; no celular começam recolhidos. + setFiltrosAbertos(window.matchMedia('(min-width: 1024px)').matches); }, []); useEffect(() => { @@ -147,18 +142,33 @@ export default function PlacasPage() { const activeFilterCount = Object.keys(activeFilters).length + (searchTerm ? 1 : 0); + const filterableColumns = headers.filter((h) => !excludedColumns.includes(h)); + + const hero = ( + + ); + if (loading) { return ( -
+
- -
-
- -

Consultando banco de placas...

-
+
+ {hero} +
+
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+

Consultando o banco de placas…

+
-
); @@ -166,227 +176,150 @@ export default function PlacasPage() { if (error) { return ( -
+
- -
-
-
- +
+ {hero} +
+
+
-

Falha ao sincronizar dados

-

{error}

- - -
+
-
); } - const filterableColumns = headers.filter(h => !excludedColumns.includes(h)); + const filtros = ( +
+
+ +
+
+
+ + {filterableColumns.map((column) => { + const filterData = filterOptions[column]; + if (!filterData) return null; + + if (filterData.isBinary) { + return ( + + ); + } + + return ( +
+ + {column} + {activeFilters[column]?.length > 0 && {activeFilters[column].length}} + +
+ {filterData.values.map((option) => ( + + ))} +
+
+ ); + })} +
+ ); return ( -
+
-
- {/* Top Hero Heading Block */} -
-
- - Encontre sua placa ideal -
- -

- Catálogo de Placas ESP32 -

+
+ {hero} -

- Compare e selecione placas oficiais e alternativas de desenvolvimento do microcontrolador ESP32 para seus projetos. -

- -
- -

- Links de lojas integradas de fontes verificadas e confiáveis -

-
-
- -
- {/* Left Filters Sidebar */} -
-
- - {/* Sidebar Banner Header */} -
-
-
-

- - Filtros de Busca -

- {activeFilterCount > 0 && ( -

- {activeFilterCount} selecionado(s) -

- )} -
- +
+
+
-
+ +
{filtros}
+ + - {/* Sidebar Filters Area */} -
- {/* Text search by name */} -
- -
- setSearchTerm(e.target.value)} - className="w-full px-4 py-3 pl-10 border border-slate-300 dark:border-slate-800 rounded-2xl focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-500 focus:outline-none transition-colors text-xs bg-slate-50/50 dark:bg-slate-950/20 text-slate-800 dark:text-slate-100" - /> - -
-
- -
- - {/* Dinamic filters container with customized scrollbar */} -
- {filterableColumns.map((column) => { - const filterData = filterOptions[column]; - if (!filterData) return null; - - const isActive = activeFilters[column]?.length > 0; - - return ( -
- {filterData.isBinary ? ( - // Binary filter (checkbox inside title wrapper) - - ) : ( - // Standard options filter with dropdown listing - <> -

- {column} - {isActive && ( - - {activeFilters[column].length} - - )} -

- -
- {filterData.values.map((option) => ( - - ))} -
- - )} -
- ); - })} -
-
-
-
- - {/* Right Catalog Boards Grid */} -
-
-

- Total Encontrado: {filteredBoards.length} Placa(s) +

+

+ {filteredBoards.length}{" "} + {filteredBoards.length === 1 ? "placa encontrada" : "placas encontradas"}

-
- {filteredBoards.length === 0 ? ( -
- -

Nenhuma Placa Correspondente

-

Tente ajustar ou redefinir seus filtros laterais para expandir a busca.

-
- ) : ( -
- {filteredBoards.map((board, index) => ( - - ))} -
- )} + {filteredBoards.length === 0 ? ( +
+
+ ) : ( +
+ {filteredBoards.map((board, index) => ( + + ))} +
+ )} +
-
+
- -
); -} \ No newline at end of file +} diff --git a/components/Footer.jsx b/components/Footer.jsx index 3a0d683..9f9d3e5 100644 --- a/components/Footer.jsx +++ b/components/Footer.jsx @@ -1,164 +1,166 @@ -"use client"; - -import { useEffect, useState } from "react"; import Link from "next/link"; -import { ExternalLink } from "lucide-react"; +import { ArrowUpRight } from "lucide-react"; +import { SITE } from "@/lib/seo"; + +const COLUMNS = [ + { + title: "Explorar", + links: [ + { href: "/series", label: "Séries ESP32" }, + { href: "/comparacao", label: "Comparador" }, + { href: "/catalogo", label: "Catálogo de placas" }, + { href: "/livro", label: "O livro" }, + ], + }, + { + title: "Ferramentas", + links: [ + { href: "/seletor", label: "Seletor inteligente" }, + { href: "/diagnostico", label: "Diagnóstico" }, + { href: "/componentes", label: "Component Registry" }, + { href: "/frameworks", label: "Frameworks e SDKs" }, + ], + }, + { + title: "Projeto", + links: [ + { href: "/sobre", label: "Sobre o ESPDocs" }, + { href: "/glossario", label: "Glossário" }, + { href: SITE.repositorio, label: "Repositório", external: true }, + { href: `${SITE.repositorio}/issues`, label: "Reportar um problema", external: true }, + ], + }, +]; export default function Footer() { - const [year, setYear] = useState(2026); - - useEffect(() => { - setYear(new Date().getFullYear()); - }, []); + const year = new Date().getFullYear(); return ( -