Adição dos novos modelos e do tema escuro

This commit is contained in:
Lucas Rayan committed 2026-05-28 09:06:39 -03:00
1 parent e3b4600300
commit de4abf7cc7
42 files changed
+6551 -3891

No files matched your search

+89 -91
View File
@@ -1,101 +1,99 @@
"use client";
'use client';
import { Award, CheckCircle, HelpCircle, ExternalLink } from 'lucide-react';
export default function BoardCard({ board }) {
const mainFields = ['Nome', 'Link', 'Imagem', 'Loja Oficial'];
const technicalFields = Object.keys(board).filter(
key => !mainFields.includes(key) && board[key]
);
const mainFields = ['Nome', 'Link', 'Imagem', 'Loja Oficial'];
const technicalFields = Object.keys(board).filter(
key => !mainFields.includes(key) && board[key]
);
const hasFeature = (value) => {
const yesValues = ['sim', 'opcional'];
return yesValues.includes(value?.toString().toLowerCase().trim());
};
const hasFeature = (value) => {
const yesValues = ['sim', 'opcional'];
return yesValues.includes(value?.toString().toLowerCase().trim());
};
// Filtrar apenas características que têm "Sim" ou "Opcional"
const availableFeatures = technicalFields.filter(field => hasFeature(board[field]));
const optionalFeatures = technicalFields.filter(field =>
board[field]?.toString().toLowerCase().trim() === 'opcional'
);
// Filtrar apenas características que têm "Sim" ou "Opcional"
const availableFeatures = technicalFields.filter(field => hasFeature(board[field]));
const optionalFeatures = technicalFields.filter(field =>
board[field]?.toString().toLowerCase().trim() === 'opcional'
);
return (
<div className="bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden group flex flex-col h-full">
{/* Imagem */}
{board.Imagem && (
<div
className="mx-auto w-fit"
dangerouslySetInnerHTML={{ __html: board.Imagem }}
/>
)}
return (
<div className="bg-white/80 dark:bg-slate-900/40 backdrop-blur-xl border border-slate-200/60 dark:border-slate-800/80 rounded-3xl shadow-lg hover:shadow-2xl hover:-translate-y-1.5 transition-all duration-300 overflow-hidden group flex flex-col h-full animate-fadeIn select-none">
{/* Image Container with premium glass background */}
{board.Imagem && (
<div
className="mx-auto w-full p-4 flex justify-center bg-white dark:bg-white border-b border-slate-100 dark:border-slate-850/40 shrink-0"
dangerouslySetInnerHTML={{ __html: board.Imagem }}
/>
)}
<div className="p-6 flex flex-col grow">
{/* Nome */}
<h3 className="text-xl font-bold text-gray-800 mb-2 line-clamp-2">
{board.Nome || 'Placa sem nome'}
</h3>
<div className="p-6 flex flex-col grow justify-between">
<div>
{/* Loja Oficial Badge */}
{board['Loja Oficial'] && board['Loja Oficial'].toLowerCase() === 'sim' && (
<div className="mb-2">
<span className="inline-flex items-center gap-1 bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-wider">
<Award className="w-3.5 h-3.5 shrink-0" />
<span>Loja Oficial</span>
</span>
</div>
)}
{/* Loja Oficial */}
{board['Loja Oficial'] && board['Loja Oficial'].toLowerCase() === 'sim' && (
<div className="mb-4">
<p className="text-sm text-blue-600 flex items-center gap-1.5">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>Link da loja oficial do fabricante</span>
</p>
</div>
)}
{/* Nome */}
<h3 className="text-base font-display font-extrabold text-slate-850 dark:text-slate-100 mb-4 line-clamp-2 leading-snug group-hover:text-purple-650 dark:group-hover:text-purple-400 transition-colors">
{board.Nome || 'Placa Genérica'}
</h3>
{/* Características Técnicas */}
{availableFeatures.length > 0 && (
<div className="mb-4 grow">
<div className="flex items-center gap-2 mb-3">
<svg className="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<h4 className="text-sm font-semibold text-gray-700">Recursos Disponíveis</h4>
</div>
<div className="flex flex-wrap gap-2">
{availableFeatures.map((field) => {
const isOptional = optionalFeatures.includes(field);
return (
<div
key={field}
className={`text-xs px-3 py-1.5 rounded-lg font-medium flex items-center gap-1 ${
isOptional
? 'bg-yellow-100 text-yellow-700 border border-yellow-200'
: 'bg-green-100 text-green-700 border border-green-200'
}`}
>
{isOptional ? (
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
) : (
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
<span>{field}</span>
</div>
);
})}
</div>
</div>
)}
{/* Características Técnicas */}
{availableFeatures.length > 0 && (
<div className="mb-6">
<h4 className="text-[9px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest flex items-center gap-1.5 mb-2.5">
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
<span>Recursos Embarcados</span>
</h4>
<div className="flex flex-wrap gap-1.5">
{availableFeatures.map((field) => {
const isOptional = optionalFeatures.includes(field);
return (
<div
key={field}
className={`text-[9px] px-2 py-0.5 rounded-lg font-bold flex items-center gap-1 ${
isOptional
? 'bg-amber-500/10 dark:bg-amber-500/15 border border-amber-500/20 text-amber-600 dark:text-amber-400'
: 'bg-emerald-500/10 dark:bg-emerald-500/15 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400'
}`}
>
{isOptional ? (
<HelpCircle className="w-2.5 h-2.5 shrink-0" />
) : (
<CheckCircle className="w-2.5 h-2.5 shrink-0" />
)}
<span>{field}</span>
</div>
);
})}
</div>
</div>
)}
</div>
{/* Link para mais informações - sempre no final */}
{board.Link && (
<a
href={board.Link}
target="_blank"
rel="noopener noreferrer"
className="block w-full bg-linear-to-r from-blue-600 to-purple-600 text-white text-center px-4 py-3 rounded-xl font-semibold hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300 mt-auto"
>
Abrir no AliExpress 🔗
</a>
)}
</div>
</div>
);
{/* Link de Compra */}
{board.Link && (
<a
href={board.Link}
target="_blank"
rel="noopener noreferrer"
className="block w-full bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-950 text-center px-4 py-3 rounded-xl font-bold text-xs uppercase tracking-wider hover:opacity-90 active:scale-98 transition-all shadow-md mt-auto flex items-center justify-center gap-1.5"
>
<span>Adquirir no AliExpress</span>
<ExternalLink className="w-3.5 h-3.5" />
</a>
)}
</div>
</div>
);
}
+155 -118
View File
@@ -1,141 +1,178 @@
'use client'
import { useEffect, useState } from 'react'
import { Code, Copy, CheckCircle, ExternalLink, HelpCircle } from 'lucide-react'
export default function CodeExample({ framework }) {
const examplesUrl = framework?.exemplos;
const color = framework?.cor || '#6B7280';
const examplesUrl = framework?.exemplos;
const color = framework?.cor || '#6B7280';
const [sample, setSample] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
const [sample, setSample] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
useEffect(() => {
setSample('');
setError(null);
setLoading(false);
useEffect(() => {
setSample('');
setError(null);
setLoading(false);
if (!framework) return;
if (!framework) return;
const exemploPath = framework.exemplo;
if (!exemploPath) {
const msg = `// Exemplos completos: ${examplesUrl || 'Ver repositório'}\n// Abra o link acima para ver exemplos e código completo.\n`;
setSample(msg);
return;
}
const exemploPath = framework.exemplo;
if (!exemploPath) {
const msg = `// Exemplos completos: ${examplesUrl || 'Ver repositório'}\n// Abra o link acima para ver exemplos e código completo.\n`;
setSample(msg);
return;
}
const url = exemploPath.startsWith('/') ? exemploPath : `/${exemploPath}`;
const url = exemploPath.startsWith('/') ? exemploPath : `/${exemploPath}`;
let cancelled = false;
setLoading(true);
let cancelled = false;
setLoading(true);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
})
.then((text) => {
if (cancelled) return;
if (text && text.trim().length > 0) {
setSample(text);
} else {
setSample(`// Arquivo de exemplo está vazio. Consulte: ${examplesUrl || 'repositório'}\n`);
setError('Arquivo vazio');
}
})
.catch((err) => {
if (cancelled) return;
setSample(`// Não foi possível carregar o exemplo local. Consulte: ${examplesUrl || 'repositório'}\n`);
setError(err.message || 'Falha ao carregar exemplo');
})
.finally(() => {
if (!cancelled) setLoading(false);
});
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
})
.then((text) => {
if (cancelled) return;
if (text && text.trim().length > 0) {
setSample(text);
} else {
setSample(`// Arquivo de exemplo está vazio. Consulte o repositório oficial.\n`);
setError('Arquivo vazio');
}
})
.catch((err) => {
if (cancelled) return;
setSample(`// Não foi possível carregar o exemplo localmente.\n// Consulte o link do GitHub acima para exemplos funcionais.\n`);
setError(err.message || 'Falha ao carregar exemplo');
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [framework?.exemplo, framework?.exemplos]);
return () => {
cancelled = true;
};
}, [framework?.exemplo, framework?.exemplos]);
const lines = (sample || '').split('\n');
const lines = (sample || '').split('\n');
async function handleCopy() {
try {
await navigator.clipboard.writeText(sample);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {}
}
async function handleCopy() {
try {
await navigator.clipboard.writeText(sample);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {}
}
return (
<div className="bg-white rounded-2xl shadow-lg p-6 border-2 border-gray-100 mb-8">
<div className="md:flex items-start justify-between gap-4 mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-1">Exemplo de Código</h3>
<p className="text-sm text-gray-600">Trecho rápido para começar — personalize conforme necessário.</p>
</div>
return (
<div className="bg-white/80 dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-200/60 dark:border-slate-800/80 shadow-2xl mb-8 animate-fadeIn">
<div className="md:flex items-center justify-between gap-6 mb-6">
<div className="select-none">
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100 mb-1 flex items-center gap-2">
<Code className="w-5 h-5" style={{ color }} />
<span>Demonstração de Código</span>
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400 font-semibold">Trecho de inicialização rápida — copie e personalize em seu projeto.</p>
</div>
<div className="grid grid-cols-2 md:flex items-center gap-2">
<span
className="inline-flex items-center text-sm font-semibold px-3 py-1 rounded-full text-white"
style={{ backgroundColor: color }}
>
{framework?.linguagem || 'Código'}
</span>
<div className="flex flex-wrap items-center gap-2.5 mt-4 md:mt-0 select-none">
<span
className="inline-flex items-center text-[10px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider"
style={{
backgroundColor: `${color}18`,
color,
border: `1px solid ${color}25`
}}
>
{framework?.linguagem || 'Código'}
</span>
<button
onClick={handleCopy}
className="inline-flex items-center gap-2 bg-white text-gray-800 px-3 py-2 rounded-lg font-medium shadow border-2 border-gray-100 cursor-pointer hover:shadow-lg transition"
title="Copiar código"
>
{copied ? '✅ Copiado' : '📋 Copiar'}
</button>
<button
onClick={handleCopy}
className="inline-flex items-center gap-1.5 bg-slate-50 dark:bg-slate-850 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-250 px-3.5 py-2 rounded-xl font-bold text-xs shadow-xs border border-slate-200/60 dark:border-slate-800/80 cursor-pointer transition active:scale-95"
title="Copiar código para a área de transferência"
>
{copied ? (
<>
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
<span className="text-emerald-600 dark:text-emerald-450">Copiado!</span>
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
<span>Copiar</span>
</>
)}
</button>
{examplesUrl && (
<a
href={examplesUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg font-semibold text-white col-span-2"
style={{
background: `linear-gradient(90deg, ${color} 0%, rgba(124,58,237,1) 100%)`
}}
title="Abrir exemplos no GitHub"
>
🔗 Ver Outros no GitHub
</a>
)}
</div>
</div>
{examplesUrl && (
<a
href={examplesUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white shadow-lg active:scale-95 transition hover:opacity-95"
style={{
background: `linear-gradient(90deg, ${color} 0%, rgba(147,51,234,1) 100%)`,
boxShadow: `0 8px 20px -8px ${color}`
}}
title="Abrir exemplos no GitHub"
>
<span>Mais Exemplos</span>
<ExternalLink className="w-3.5 h-3.5" />
</a>
)}
</div>
</div>
<div className="rounded-lg overflow-hidden border">
<div className="p-0">
<div className="flex items-center justify-between bg-linear-to-r from-gray-900 to-gray-800 p-3">
<div className="text-xs text-gray-300 font-medium">{framework?.nome}</div>
<div className="text-xs text-gray-400">
{loading ? 'Carregando exemplo...' : (framework?.exemplo ? 'Exemplo do framework' : 'Abrir no GitHub')}
</div>
</div>
</div>
<div className="rounded-2xl overflow-hidden border border-slate-200/60 dark:border-slate-800/80 shadow-md">
<div className="select-none">
<div className="flex items-center justify-between bg-slate-100/80 dark:bg-slate-950/70 border-b border-slate-200/60 dark:border-slate-850/60 px-4 py-3.5">
<div className="text-xs font-bold text-slate-750 dark:text-slate-250 flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-red-500/80" />
<span className="w-2.5 h-2.5 rounded-full bg-yellow-500/80" />
<span className="w-2.5 h-2.5 rounded-full bg-green-500/80" />
<span className="ml-2 font-mono text-[10px] text-slate-500 dark:text-slate-400">{framework?.nome} example</span>
</div>
<div className="text-[9px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
{loading ? 'Buscando exemplo...' : (framework?.exemplo ? 'Local Source' : 'External GitHub')}
</div>
</div>
</div>
<div className="flex">
<pre
className="bg-gray-900 text-gray-100 p-4 w-full overflow-auto"
style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", "Courier New", monospace' }}
>
{lines.map((line, idx) => (
<div key={idx} className="flex select-text">
<span className="text-gray-500 pr-4 text-right w-8 select-none">{idx + 1}</span>
<code className="whitespace-pre-wrap flex-1">{line === '' ? '\u00A0' : line}</code>
</div>
))}
</pre>
</div>
</div>
<div className="flex bg-slate-950">
<pre
className="text-slate-200 p-5 w-full overflow-auto text-xs leading-relaxed scrollbar-thin select-text"
style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", "Courier New", monospace' }}
>
{lines.map((line, idx) => (
<div key={idx} className="flex">
<span className="text-slate-600 dark:text-slate-650 pr-4 text-right w-8 select-none border-r border-slate-900/60 mr-4 font-semibold">{idx + 1}</span>
<code className="whitespace-pre flex-1 text-left">{line === '' ? '\u00A0' : line}</code>
</div>
))}
</pre>
</div>
</div>
{error && <p className="text-xs text-red-500 mt-2">Não foi possível carregar o arquivo de exemplo: {error}. Usando instrução para o repositório.</p>}
<p className="text-xs text-gray-500 mt-3">Dica: personalize este trecho conforme o seu projeto. Os exemplos completos estão no repositório.</p>
</div>
);
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-xl p-3.5 mt-3 select-none">
<p className="text-[10px] font-bold text-red-600 dark:text-red-400">
Nota: Não foi possível carregar o arquivo de exemplo local ({error}). Use o botão "Mais Exemplos" para ver os códigos oficiais.
</p>
</div>
)}
<div className="mt-4 flex items-start gap-2 bg-slate-50/50 dark:bg-slate-950/20 border border-slate-150/40 dark:border-slate-850/40 p-4 rounded-2xl select-none">
<HelpCircle className="w-4.5 h-4.5 text-purple-500 shrink-0 mt-0.5" />
<p className="text-[10px] font-semibold text-slate-500 dark:text-slate-400 leading-relaxed">
<strong>Dica de Desenvolvimento:</strong> Personalize o trecho de inicialização com as pinagens e configurações corretas correspondentes à série do seu microcontrolador (ex: ESP32-S3, ESP32-C6). A documentação detalhada da API pode ser acessada na barra superior do framework.
</p>
</div>
</div>
);
}
File diff suppressed because it is too large. Load diff
+136 -112
View File
@@ -1,123 +1,147 @@
"use client";
import { useEffect, useState } from "react";
import { ExternalLink } from "lucide-react";
export default function Footer() {
const year = new Date().getFullYear();
const [year, setYear] = useState(2026);
return (
<footer role="contentinfo" aria-label="Rodapé do site ESPDocs" className="bg-linear-to-r from-blue-600 to-purple-600 text-white mt-12">
<div className="max-w-6xl mx-auto px-6 pt-8 pb-4 rounded-t-2xl shadow-inner border-t border-white/10">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-3">
<h3 className="text-2xl font-extrabold tracking-tight">ESPDocs</h3>
<p className="text-sm text-white/90 max-w-sm">
Documentação não oficial em português para ESP32 — focada em clareza, exemplos práticos e comunidade.
</p>
useEffect(() => {
setYear(new Date().getFullYear());
}, []);
<div className="">
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
aria-label="Ver repositório do ESPDocs no GitHub (abre em nova aba)"
className="inline-flex items-center gap-2 bg-white/10 hover:bg-white/20 px-3 py-2 rounded-md text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-white/30 transition"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true" className="opacity-90">
<path d="M12 .5a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2.1c-3.3.7-4-1.6-4-1.6-.5-1.2-1.2-1.5-1.2-1.5-1-.7.1-.7.1-.7 1.1.1 1.7 1.1 1.7 1.1 1 .1 1.6-.7 1.9-1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.6.1-3.3 0 0 1-.3 3.3 1.2a11.4 11.4 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 3 .1 3.3.8.8 1.2 1.9 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.5.4.9 1.1.9 2.2v3.2c0 .3.2.7.8.6A12 12 0 0012 .5z" fill="currentColor"/>
</svg>
Ver no GitHub
</a>
</div>
</div>
return (
<footer role="contentinfo" aria-label="Rodapé do site ESPDocs" className="bg-slate-50 dark:bg-slate-950 text-slate-600 dark:text-slate-400 mt-24 border-t border-slate-250/30 dark:border-slate-800/80 transition-all duration-300">
<div className="max-w-7xl mx-auto px-6 py-12 md:py-16">
<div className="grid grid-cols-1 md:grid-cols-3 gap-10 md:gap-16">
{/* Coluna 1: Logo & Info */}
<div className="space-y-4">
<h3 className="text-2xl font-display font-bold text-slate-800 dark:text-slate-100">
ESPDocs
</h3>
<p className="text-sm leading-relaxed text-slate-500 dark:text-slate-400">
Plataforma brasileira e independente que centraliza e organiza informações técnicas sobre o ecossistema ESP32. Focada em clareza, usabilidade e agilidade no desenvolvimento de hardware.
</p>
<div className="pt-2">
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
aria-label="Ver repositório do ESPDocs no GitHub (abre em nova aba)"
className="inline-flex items-center gap-2 bg-slate-200/60 dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all cursor-pointer hover:shadow-xs active:scale-[0.98]"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 .5a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2.1c-3.3.7-4-1.6-4-1.6-.5-1.2-1.2-1.5-1.2-1.5-1-.7.1-.7.1-.7 1.1.1 1.7 1.1 1.7 1.1 1 .1 1.6-.7 1.9-1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.6.1-3.3 0 0 1-.3 3.3 1.2a11.4 11.4 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 3 .1 3.3.8.8 1.2 1.9 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.5.4.9 1.1.9 2.2v3.2c0 .3.2.7.8.6A12 12 0 0012 .5z" fill="currentColor"/>
</svg>
<span>Ver no GitHub</span>
</a>
</div>
</div>
<nav aria-label="Links úteis" className="text-sm">
<h4 className="text-sm font-semibold mb-2">Links úteis</h4>
<ul className="space-y-2">
<li>
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
className="hover:underline text-white/95 focus:outline-none focus:ring-2 focus:ring-white/20 rounded"
aria-label="Abrir repositório no GitHub"
>
Repositório no GitHub
</a>
</li>
<li>
<a
href="https://github.com/lucasrguerra/ESPDocs/issues"
target="_blank"
rel="noopener noreferrer"
className="hover:underline text-white/90 focus:outline-none focus:ring-2 focus:ring-white/20 rounded"
aria-label="Abrir issues do repositório"
>
Reportar bug / Abrir issue
</a>
</li>
<li>
<a
href="https://cienciaembarcada.com.br"
target="_blank"
rel="noopener noreferrer"
className="hover:underline text-white/90 focus:outline-none focus:ring-2 focus:ring-white/20 rounded"
aria-label="Visitar Ciência Embarcada (abre em nova aba)"
>
Ciência Embarcada
</a>
</li>
</ul>
</nav>
{/* Coluna 2: Links Úteis */}
<nav aria-label="Links úteis" className="space-y-4">
<h4 className="text-sm font-bold uppercase tracking-wider text-slate-800 dark:text-slate-200">
Recursos
</h4>
<ul className="space-y-3 text-sm">
<li>
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5"
aria-label="Abrir repositório no GitHub"
>
<span>Repositório Principal</span>
<ExternalLink className="w-3.5 h-3.5 opacity-60" />
</a>
</li>
<li>
<a
href="https://github.com/lucasrguerra/ESPDocs/issues"
target="_blank"
rel="noopener noreferrer"
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5"
aria-label="Abrir issues do repositório"
>
<span>Reportar um Bug / Feedback</span>
<ExternalLink className="w-3.5 h-3.5 opacity-60" />
</a>
</li>
<li>
<a
href="https://cienciaembarcada.com.br"
target="_blank"
rel="noopener noreferrer"
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5"
aria-label="Visitar Ciência Embarcada (abre em nova aba)"
>
<span className="font-semibold text-purple-600 dark:text-purple-400">Ciência Embarcada</span>
<ExternalLink className="w-3.5 h-3.5 opacity-60 text-purple-600 dark:text-purple-400" />
</a>
</li>
</ul>
</nav>
<div className="space-y-3">
<h4 className="text-sm font-semibold">Créditos</h4>
<p className="text-sm text-white/90">
Desenvolvido por{' '}
<a
href="https://linkedin.com/in/lucasrguerra"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white"
aria-label="Perfil LinkedIn de Lucas Rayan Guerra (abre em nova aba)"
>
Lucas Rayan Guerra
</a>
.
</p>
{/* Coluna 3: Autor & Social */}
<div className="space-y-4">
<h4 className="text-sm font-bold uppercase tracking-wider text-slate-800 dark:text-slate-200">
Sobre o Autor
</h4>
<p className="text-sm leading-relaxed text-slate-500 dark:text-slate-400">
Desenvolvido com dedicação por{' '}
<a
href="https://linkedin.com/in/lucasrguerra"
target="_blank"
rel="noopener noreferrer"
className="font-semibold underline text-slate-700 dark:text-slate-200 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
aria-label="Perfil LinkedIn de Lucas Rayan Guerra (abre em nova aba)"
>
Lucas Rayan Guerra
</a>
, compartilhando eletrônica, IoT e computação no Brasil.
</p>
<div className="flex items-center gap-3 mt-2">
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center w-9 h-9 rounded-md bg-white/10 hover:bg-white/20 focus:outline-none focus:ring-2 focus:ring-white/20"
aria-label="GitHub do projeto"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 .5a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2.1c-3.3.7-4-1.6-4-1.6-.5-1.2-1.2-1.5-1.2-1.5-1-.7.1-.7.1-.7 1.1.1 1.7 1.1 1.7 1.1 1 .1 1.6-.7 1.9-1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.6.1-3.3 0 0 1-.3 3.3 1.2a11.4 11.4 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 3 .1 3.3.8.8 1.2 1.9 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.5.4.9 1.1.9 2.2v3.2c0 .3.2.7.8.6A12 12 0 0012 .5z" fill="currentColor"/>
</svg>
</a>
<div className="flex items-center gap-3 pt-2">
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center w-10 h-10 rounded-xl bg-slate-200/60 dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 transition-all cursor-pointer shadow-xs"
aria-label="GitHub do projeto"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 .5a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2.1c-3.3.7-4-1.6-4-1.6-.5-1.2-1.2-1.5-1.2-1.5-1-.7.1-.7.1-.7 1.1.1 1.7 1.1 1.7 1.1 1 .1 1.6-.7 1.9-1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.6.1-3.3 0 0 1-.3 3.3 1.2a11.4 11.4 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 3 .1 3.3.8.8 1.2 1.9 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.5.4.9 1.1.9 2.2v3.2c0 .3.2.7.8.6A12 12 0 0012 .5z" fill="currentColor"/>
</svg>
</a>
<a
href="https://linkedin.com/in/lucasrguerra"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center w-9 h-9 rounded-md bg-white/10 hover:bg-white/20 focus:outline-none focus:ring-2 focus:ring-white/20"
aria-label="LinkedIn do autor"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M4.98 3.5a2.5 2.5 0 11.001 5.001A2.5 2.5 0 014.98 3.5zM3 8.98h4v12H3v-12zM9.5 8.98h3.8v1.6h.1c.5-.9 1.8-1.9 3.6-1.9 3.8 0 4.5 2.5 4.5 5.8v6.5h-4v-5.8c0-1.4-.1-3.2-2-3.2-2 0-2.3 1.6-2.3 3.1v5.9h-4v-12z" fill="currentColor"/>
</svg>
</a>
</div>
</div>
</div>
<a
href="https://linkedin.com/in/lucasrguerra"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center w-10 h-10 rounded-xl bg-slate-200/60 dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 transition-all cursor-pointer shadow-xs"
aria-label="LinkedIn do autor"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M4.98 3.5a2.5 2.5 0 11.001 5.001A2.5 2.5 0 014.98 3.5zM3 8.98h4v12H3v-12zM9.5 8.98h3.8v1.6h.1c.5-.9 1.8-1.9 3.6-1.9 3.8 0 4.5 2.5 4.5 5.8v6.5h-4v-5.8c0-1.4-.1-3.2-2-3.2-2 0-2.3 1.6-2.3 3.1v5.9h-4v-12z" fill="currentColor"/>
</svg>
</a>
</div>
</div>
</div>
<div className="mt-6 pt-4 border-t border-white/10 text-sm text-white/80 text-center">
<div>
© {year} ESPDocs — Conteúdo não oficial. Consulte sempre datasheets e documentos oficiais para decisões críticas.
</div>
</div>
</div>
</footer>
);
{/* Divisor & Isenção */}
<div className="mt-12 pt-8 border-t border-slate-205/60 dark:border-slate-800/80 flex flex-col md:flex-row justify-between items-center gap-4 text-xs text-slate-400 dark:text-slate-500">
<p className="text-center md:text-left leading-relaxed">
© {year} ESPDocs. Esta plataforma independente de código aberto não possui afiliação com a Espressif Systems.
</p>
<p className="text-center md:text-right shrink-0">
Feito para a comunidade maker 🇧🇷
</p>
</div>
</div>
</footer>
);
}
+206 -164
View File
@@ -1,181 +1,223 @@
"use client";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { useState, useEffect } from "react";
import Link from "next/link";
import Button from "@mui/material/Button";
import Drawer from "@mui/material/Drawer";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import MenuIcon from "@mui/icons-material/Menu";
import HomeIcon from "@mui/icons-material/Home";
import AboutIcon from "@mui/icons-material/Info";
import ModelsIcon from "@mui/icons-material/Memory";
import FrameworksIcon from "@mui/icons-material/Apps";
import CompareIcon from "@mui/icons-material/CompareArrows";
import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
import MenuBookIcon from "@mui/icons-material/MenuBook";
import FindInPageIcon from "@mui/icons-material/FindInPage";
import { useTheme } from "next-themes";
import { createPortal } from "react-dom";
import {
Home,
Info,
BookOpen,
Cpu,
SlidersHorizontal,
Terminal,
Scale,
ShoppingBag,
Menu,
X,
Sun,
Moon
} from "lucide-react";
export default function Header() {
const [drawerOpen, setDrawerOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const pathname = usePathname();
const pathname = usePathname();
const menuItems = [
{ text: "Início", href: "/", icon: <HomeIcon />, featured: true },
{ text: "Sobre o Projeto", brief: "Sobre", href: "/sobre", icon: <AboutIcon /> },
{ text: "Glossário", href: "/glossario", icon: <MenuBookIcon /> },
{ text: "Séries", href: "/series", icon: <ModelsIcon />, featured: true },
{ text: "Seletor de ESP32", brief: "Seletor", href: "/seletor", icon: <FindInPageIcon />, featured: true },
{ text: "Frameworks", href: "/frameworks", icon: <FrameworksIcon />, featured: true },
{ text: "Comparar Séries", brief: "Comparar", href: "/comparacao", icon: <CompareIcon />, featured: true },
{ text: "Catálogo de Placas", brief: "Catálogo", href: "/catalogo", icon: <ShoppingCartIcon /> },
];
useEffect(() => {
setMounted(true);
}, []);
const featuredItems = menuItems.filter(item => item.featured);
const menuItems = [
{ text: "Início", href: "/", icon: <Home className="w-4 h-4" />, featured: true },
{ text: "Sobre o Projeto", brief: "Sobre", href: "/sobre", icon: <Info className="w-4 h-4" /> },
{ text: "Glossário", href: "/glossario", icon: <BookOpen className="w-4 h-4" /> },
{ text: "Séries", href: "/series", icon: <Cpu className="w-4 h-4" />, featured: true },
{ text: "Seletor de ESP32", brief: "Seletor", href: "/seletor", icon: <SlidersHorizontal className="w-4 h-4" />, featured: true },
{ text: "Frameworks", href: "/frameworks", icon: <Terminal className="w-4 h-4" />, featured: true },
{ text: "Comparar Séries", brief: "Comparar", href: "/comparacao", icon: <Scale className="w-4 h-4" />, featured: true },
{ text: "Catálogo de Placas", brief: "Catálogo", href: "/catalogo", icon: <ShoppingBag className="w-4 h-4" /> },
];
function isActive(href) {
if (href === "/") {
return pathname === "/";
}
return String(pathname).includes(href) && href !== "/";
}
const featuredItems = menuItems.filter(item => item.featured);
return (
<header className="bg-white/80 backdrop-blur-md py-4 px-4 md:px-8 text-black shadow-lg border-b border-gray-100 sticky top-0 z-50">
<div className="flex justify-between max-w-7xl mx-auto items-center gap-8">
<Link href="/" className="group shrink-0">
<h1 className="text-2xl md:text-3xl font-bold bg-linear-to-r from-blue-600 via-purple-600 to-pink-600 bg-clip-text text-transparent transition-all duration-300 group-hover:scale-105">
ESPDocs
</h1>
</Link>
function isActive(href) {
if (href === "/") {
return pathname === "/";
}
return String(pathname).includes(href) && href !== "/";
}
{/* Menu Desktop - visível em telas médias e grandes */}
<nav className="hidden lg:flex items-center gap-2 flex-1 justify-end" aria-label="Menu principal">
{featuredItems.map((item) => (
<Link
key={item.text}
href={item.href}
className={`group flex items-center gap-2 px-4 py-2.5 rounded-xl font-medium transition-all duration-300 ${
isActive(item.href)
? "bg-linear-to-r from-blue-600 to-purple-600 text-white shadow-lg shadow-purple-200"
: "text-gray-700 hover:bg-linear-to-r hover:from-blue-50 hover:to-purple-50 hover:shadow-md hover:-translate-y-0.5"
}`}
aria-label={item.brief || item.text}
aria-current={isActive(item.href) ? "page" : undefined}
>
<span className={`transition-transform duration-300 ${
isActive(item.href) ? "scale-110" : "group-hover:scale-110"
}`}>
{item.icon}
</span>
<span className="text-sm whitespace-nowrap">{item.brief || item.text}</span>
</Link>
))}
{/* Botão Menu Completo Desktop */}
<Button
variant="outlined"
onClick={() => setDrawerOpen(true)}
className="border-2 border-gray-300 text-gray-700 hover:border-purple-500 hover:text-purple-600 transition-all duration-300 hover:shadow-lg rounded-xl! px-4"
aria-label="Abrir menu completo"
aria-expanded={drawerOpen}
aria-controls="mobile-menu-drawer"
>
<MenuIcon />
</Button>
</nav>
// Lock scroll when mobile menu is open
useEffect(() => {
if (drawerOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [drawerOpen]);
{/* Botão Menu Mobile - visível apenas em telas pequenas */}
<div className="lg:hidden">
<Button
variant="outlined"
onClick={() => setDrawerOpen(true)}
className="border-2 border-gray-300 text-gray-700 hover:border-purple-500 hover:text-purple-600 transition-all duration-300 hover:shadow-lg rounded-xl! px-4"
aria-label="Abrir menu de navegação"
aria-expanded={drawerOpen}
aria-controls="mobile-menu-drawer"
>
<MenuIcon />
</Button>
</div>
</div>
return (
<header className="bg-white/80 dark:bg-slate-950/80 backdrop-blur-md py-4 px-4 md:px-8 shadow-xs border-b border-slate-100 dark:border-slate-800/60 sticky top-0 z-50 transition-colors duration-300">
<div className="flex justify-between max-w-7xl mx-auto items-center gap-8">
{/* LOGO */}
<Link href="/" className="group shrink-0">
<h1 className="text-2xl md:text-3xl font-display font-extrabold bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent transition-all duration-300 group-hover:scale-[1.02]">
ESPDocs
</h1>
</Link>
{/* Drawer Mobile */}
<Drawer
anchor="right"
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
id="mobile-menu-drawer"
>
<div className="p-6 w-72">
<div className="mb-6">
<h2 className="text-2xl font-bold bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Menu
</h2>
<div className="h-1 w-16 bg-linear-to-r from-blue-600 to-purple-600 rounded-full mt-2"></div>
</div>
{/* Menu Desktop */}
<nav className="hidden lg:flex items-center gap-2 flex-1 justify-end" aria-label="Menu principal">
{featuredItems.map((item) => (
<Link
key={item.text}
href={item.href}
className={`group flex items-center gap-2 px-4 py-2 rounded-xl font-medium transition-all duration-300 text-sm ${
isActive(item.href)
? "bg-gradient-to-r from-blue-600 to-purple-600 text-white shadow-md shadow-purple-500/10"
: "text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-900/60 hover:text-purple-600 dark:hover:text-purple-400"
}`}
aria-label={item.brief || item.text}
aria-current={isActive(item.href) ? "page" : undefined}
>
<span className={`transition-transform duration-300 ${
isActive(item.href) ? "scale-110" : "group-hover:scale-110"
}`}>
{item.icon}
</span>
<span className="whitespace-nowrap">{item.brief || item.text}</span>
</Link>
))}
<List component="nav" aria-label="Menu de navegação">
{menuItems.map((item, index) => (
<ListItem
key={item.text}
disablePadding
className="mb-3"
style={{
animation: `slideIn 0.3s ease-out ${index * 0.1}s both`
}}
>
<div className="w-full">
<Link
className={`flex items-center w-full p-4 text-left rounded-xl transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
isActive(item.href)
? "bg-linear-to-r from-blue-600 to-purple-600 text-white shadow-lg transform scale-105"
: "hover:bg-white hover:shadow-md hover:transform hover:scale-102 text-gray-700"
}`}
onClick={() => { setDrawerOpen(false); }}
href={item.href}
aria-label={item.text}
aria-current={isActive(item.href) ? "page" : undefined}
>
<span className={`transition-transform duration-300 ${isActive(item.href) ? "scale-110" : "group-hover:scale-110"}`}>
{item.icon}
</span>
<span className="ml-3 font-medium">{item.text}</span>
{isActive(item.href) && (
<span className="ml-auto">
<span className="inline-block w-2 h-2 bg-white rounded-full animate-pulse"></span>
</span>
)}
</Link>
</div>
</ListItem>
))}
</List>
{/* Divider */}
<div className="h-5 w-px bg-slate-200 dark:bg-slate-800 mx-2" />
<div className="mt-8 p-4 bg-white/50 backdrop-blur-sm rounded-xl border border-gray-200">
<p className="text-xs text-gray-600 text-center">
Documentação não oficial
</p>
</div>
</div>
</Drawer>
{/* Theme Switcher Button */}
<ThemeToggle />
<style jsx global>{`
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
`}</style>
</header>
);
{/* Full Menu Burger Trigger */}
<button
onClick={() => setDrawerOpen(true)}
className="p-2.5 rounded-xl border border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-900/80 hover:border-purple-500 dark:hover:border-purple-400 hover:text-purple-600 dark:hover:text-purple-400 transition-all cursor-pointer"
aria-label="Abrir menu completo"
aria-expanded={drawerOpen}
>
<Menu className="w-5 h-5" />
</button>
</nav>
{/* Botões Mobile (Theme + Burger) */}
<div className="flex lg:hidden items-center gap-2">
<ThemeToggle />
<button
onClick={() => setDrawerOpen(true)}
className="p-2.5 rounded-xl border border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-900/80 hover:border-purple-500 dark:hover:border-purple-400 transition-all cursor-pointer"
aria-label="Abrir menu de navegação"
aria-expanded={drawerOpen}
>
<Menu className="w-5 h-5" />
</button>
</div>
</div>
{/* Render side-panel and overlay at body level via React Portal to prevent layout leakage */}
{mounted && createPortal(
<>
{drawerOpen && (
<div
className="fixed inset-0 bg-slate-950/50 dark:bg-black/75 backdrop-blur-xs z-[100] transition-opacity duration-300"
onClick={() => setDrawerOpen(false)}
/>
)}
<div
className={`fixed top-0 right-0 h-screen w-80 max-w-[85vw] bg-white dark:bg-slate-900 shadow-2xl z-[101] transform transition-transform duration-300 ease-in-out flex flex-col border-l border-slate-200 dark:border-slate-800 ${
drawerOpen ? "translate-x-0" : "translate-x-full"
}`}
>
{/* Drawer Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-100 dark:border-slate-800/80">
<h2 className="text-xl font-display font-bold bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
Menu Navegação
</h2>
<button
onClick={() => setDrawerOpen(false)}
className="p-2 text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/60 transition-all cursor-pointer"
aria-label="Fechar menu"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Drawer Menu Items */}
<nav className="flex-1 overflow-y-auto p-6 animate-fadeIn" aria-label="Navegação móvel">
<ul className="space-y-2">
{menuItems.map((item) => (
<li key={item.text}>
<Link
href={item.href}
className={`flex items-center gap-3 w-full p-3.5 rounded-xl font-medium transition-all duration-205 text-sm ${
isActive(item.href)
? "bg-gradient-to-r from-blue-600 to-purple-600 text-white shadow-md shadow-purple-500/10"
: "text-slate-650 dark:text-slate-350 hover:bg-slate-50 dark:hover:bg-slate-800/40 hover:text-purple-600 dark:hover:text-purple-400"
}`}
onClick={() => setDrawerOpen(false)}
aria-current={isActive(item.href) ? "page" : undefined}
>
<span className="shrink-0">{item.icon}</span>
<span className="grow text-left">{item.text}</span>
{isActive(item.href) && (
<span className="w-1.5 h-1.5 bg-white rounded-full animate-ping" />
)}
</Link>
</li>
))}
</ul>
</nav>
{/* Drawer Footer Info */}
<div className="p-6 border-t border-slate-100 dark:border-slate-800/80 bg-slate-50/50 dark:bg-slate-900/50">
<p className="text-center text-xs text-slate-400 dark:text-slate-500 font-medium">
ESPDocs • Plataforma Não Oficial
</p>
</div>
</div>
</>
, document.body
)}
</header>
);
}
/* Internal ThemeToggle Component */
function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) {
return (
<div className="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-900 animate-pulse border border-transparent" />
);
}
return (
<button
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
className="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-900 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-800/80 hover:text-purple-600 dark:hover:text-purple-400 border border-slate-250/20 dark:border-slate-800/60 shadow-xs flex items-center justify-center transition-all duration-300 cursor-pointer active:scale-95 shrink-0"
aria-label="Alternar tema"
>
{resolvedTheme === "dark" ? (
<Sun className="w-5 h-5 text-yellow-500" />
) : (
<Moon className="w-5 h-5 text-indigo-600" />
)}
</button>
);
}
+11
View File
@@ -0,0 +1,11 @@
"use client";
import { ThemeProvider } from "next-themes";
export default function Providers({ children }) {
return (
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={true}>
{children}
</ThemeProvider>
);
}
+72 -70
View File
@@ -1,80 +1,82 @@
'use client';
import { useState } from 'react';
import { ChevronDown } from 'lucide-react';
export default function SeriesTabMenu({ tabs, color }) {
const availableTabs = tabs.filter(tab => tab.available);
const [activeTab, setActiveTab] = useState(availableTabs[0]?.id || 'specs');
const availableTabs = tabs.filter(tab => tab.available);
const [activeTab, setActiveTab] = useState(availableTabs[0]?.id || 'specs');
const activeTabContent = availableTabs.find(tab => tab.id === activeTab)?.content;
const activeTabContent = availableTabs.find(tab => tab.id === activeTab)?.content;
return (
<div className="mb-8">
<div className="hidden md:flex bg-white rounded-2xl shadow-lg py-2 px-4 mb-6 border-2 border-gray-100 gap-4">
{availableTabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex-1 flex items-center justify-center gap-2 py-4 px-6 rounded-xl font-semibold transition-all duration-300 cursor-pointer ${
activeTab === tab.id
? 'text-white shadow-lg transform scale-105'
: 'text-gray-600 hover:bg-gray-50'
}`}
style={
activeTab === tab.id
? { backgroundColor: color }
: {}
}
>
<span className="hidden lg:inline">{tab.label}</span>
</button>
))}
</div>
return (
<div className="mb-12">
{/* Desktop Tabs */}
<div className="hidden md:flex bg-white/70 dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-xl py-2 px-3 mb-6 border border-slate-200/60 dark:border-slate-800/80 gap-3">
{availableTabs.map((tab) => {
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex-1 flex items-center justify-center gap-2 py-3 px-5 rounded-xl font-bold text-xs uppercase tracking-wider transition-all duration-300 cursor-pointer active:scale-98 ${
isActive
? 'text-white shadow-lg scale-[1.02]'
: 'text-slate-650 dark:text-slate-350 hover:bg-slate-100/50 dark:hover:bg-slate-800/40 hover:text-slate-800 dark:hover:text-slate-100'
}`}
style={
isActive
? { backgroundColor: color, boxShadow: `0 10px 20px -10px ${color}` }
: {}
}
>
<span>{tab.label}</span>
</button>
);
})}
</div>
<div className="md:hidden mb-6">
<div className="bg-white rounded-2xl shadow-lg border-2 border-gray-100 overflow-hidden">
<button
onClick={() => {
const dropdown = document.getElementById('mobile-tab-dropdown');
dropdown.classList.toggle('hidden');
}}
className="w-full flex items-center justify-between py-4 px-6 font-semibold text-white"
style={{ backgroundColor: color }}
>
<div className="flex items-center gap-3">
<span className="text-xl">
{availableTabs.find(tab => tab.id === activeTab)?.icon}
</span>
<span>
{availableTabs.find(tab => tab.id === activeTab)?.label}
</span>
</div>
<span className="text-xl">▼</span>
</button>
<div id="mobile-tab-dropdown" className="hidden">
{availableTabs.map((tab) => (
tab.id !== activeTab && (
<button
key={tab.id}
onClick={() => {
setActiveTab(tab.id);
document.getElementById('mobile-tab-dropdown').classList.add('hidden');
}}
className="w-full flex items-center gap-3 py-4 px-6 text-gray-700 hover:bg-gray-50 transition-colors border-t border-gray-100"
>
<span className="text-xl">{tab.icon}</span>
<span className="font-medium">{tab.label}</span>
</button>
)
))}
</div>
</div>
</div>
{/* Mobile Dropdown Tab Selector */}
<div className="md:hidden mb-6">
<div className="bg-white/70 dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-lg border border-slate-200/60 dark:border-slate-800/80 overflow-hidden">
<button
onClick={() => {
const dropdown = document.getElementById('mobile-tab-dropdown');
dropdown.classList.toggle('hidden');
}}
className="w-full flex items-center justify-between py-4 px-6 font-bold text-sm uppercase tracking-wider text-white transition-all duration-300"
style={{ backgroundColor: color }}
>
<div className="flex items-center gap-3">
<span>
{availableTabs.find(tab => tab.id === activeTab)?.label}
</span>
</div>
<ChevronDown className="w-4 h-4 shrink-0 transition-transform duration-300" />
</button>
<div id="mobile-tab-dropdown" className="hidden">
{availableTabs.map((tab) => (
tab.id !== activeTab && (
<button
key={tab.id}
onClick={() => {
setActiveTab(tab.id);
document.getElementById('mobile-tab-dropdown').classList.add('hidden');
}}
className="w-full flex items-center gap-3 py-4 px-6 text-slate-700 dark:text-slate-300 hover:bg-slate-150/40 dark:hover:bg-slate-800/30 transition-colors border-t border-slate-200/60 dark:border-slate-800/60 font-semibold text-xs uppercase tracking-wider text-left"
>
<span>{tab.label}</span>
</button>
)
))}
</div>
</div>
</div>
<div className="animate-fadeIn">
{activeTabContent}
</div>
</div>
);
<div className="animate-fadeIn">
{activeTabContent}
</div>
</div>
);
}