Adição dos novos modelos e do tema escuro
This commit is contained in:
1 parent
e3b4600300
commit
de4abf7cc7
42 files changed
+6551
-3891
No files matched your search
+89
-91
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
+668
-731
File diff suppressed because it is too large.
Load diff
+136
-112
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user