feat: Initialize CAR Auto Center project with Vite and React

Sets up the foundational structure for the CAR Auto Center application using Vite and React. Includes project dependencies, basic HTML structure, TypeScript configuration, and initial README content. This commit establishes the project's build tool, core libraries, and essential configuration files.
This commit is contained in:
MMrp89
2026-02-19 16:22:10 -03:00
parent 638c7c3eef
commit c0bd6d7e3d
20 changed files with 3181 additions and 8 deletions

View File

@@ -0,0 +1,880 @@
import React, { useState } from 'react';
import {
LayoutDashboard,
Settings,
LogOut,
Save,
Monitor,
Box,
Users,
MessageSquare,
Wrench,
Tags,
FileText,
Check,
Eye,
Trash2,
PlusCircle,
Palette,
Briefcase,
Image,
Upload,
Menu as MenuIcon,
Columns,
X,
ArrowLeftRight
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useData } from '../../contexts/DataContext';
import { Hero } from '../Hero';
import { Header } from '../Header';
import {
ServicesSection,
PackagesSection,
AboutSection,
TeamSection,
TestimonialsSection,
FaqSection,
BlogSection,
SpecialOffersSection,
ClientsSection,
WhyChooseSection,
GallerySection,
StatsSection,
Footer,
BeforeAfterSection
} from '../AppContent';
import { FooterColumn, FooterLink } from '../../types';
const SidebarItem = ({ icon: Icon, label, active, onClick }: any) => (
<button
onClick={onClick}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors text-sm font-medium ${
active
? 'bg-primary text-white'
: 'text-gray-400 hover:bg-zinc-800 hover:text-white'
}`}
>
<Icon size={18} />
{label}
</button>
);
const InputGroup = ({ label, value, onChange, type = "text", textarea = false, hint }: any) => (
<div className="mb-4">
<div className="flex justify-between">
<label className="block text-xs font-bold text-gray-500 uppercase mb-1">{label}</label>
{hint && <span className="text-xs text-gray-600">{hint}</span>}
</div>
{textarea ? (
<textarea
className="w-full bg-zinc-900 border border-zinc-700 rounded p-2 text-white text-sm focus:border-primary outline-none min-h-[100px]"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
) : (
<input
type={type}
className="w-full bg-zinc-900 border border-zinc-700 rounded p-2 text-white text-sm focus:border-primary outline-none"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)}
</div>
);
const SectionTextEditor = ({ sectionKey, title = "Textos da Seção" }: { sectionKey: string, title?: string }) => {
const { data, updateData } = useData();
// @ts-ignore
const texts = data.texts[sectionKey];
if (!texts) return null;
const updateText = (field: string, value: string) => {
// @ts-ignore
updateData('texts', {
...data.texts,
[sectionKey]: {
// @ts-ignore
...data.texts[sectionKey],
[field]: value
}
});
};
return (
<div className="p-4 bg-zinc-900 border border-zinc-800 rounded-lg mb-8">
<h4 className="text-primary font-bold border-b border-zinc-800 pb-2 mb-4">{title}</h4>
<div className="grid gap-4">
{Object.keys(texts).map((key) => (
<InputGroup
key={key}
label={key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1').trim()} // CamelCase to Title Case
value={texts[key]}
onChange={(v: string) => updateText(key, v)}
textarea={key === 'description' || key === 'subtitle'}
/>
))}
</div>
</div>
);
}
const Toggle = ({ label, checked, onChange }: any) => (
<div className="flex items-center justify-between p-4 bg-zinc-900 rounded border border-zinc-800">
<span className="text-white font-medium">{label}</span>
<button
onClick={() => onChange(!checked)}
className={`w-12 h-6 rounded-full p-1 transition-colors ${checked ? 'bg-green-500' : 'bg-zinc-700'}`}
>
<div className={`w-4 h-4 rounded-full bg-white transition-transform ${checked ? 'translate-x-6' : 'translate-x-0'}`} />
</button>
</div>
);
// Mapa de tradução para as chaves de visibilidade
const sectionLabels: Record<string, string> = {
hero: 'Topo / Início (Hero)',
specialOffers: 'Ofertas Especiais',
about: 'Sobre Nós',
services: 'Serviços',
bigCta: 'Chamada para Ação (Grande)',
packages: 'Pacotes de Serviços',
gallery: 'Galeria de Fotos',
beforeAfter: 'Comparativo Antes/Depois',
stats: 'Estatísticas',
whyChoose: 'Por que Escolher',
testimonials: 'Depoimentos',
team: 'Equipe',
faq: 'Perguntas Frequentes (FAQ)',
blog: 'Blog / Notícias',
contact: 'Formulário de Contato',
clients: 'Logos de Clientes'
};
export const Dashboard: React.FC = () => {
const navigate = useNavigate();
const { data, updateData } = useData();
const [activeSection, setActiveSection] = useState('config'); // 'config' | 'visibility' | 'content-hero' etc.
const [isPreviewOpen, setIsPreviewOpen] = useState(true);
const [isSaved, setIsSaved] = useState(false);
const [newImageUrl, setNewImageUrl] = useState('');
const handleLogout = () => {
localStorage.removeItem('admin_auth');
navigate('/admin');
};
const handleSave = () => {
setIsSaved(true);
setTimeout(() => setIsSaved(false), 2000);
};
const updateSettings = (field: string, value: any) => {
updateData('settings', { ...data.settings, [field]: value });
};
const updateSocial = (field: string, value: string) => {
updateData('settings', { ...data.settings, social: { ...data.settings.social, [field]: value } });
};
const updateVisibility = (section: string, value: boolean) => {
updateData('visibility', { ...data.visibility, [section]: value });
};
const updateArrayItem = (section: string, index: number, field: string, value: any) => {
// @ts-ignore
const newArray = [...data[section]];
newArray[index] = { ...newArray[index], [field]: value };
// @ts-ignore
updateData(section, newArray);
};
const addItem = (section: string, emptyItem: any) => {
// @ts-ignore
const newArray = [...data[section], emptyItem];
// @ts-ignore
updateData(section, newArray);
};
const removeItem = (section: string, index: number) => {
// @ts-ignore
const newArray = data[section].filter((_, i) => i !== index);
// @ts-ignore
updateData(section, newArray);
};
// --- Header Helpers ---
const updateHeaderItem = (index: number, field: string, value: any) => {
const newItems = [...data.header.items];
// @ts-ignore
newItems[index] = { ...newItems[index], [field]: value };
updateData('header', { ...data.header, items: newItems });
};
const addHeaderItem = () => {
const newItems = [...data.header.items, { label: 'Novo Link', href: '#' }];
updateData('header', { ...data.header, items: newItems });
};
const removeHeaderItem = (index: number) => {
const newItems = data.header.items.filter((_, i) => i !== index);
updateData('header', { ...data.header, items: newItems });
};
const updateCtaButton = (field: string, value: any) => {
updateData('header', { ...data.header, ctaButton: { ...data.header.ctaButton, [field]: value }});
}
// --- Footer Helpers ---
const updateFooterColumn = (index: number, field: string, value: any) => {
const newColumns = [...data.footer.columns];
// @ts-ignore
newColumns[index] = { ...newColumns[index], [field]: value };
updateData('footer', { ...data.footer, columns: newColumns });
};
const removeFooterColumn = (index: number) => {
const newColumns = data.footer.columns.filter((_, i) => i !== index);
updateData('footer', { ...data.footer, columns: newColumns });
};
const addFooterColumn = () => {
const newCol: FooterColumn = { id: `col_${Date.now()}`, title: 'Nova Coluna', type: 'custom', links: [] };
const newColumns = [...data.footer.columns, newCol];
updateData('footer', { ...data.footer, columns: newColumns });
};
const updateFooterLink = (colIndex: number, linkIndex: number, field: string, value: string) => {
const newColumns = [...data.footer.columns];
const col = newColumns[colIndex];
if (col.links) {
const newLinks = [...col.links];
// @ts-ignore
newLinks[linkIndex] = { ...newLinks[linkIndex], [field]: value };
col.links = newLinks;
updateData('footer', { ...data.footer, columns: newColumns });
}
};
const addFooterLink = (colIndex: number) => {
const newColumns = [...data.footer.columns];
const col = newColumns[colIndex];
if (!col.links) col.links = [];
col.links.push({ label: 'Novo Link', href: '#' });
updateData('footer', { ...data.footer, columns: newColumns });
};
const removeFooterLink = (colIndex: number, linkIndex: number) => {
const newColumns = [...data.footer.columns];
const col = newColumns[colIndex];
if (col.links) {
col.links = col.links.filter((_, i) => i !== linkIndex);
updateData('footer', { ...data.footer, columns: newColumns });
}
}
// Função específica para Upload de Imagens na Galeria
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
const promises: Promise<string>[] = [];
Array.from(files).forEach(file => {
const reader = new FileReader();
const promise = new Promise<string>((resolve) => {
reader.onload = (e) => {
if (e.target?.result) resolve(e.target.result as string);
};
});
reader.readAsDataURL(file as Blob);
promises.push(promise);
});
Promise.all(promises).then(base64Images => {
// @ts-ignore
const updatedGallery = [...data.gallery, ...base64Images];
updateData('gallery', updatedGallery);
});
}
};
const handleAddUrlToGallery = () => {
if (newImageUrl) {
// @ts-ignore
const updatedGallery = [...data.gallery, newImageUrl];
updateData('gallery', updatedGallery);
setNewImageUrl('');
}
};
const renderEditor = () => {
switch(activeSection) {
case 'config':
return (
<div className="space-y-6">
<h3 className="text-xl font-bold text-white mb-4">Configurações Gerais</h3>
<div className="p-4 bg-zinc-900 border border-zinc-800 rounded-lg space-y-4">
<h4 className="text-primary font-bold border-b border-zinc-800 pb-2 mb-4">Identidade Visual</h4>
<InputGroup label="Nome do Site" value={data.settings.siteName} onChange={(v: string) => updateSettings('siteName', v)} />
<div className="grid grid-cols-2 gap-4">
<InputGroup label="Cor Principal (Hex)" value={data.settings.primaryColor} onChange={(v: string) => updateSettings('primaryColor', v)} type="color" />
<InputGroup label="Texto da Cor" value={data.settings.primaryColor} onChange={(v: string) => updateSettings('primaryColor', v)} />
</div>
<InputGroup label="URL do Logo" value={data.settings.logoUrl} onChange={(v: string) => updateSettings('logoUrl', v)} hint="Deixe vazio para usar texto" />
<InputGroup label="URL do Favicon" value={data.settings.faviconUrl} onChange={(v: string) => updateSettings('faviconUrl', v)} />
</div>
<div className="p-4 bg-zinc-900 border border-zinc-800 rounded-lg space-y-4">
<h4 className="text-primary font-bold border-b border-zinc-800 pb-2 mb-4">Contato & Social</h4>
<InputGroup label="WhatsApp (Apenas números)" value={data.settings.whatsappNumber} onChange={(v: string) => updateSettings('whatsappNumber', v)} hint="Ex: 5511999999999" />
<InputGroup label="Telefone Visível" value={data.settings.contactPhoneDisplay} onChange={(v: string) => updateSettings('contactPhoneDisplay', v)} />
<InputGroup label="Email para Contato" value={data.settings.contactEmail} onChange={(v: string) => updateSettings('contactEmail', v)} />
<InputGroup label="Endereço" value={data.settings.address} onChange={(v: string) => updateSettings('address', v)} />
<div className="mt-4 pt-4 border-t border-zinc-800">
<h5 className="text-white font-bold mb-3 text-sm">Redes Sociais (Deixe vazio para ocultar)</h5>
<InputGroup label="Instagram URL" value={data.settings.social?.instagram || ''} onChange={(v: string) => updateSocial('instagram', v)} />
<InputGroup label="Facebook URL" value={data.settings.social?.facebook || ''} onChange={(v: string) => updateSocial('facebook', v)} />
<InputGroup label="Twitter/X URL" value={data.settings.social?.twitter || ''} onChange={(v: string) => updateSocial('twitter', v)} />
<InputGroup label="YouTube URL" value={data.settings.social?.youtube || ''} onChange={(v: string) => updateSocial('youtube', v)} />
<InputGroup label="LinkedIn URL" value={data.settings.social?.linkedin || ''} onChange={(v: string) => updateSocial('linkedin', v)} />
</div>
</div>
</div>
);
case 'visibility':
return (
<div className="space-y-4">
<h3 className="text-xl font-bold text-white mb-4">Controle de Seções (Ativar/Desativar)</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.keys(data.visibility).map((key) => (
<Toggle
key={key}
label={sectionLabels[key] || key}
checked={data.visibility[key as keyof typeof data.visibility]}
onChange={(v: boolean) => updateVisibility(key, v)}
/>
))}
</div>
</div>
);
case 'header':
return (
<div className="space-y-6">
<h3 className="text-xl font-bold text-white mb-4">Menu & Cabeçalho</h3>
<div className="p-4 bg-zinc-900 border border-zinc-800 rounded-lg mb-6">
<h4 className="text-primary font-bold mb-4">Botão de Destaque (CTA)</h4>
<Toggle label="Exibir Botão" checked={data.header.ctaButton.show} onChange={(v: boolean) => updateCtaButton('show', v)} />
{data.header.ctaButton.show && (
<div className="mt-4 grid gap-4">
<InputGroup label="Texto do Botão" value={data.header.ctaButton.text} onChange={(v: string) => updateCtaButton('text', v)} />
<InputGroup label="URL de Destino" value={data.header.ctaButton.url} onChange={(v: string) => updateCtaButton('url', v)} hint="Deixe vazio para abrir o WhatsApp" />
</div>
)}
</div>
<div className="space-y-4">
<div className="flex justify-between items-center">
<h4 className="text-primary font-bold">Itens do Menu</h4>
<button onClick={addHeaderItem} className="text-sm text-green-500 hover:text-green-400 flex items-center gap-1"><PlusCircle size={16}/> Adicionar</button>
</div>
{data.header.items.map((item, idx) => (
<div key={idx} className="flex gap-4 items-center bg-zinc-900 p-3 rounded border border-zinc-800">
<div className="flex-1 grid grid-cols-2 gap-4">
<input type="text" className="bg-black border border-zinc-700 rounded p-2 text-white text-sm" value={item.label} onChange={(e) => updateHeaderItem(idx, 'label', e.target.value)} placeholder="Nome" />
<input type="text" className="bg-black border border-zinc-700 rounded p-2 text-white text-sm" value={item.href} onChange={(e) => updateHeaderItem(idx, 'href', e.target.value)} placeholder="Link (/sobre ou https://...)" />
</div>
<button onClick={() => removeHeaderItem(idx)} className="text-red-500 hover:text-red-400 p-2"><Trash2 size={18}/></button>
</div>
))}
</div>
</div>
);
case 'footer':
return (
<div className="space-y-8">
<h3 className="text-xl font-bold text-white mb-4">Configuração do Rodapé</h3>
<InputGroup label="Descrição do Rodapé" value={data.footer.description} onChange={(v: string) => updateData('footer', { ...data.footer, description: v })} textarea />
<div>
<div className="flex justify-between items-center mb-4">
<h4 className="text-primary font-bold">Estrutura de Colunas</h4>
<button onClick={addFooterColumn} className="text-sm text-green-500 hover:text-green-400 flex items-center gap-1"><PlusCircle size={16}/> Adicionar Coluna</button>
</div>
<div className="space-y-6">
{data.footer.columns.map((col, idx) => (
<div key={col.id} className="bg-zinc-900 border border-zinc-800 rounded-lg p-4 relative">
<button onClick={() => removeFooterColumn(idx)} className="absolute top-4 right-4 text-red-500 hover:text-red-400"><Trash2 size={18}/></button>
<div className="grid md:grid-cols-2 gap-4 mb-4">
<InputGroup label="Título da Coluna" value={col.title} onChange={(v: string) => updateFooterColumn(idx, 'title', v)} />
<div>
<label className="block text-xs font-bold text-gray-500 uppercase mb-1">Tipo de Conteúdo</label>
<select
className="w-full bg-zinc-950 border border-zinc-700 rounded p-2 text-white text-sm focus:border-primary outline-none"
value={col.type}
onChange={(e) => updateFooterColumn(idx, 'type', e.target.value)}
>
<option value="custom">Links Personalizados</option>
<option value="services_dynamic">Lista de Serviços (Automático)</option>
<option value="hours">Horário de Atendimento</option>
</select>
</div>
</div>
{col.type === 'custom' && (
<div className="bg-zinc-950 p-4 rounded border border-zinc-800">
<div className="flex justify-between items-center mb-2">
<span className="text-xs font-bold text-gray-500 uppercase">Links da Coluna</span>
<button onClick={() => addFooterLink(idx)} className="text-xs text-primary hover:text-white">+ Adicionar Link</button>
</div>
<div className="space-y-2">
{col.links?.map((link, linkIdx) => (
<div key={linkIdx} className="flex gap-2 items-center">
<input type="text" className="flex-1 bg-zinc-900 border border-zinc-700 rounded p-1.5 text-white text-xs" value={link.label} onChange={(e) => updateFooterLink(idx, linkIdx, 'label', e.target.value)} placeholder="Texto" />
<input type="text" className="flex-1 bg-zinc-900 border border-zinc-700 rounded p-1.5 text-white text-xs" value={link.href} onChange={(e) => updateFooterLink(idx, linkIdx, 'href', e.target.value)} placeholder="URL" />
<button onClick={() => removeFooterLink(idx, linkIdx)} className="text-red-500 p-1"><X size={14}/></button>
</div>
))}
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
);
case 'hero':
return (
<div className="space-y-6">
<h3 className="text-xl font-bold text-white mb-4">Editar Início (Hero)</h3>
<InputGroup label="Título Principal" value={data.hero.title} onChange={(v: string) => updateData('hero', {...data.hero, title: v})} />
<InputGroup label="Subtítulo" value={data.hero.subtitle} onChange={(v: string) => updateData('hero', {...data.hero, subtitle: v})} textarea />
<InputGroup label="Texto do Botão" value={data.hero.buttonText} onChange={(v: string) => updateData('hero', {...data.hero, buttonText: v})} />
<InputGroup label="URL da Imagem de Fundo" value={data.hero.bgImage} onChange={(v: string) => updateData('hero', {...data.hero, bgImage: v})} />
<SectionTextEditor sectionKey="hero" title="Textos do Rodapé (Features)" />
</div>
);
case 'services':
return (
<div className="space-y-8">
<SectionTextEditor sectionKey="services" />
<div className="flex justify-between items-center">
<h3 className="text-xl font-bold text-white">Lista de Serviços</h3>
<button onClick={() => addItem('services', { id: Date.now(), title: 'Novo Serviço', description: 'Descrição', image: '' })} className="flex items-center gap-2 text-primary hover:text-white transition-colors text-sm">
<PlusCircle size={16} /> Adicionar Serviço
</button>
</div>
{data.services.map((service, idx) => (
<div key={idx} className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-lg relative group">
<button onClick={() => removeItem('services', idx)} className="absolute top-4 right-4 text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 size={16}/></button>
<InputGroup label="Título" value={service.title} onChange={(v: string) => updateArrayItem('services', idx, 'title', v)} />
<InputGroup label="Descrição" value={service.description} onChange={(v: string) => updateArrayItem('services', idx, 'description', v)} textarea />
<InputGroup label="URL Imagem" value={service.image} onChange={(v: string) => updateArrayItem('services', idx, 'image', v)} />
</div>
))}
</div>
);
case 'offers':
return (
<div className="space-y-8">
<SectionTextEditor sectionKey="specialOffers" title="Títulos da Seção" />
<div className="flex justify-between items-center">
<h3 className="text-xl font-bold text-white">Lista de Ofertas (Pneus)</h3>
<button onClick={() => addItem('specialOffers', { id: Date.now(), title: 'Novo Pneu', image: '', price: 'R$ 0,00', installment: '', rating: 5, reviews: 0, specs: { fuel: 'C', grip: 'C', noise: '70dB' } })} className="flex items-center gap-2 text-primary hover:text-white transition-colors text-sm">
<PlusCircle size={16} /> Adicionar Pneu
</button>
</div>
{data.specialOffers.map((offer, idx) => (
<div key={idx} className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-lg relative group">
<button onClick={() => removeItem('specialOffers', idx)} className="absolute top-4 right-4 text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 size={16}/></button>
<InputGroup label="Nome do Produto" value={offer.title} onChange={(v: string) => updateArrayItem('specialOffers', idx, 'title', v)} />
<div className="grid grid-cols-2 gap-4">
<InputGroup label="Preço (R$)" value={offer.price} onChange={(v: string) => updateArrayItem('specialOffers', idx, 'price', v)} />
<InputGroup label="Parcelamento" value={offer.installment} onChange={(v: string) => updateArrayItem('specialOffers', idx, 'installment', v)} />
</div>
<InputGroup label="URL Imagem" value={offer.image} onChange={(v: string) => updateArrayItem('specialOffers', idx, 'image', v)} />
<div className="grid grid-cols-3 gap-2 mt-4">
<InputGroup label="Combustível" value={offer.specs.fuel} onChange={(v: string) => {
const newSpecs = { ...offer.specs, fuel: v };
updateArrayItem('specialOffers', idx, 'specs', newSpecs);
}} />
<InputGroup label="Aderência" value={offer.specs.grip} onChange={(v: string) => {
const newSpecs = { ...offer.specs, grip: v };
updateArrayItem('specialOffers', idx, 'specs', newSpecs);
}} />
<InputGroup label="Ruído" value={offer.specs.noise} onChange={(v: string) => {
const newSpecs = { ...offer.specs, noise: v };
updateArrayItem('specialOffers', idx, 'specs', newSpecs);
}} />
</div>
</div>
))}
</div>
);
case 'packages':
return (
<div className="space-y-8">
<SectionTextEditor sectionKey="packages" />
<div className="flex justify-between items-center">
<h3 className="text-xl font-bold text-white">Lista de Pacotes</h3>
<button onClick={() => addItem('packages', { name: 'Novo Pacote', price: 0, features: [], recommended: false })} className="flex items-center gap-2 text-primary hover:text-white transition-colors text-sm">
<PlusCircle size={16} /> Adicionar Pacote
</button>
</div>
{data.packages.map((pkg, idx) => (
<div key={idx} className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-lg relative group">
<button onClick={() => removeItem('packages', idx)} className="absolute top-4 right-4 text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 size={16}/></button>
<InputGroup label="Nome" value={pkg.name} onChange={(v: string) => updateArrayItem('packages', idx, 'name', v)} />
<InputGroup label="Preço" value={pkg.price} onChange={(v: string) => updateArrayItem('packages', idx, 'price', v)} />
<div className="mb-4">
<label className="block text-xs font-bold text-gray-500 uppercase mb-1">Itens (Separar por vírgula)</label>
<textarea
className="w-full bg-zinc-900 border border-zinc-700 rounded p-2 text-white text-sm focus:border-primary outline-none"
value={pkg.features.join(', ')}
onChange={(e) => updateArrayItem('packages', idx, 'features', e.target.value.split(',').map(s => s.trim()))}
/>
</div>
<Toggle label="Recomendado" checked={pkg.recommended} onChange={(v: boolean) => updateArrayItem('packages', idx, 'recommended', v)} />
</div>
))}
</div>
);
case 'gallery':
return (
<div className="space-y-8">
<h3 className="text-xl font-bold text-white">Galeria de Fotos</h3>
<SectionTextEditor sectionKey="gallery" />
<div className="bg-zinc-900 p-6 rounded-lg border border-zinc-800">
<div className="flex flex-col gap-4 mb-6">
<label className="block text-sm font-bold text-gray-400">Adicionar Imagem via Upload</label>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 cursor-pointer bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg transition-colors font-medium">
<Upload size={18} />
Escolher Arquivos
<input type="file" multiple accept="image/*" onChange={handleImageUpload} className="hidden" />
</label>
<span className="text-gray-500 text-xs">Suporta múltiplos arquivos (JPG, PNG)</span>
</div>
</div>
<div className="relative flex items-center gap-2 mb-2">
<div className="h-px bg-zinc-700 flex-1"></div>
<span className="text-gray-500 text-xs uppercase font-bold">OU URL DIRETA</span>
<div className="h-px bg-zinc-700 flex-1"></div>
</div>
<div className="flex gap-2">
<input
type="text"
value={newImageUrl}
onChange={(e) => setNewImageUrl(e.target.value)}
placeholder="https://exemplo.com/imagem.jpg"
className="flex-1 bg-black border border-zinc-700 rounded p-2 text-white text-sm focus:border-primary outline-none"
/>
<button
onClick={handleAddUrlToGallery}
className="bg-zinc-700 hover:bg-zinc-600 text-white px-4 py-2 rounded font-medium transition-colors"
>
Adicionar
</button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{data.gallery.map((img, idx) => (
<div key={idx} className="aspect-square relative group rounded-lg overflow-hidden border border-zinc-800">
<img src={img} alt="Galeria" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<button
onClick={() => removeItem('gallery', idx)}
className="bg-red-500 hover:bg-red-600 text-white p-2 rounded-full transform hover:scale-110 transition-all"
>
<Trash2 size={20} />
</button>
</div>
</div>
))}
</div>
</div>
);
// NOVA SEÇÃO NO EDITOR
case 'beforeAfter':
return (
<div className="space-y-8">
<h3 className="text-xl font-bold text-white">Comparativo Antes e Depois</h3>
<SectionTextEditor sectionKey="beforeAfter" />
<div className="flex justify-between items-center">
<h4 className="text-primary font-bold">Itens do Comparativo</h4>
<button onClick={() => addItem('beforeAfter', { id: Date.now(), title: "Novo Item", imageBefore: "", imageAfter: "" })} className="text-sm text-green-500 hover:text-green-400 flex items-center gap-1"><PlusCircle size={16}/> Adicionar</button>
</div>
<div className="space-y-6">
{data.beforeAfter.map((item, idx) => (
<div key={item.id} className="p-4 bg-zinc-900 border border-zinc-800 rounded-lg relative">
<button onClick={() => removeItem('beforeAfter', idx)} className="absolute top-4 right-4 text-red-500 hover:text-red-400"><Trash2 size={18}/></button>
<InputGroup label="Título do Serviço" value={item.title} onChange={(v: string) => updateArrayItem('beforeAfter', idx, 'title', v)} />
<div className="grid md:grid-cols-2 gap-4">
<div>
<InputGroup label="URL Imagem ANTES" value={item.imageBefore} onChange={(v: string) => updateArrayItem('beforeAfter', idx, 'imageBefore', v)} />
<div className="aspect-video bg-black rounded overflow-hidden">
<img src={item.imageBefore} className="w-full h-full object-cover opacity-70" alt="Preview Antes" />
</div>
</div>
<div>
<InputGroup label="URL Imagem DEPOIS" value={item.imageAfter} onChange={(v: string) => updateArrayItem('beforeAfter', idx, 'imageAfter', v)} />
<div className="aspect-video bg-black rounded overflow-hidden">
<img src={item.imageAfter} className="w-full h-full object-cover" alt="Preview Depois" />
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
case 'team':
return (
<div className="space-y-6">
<SectionTextEditor sectionKey="team" />
<div className="flex justify-between items-center">
<h3 className="text-xl font-bold text-white">Lista da Equipe</h3>
<button onClick={() => addItem('team', { name: 'Nome', role: 'Cargo', image: '' })} className="flex items-center gap-2 text-primary hover:text-white transition-colors text-sm">
<PlusCircle size={16} /> Adicionar Membro
</button>
</div>
{data.team.map((member, idx) => (
<div key={idx} className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-lg flex gap-4 relative group">
<button onClick={() => removeItem('team', idx)} className="absolute top-4 right-4 text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 size={16}/></button>
<div className="w-20 h-20 shrink-0">
<img src={member.image} className="w-full h-full object-cover rounded" />
</div>
<div className="flex-1">
<InputGroup label="Nome" value={member.name} onChange={(v: string) => updateArrayItem('team', idx, 'name', v)} />
<InputGroup label="Cargo" value={member.role} onChange={(v: string) => updateArrayItem('team', idx, 'role', v)} />
<InputGroup label="Foto URL" value={member.image} onChange={(v: string) => updateArrayItem('team', idx, 'image', v)} />
</div>
</div>
))}
</div>
);
case 'faq':
return (
<div className="space-y-6">
<SectionTextEditor sectionKey="faq" />
<div className="flex justify-between items-center">
<h3 className="text-xl font-bold text-white">Lista de Perguntas</h3>
<button onClick={() => addItem('faqs', { question: 'Pergunta', answer: 'Resposta' })} className="flex items-center gap-2 text-primary hover:text-white transition-colors text-sm">
<PlusCircle size={16} /> Adicionar FAQ
</button>
</div>
{data.faqs.map((faq, idx) => (
<div key={idx} className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-lg relative group">
<button onClick={() => removeItem('faqs', idx)} className="absolute top-4 right-4 text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 size={16}/></button>
<InputGroup label="Pergunta" value={faq.question} onChange={(v: string) => updateArrayItem('faqs', idx, 'question', v)} />
<InputGroup label="Resposta" value={faq.answer} onChange={(v: string) => updateArrayItem('faqs', idx, 'answer', v)} textarea />
</div>
))}
</div>
);
case 'clients':
return (
<div className="space-y-6">
<h3 className="text-xl font-bold text-white mb-4">Logos de Clientes</h3>
{data.clients.map((client, idx) => (
<div key={idx} className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-lg relative group flex gap-4 items-center">
<InputGroup label="Nome" value={client.name} onChange={(v: string) => updateArrayItem('clients', idx, 'name', v)} />
<div className="flex-1">
<InputGroup label="URL Logo" value={client.url} onChange={(v: string) => updateArrayItem('clients', idx, 'url', v)} hint="Use PNG transparente" />
</div>
</div>
))}
</div>
);
case 'testimonials':
return (
<div className="space-y-6">
<SectionTextEditor sectionKey="testimonials" />
<div className="flex justify-between items-center">
<h3 className="text-xl font-bold text-white">Lista de Depoimentos</h3>
{/* Nota: em uma implementação completa, adicionar botão para novo depoimento */}
</div>
{data.testimonials.map((t, idx) => (
<div key={idx} className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-lg relative">
<InputGroup label="Nome" value={t.name} onChange={(v: string) => updateArrayItem('testimonials', idx, 'name', v)} />
<InputGroup label="Texto" value={t.text} onChange={(v: string) => updateArrayItem('testimonials', idx, 'text', v)} textarea />
<InputGroup label="Foto URL" value={t.image} onChange={(v: string) => updateArrayItem('testimonials', idx, 'image', v)} />
</div>
))}
</div>
);
case 'about':
return (
<div className="space-y-6">
<SectionTextEditor sectionKey="about" />
<h4 className="text-white font-bold border-b border-zinc-800 pb-2">Itens "O Que Prometemos"</h4>
<div className="grid sm:grid-cols-2 gap-4">
{data.promises.map((item, idx) => (
<div key={idx} className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-lg">
<InputGroup label="Título" value={item.title} onChange={(v: string) => updateArrayItem('promises', idx, 'title', v)} />
<InputGroup label="Descrição" value={item.description} onChange={(v: string) => updateArrayItem('promises', idx, 'description', v)} textarea />
</div>
))}
</div>
</div>
);
case 'blog':
return (
<div className="space-y-6">
<SectionTextEditor sectionKey="blog" />
{/* Lista de posts não editável aqui para simplificar, foca nos títulos */}
</div>
);
default:
return <div className="text-gray-500">Selecione uma opção no menu lateral.</div>;
}
};
const renderPreview = () => {
switch(activeSection) {
case 'hero': return <Hero />;
case 'header': return (
<>
<div className="h-20"></div> {/* Spacer for fixed header */}
<Header />
<div className="p-8 text-center text-gray-500">Conteúdo do Site...</div>
</>
);
case 'footer': return <Footer />;
case 'services': return <ServicesSection />;
case 'offers': return <SpecialOffersSection />;
case 'packages': return <PackagesSection />;
case 'about': return <AboutSection />;
case 'beforeAfter': return <BeforeAfterSection />; // Novo Preview
case 'team': return <TeamSection />;
case 'testimonials': return <TestimonialsSection />;
case 'faq': return <FaqSection />;
case 'blog': return <BlogSection />;
case 'clients': return <ClientsSection />;
case 'gallery': return <GallerySection />;
default: return <div className="flex items-center justify-center h-full text-gray-500">Preview Geral</div>;
}
}
return (
<div className="min-h-screen bg-black flex text-gray-100 font-sans">
{/* Sidebar */}
<aside className="w-64 border-r border-zinc-800 bg-zinc-950 flex flex-col fixed h-full z-20 overflow-y-auto">
<div className="p-6 border-b border-zinc-800">
<div className="text-xl font-black italic tracking-tighter text-white">
CMS <span className="text-primary">.</span>
</div>
</div>
<nav className="flex-1 p-4 space-y-1">
<SidebarItem icon={Settings} label="Configurações Gerais" active={activeSection === 'config'} onClick={() => setActiveSection('config')} />
<SidebarItem icon={MenuIcon} label="Menu & Cabeçalho" active={activeSection === 'header'} onClick={() => setActiveSection('header')} />
<SidebarItem icon={Columns} label="Rodapé" active={activeSection === 'footer'} onClick={() => setActiveSection('footer')} />
<SidebarItem icon={Eye} label="Visibilidade Seções" active={activeSection === 'visibility'} onClick={() => setActiveSection('visibility')} />
<div className="pt-4 pb-2 text-xs font-bold text-gray-600 uppercase">Conteúdo do Site</div>
<SidebarItem icon={LayoutDashboard} label="Início (Hero)" active={activeSection === 'hero'} onClick={() => setActiveSection('hero')} />
<SidebarItem icon={ArrowLeftRight} label="Antes & Depois" active={activeSection === 'beforeAfter'} onClick={() => setActiveSection('beforeAfter')} />
<SidebarItem icon={Tags} label="Ofertas & Pneus" active={activeSection === 'offers'} onClick={() => setActiveSection('offers')} />
<SidebarItem icon={Wrench} label="Serviços" active={activeSection === 'services'} onClick={() => setActiveSection('services')} />
<SidebarItem icon={Users} label="Sobre Nós" active={activeSection === 'about'} onClick={() => setActiveSection('about')} />
<SidebarItem icon={Box} label="Pacotes" active={activeSection === 'packages'} onClick={() => setActiveSection('packages')} />
<SidebarItem icon={Image} label="Galeria" active={activeSection === 'gallery'} onClick={() => setActiveSection('gallery')} />
<SidebarItem icon={Users} label="Equipe" active={activeSection === 'team'} onClick={() => setActiveSection('team')} />
<SidebarItem icon={MessageSquare} label="Depoimentos" active={activeSection === 'testimonials'} onClick={() => setActiveSection('testimonials')} />
<SidebarItem icon={Check} label="FAQ" active={activeSection === 'faq'} onClick={() => setActiveSection('faq')} />
<SidebarItem icon={FileText} label="Blog" active={activeSection === 'blog'} onClick={() => setActiveSection('blog')} />
<SidebarItem icon={Briefcase} label="Clientes/Logos" active={activeSection === 'clients'} onClick={() => setActiveSection('clients')} />
</nav>
<div className="p-4 border-t border-zinc-800">
<button
onClick={handleLogout}
className="flex items-center gap-2 text-red-500 hover:text-red-400 text-sm font-medium w-full px-4 py-2"
>
<LogOut size={16} /> Sair
</button>
</div>
</aside>
{/* Main Content */}
<div className="flex-1 ml-64 flex flex-col h-screen">
{/* Topbar */}
<header className="h-16 border-b border-zinc-800 bg-zinc-950 flex items-center justify-between px-8">
<h2 className="font-semibold text-white capitalize">Editando: {activeSection === 'hero' ? 'Início' : activeSection === 'offers' ? 'Ofertas' : activeSection === 'about' ? 'Sobre' : activeSection === 'header' ? 'Menu' : activeSection === 'footer' ? 'Rodapé' : sectionLabels[activeSection] || activeSection}</h2>
<div className="flex items-center gap-4">
<button
onClick={() => setIsPreviewOpen(!isPreviewOpen)}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm font-medium border ${isPreviewOpen ? 'bg-primary/10 border-primary text-primary' : 'border-zinc-700 text-gray-400'}`}
>
<Monitor size={16} /> {isPreviewOpen ? 'Ocultar Preview' : 'Ver Preview'}
</button>
<button
onClick={handleSave}
className={`flex items-center gap-2 px-4 py-2 rounded font-bold text-sm transition-all duration-300 ${
isSaved
? 'bg-green-600 text-white'
: 'bg-primary hover:brightness-110 text-white'
}`}
>
{isSaved ? <Check size={16} /> : <Save size={16} />}
{isSaved ? 'Salvo!' : 'Publicar Alterações'}
</button>
</div>
</header>
{/* Editor Area */}
<div className="flex-1 flex overflow-hidden">
{/* Form Side */}
<div className={`flex-1 overflow-y-auto p-8 bg-zinc-950 ${isPreviewOpen ? 'max-w-[50%]' : 'max-w-full'}`}>
{renderEditor()}
</div>
{/* Preview Side */}
{isPreviewOpen && (
<div className="flex-1 bg-zinc-900 overflow-y-auto border-l border-zinc-800 relative">
<div className="absolute top-4 right-4 bg-black/80 text-xs px-2 py-1 rounded text-gray-400 z-50">
Preview em Tempo Real
</div>
<div className="opacity-90 pointer-events-none origin-top scale-[0.8] h-[120%] w-[120%] -ml-[10%] mt-8">
{/* Scale hack to make desktop preview fit better */}
{renderPreview()}
</div>
</div>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,76 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Lock, Info } from 'lucide-react';
export const Login: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
if (email === 'cliente@carautocenter.com.br' && password === '123456') {
localStorage.setItem('admin_auth', 'true');
navigate('/admin/dashboard');
} else {
setError('Credenciais inválidas. Tente novamente.');
}
};
return (
<div className="min-h-screen bg-zinc-950 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-zinc-900 border border-zinc-800 p-8 rounded-xl shadow-2xl">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-[#FF6200]/10 rounded-full flex items-center justify-center mx-auto mb-4 text-[#FF6200]">
<Lock size={32} />
</div>
<h2 className="text-2xl font-bold text-white">Acesso Administrativo</h2>
<p className="text-gray-400 mt-2">CMS CAR Auto Center</p>
</div>
{/* Demo Credentials Hint */}
<div className="bg-zinc-800/50 border border-zinc-700 rounded p-3 mb-6 flex items-start gap-3">
<Info className="text-[#FF6200] shrink-0 mt-0.5" size={16} />
<div className="text-xs text-gray-300">
<p className="font-bold text-white mb-1">Credenciais de Demonstração:</p>
<p>Email: <span className="text-gray-400 font-mono">cliente@carautocenter.com.br</span></p>
<p>Senha: <span className="text-gray-400 font-mono">123456</span></p>
</div>
</div>
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-black border border-zinc-800 rounded p-3 text-white focus:border-[#FF6200] focus:outline-none focus:ring-1 focus:ring-[#FF6200]"
placeholder="seu@email.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">Senha</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-black border border-zinc-800 rounded p-3 text-white focus:border-[#FF6200] focus:outline-none focus:ring-1 focus:ring-[#FF6200]"
placeholder="••••••"
/>
</div>
{error && <p className="text-red-500 text-sm text-center">{error}</p>}
<button
type="submit"
className="w-full bg-[#FF6200] text-white font-bold py-3 rounded hover:bg-orange-700 transition-colors"
>
Entrar no Painel
</button>
</form>
</div>
</div>
);
};

760
components/AppContent.tsx Normal file
View File

@@ -0,0 +1,760 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import {
CheckCircle2,
Phone,
Star,
ChevronDown,
ChevronUp,
MapPin,
Mail,
Fuel,
CloudRain,
Volume2,
Lock,
ShieldCheck,
Wrench,
Clock,
Award,
Users,
Search,
Car,
Settings,
Battery,
Thermometer,
Droplet,
Instagram,
Facebook,
Twitter,
Youtube,
Linkedin,
ArrowLeftRight
} from 'lucide-react';
import { Button, Container, SectionTitle, cn } from './Shared';
import { useData } from '../contexts/DataContext';
import { FooterColumn } from '../types';
// ---- HELPERS ----
const IconResolver = ({ name, size = 24, className }: { name: string, size?: number, className?: string }) => {
const icons: any = {
ShieldCheck, Wrench, Clock, Award, Users, Search, Car, Settings, Battery, Thermometer, Droplet
};
const Icon = icons[name] || Settings;
return <Icon size={size} className={className} />;
};
// ---- SUB-COMPONENTS ----
const FeatureItem: React.FC<{ icon: string, title: string, description: string }> = ({ icon, title, description }) => (
<div className="flex gap-4 p-4 rounded-lg hover:bg-zinc-800/50 transition-colors">
<div className="shrink-0 w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
<IconResolver name={icon} size={24} />
</div>
<div>
<h3 className="text-lg font-bold text-white mb-1">{title}</h3>
<p className="text-gray-400 text-sm leading-relaxed">{description}</p>
</div>
</div>
);
const ServiceCard: React.FC<{ service: any, whatsappLink: string }> = ({ service, whatsappLink }) => (
<div className="group relative bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden hover:border-primary/50 transition-all duration-300 flex flex-col h-full">
<div className="h-48 overflow-hidden shrink-0">
<img src={service.image} alt={service.title} className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" />
</div>
<div className="p-6 flex flex-col flex-1">
<h3 className="text-xl font-bold text-white mb-2">{service.title}</h3>
<p className="text-gray-400 text-sm mb-4 flex-1">{service.description}</p>
<a href={whatsappLink} target="_blank" rel="noopener noreferrer" className="text-primary font-semibold text-sm uppercase tracking-wider flex items-center gap-1 group-hover:gap-2 transition-all mt-auto">
Agendar <span className="text-lg"></span>
</a>
</div>
</div>
);
const PackageCard: React.FC<{ pkg: any, whatsappLink: string }> = ({ pkg, whatsappLink }) => (
<div className={cn(
"relative p-8 rounded-2xl border flex flex-col h-full",
pkg.recommended
? "bg-zinc-900 border-primary shadow-[0_0_30px_-10px_rgba(var(--primary-color),0.3)] scale-105 z-10"
: "bg-black border-zinc-800"
)}>
{pkg.recommended && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-primary text-white text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wider">
Mais Popular
</div>
)}
<h3 className="text-xl font-bold text-white mb-2">{pkg.name}</h3>
<div className="flex items-baseline gap-1 mb-6">
<span className="text-sm text-gray-400">R$</span>
<span className="text-4xl font-black text-white">{pkg.price}</span>
<span className="text-sm text-gray-400">/ serviço</span>
</div>
<ul className="flex-1 space-y-4 mb-8">
{pkg.features.map((feat: string, i: number) => (
<li key={i} className="flex items-start gap-3 text-gray-300 text-sm">
<CheckCircle2 size={18} className="text-primary shrink-0 mt-0.5" />
{feat}
</li>
))}
</ul>
<Button variant={pkg.recommended ? 'primary' : 'outline'} className="w-full" onClick={() => window.open(whatsappLink, '_blank')}>
Escolher Plano
</Button>
</div>
);
const AccordionItem: React.FC<{ faq: any }> = ({ faq }) => {
const [isOpen, setIsOpen] = React.useState(false);
return (
<div className="border-b border-zinc-800">
<button
className="w-full py-4 flex items-center justify-between text-left text-white font-medium hover:text-primary transition-colors"
onClick={() => setIsOpen(!isOpen)}
>
{faq.question}
{isOpen ? <ChevronUp size={20} className="text-primary" /> : <ChevronDown size={20} className="text-gray-500" />}
</button>
<div className={cn("overflow-hidden transition-all duration-300", isOpen ? "max-h-40 pb-4" : "max-h-0")}>
<p className="text-gray-400 text-sm">{faq.answer}</p>
</div>
</div>
);
};
// Componente Antes e Depois Slider
const BeforeAfterSlider: React.FC<{ before: string, after: string, title: string }> = ({ before, after, title }) => {
const [sliderPosition, setSliderPosition] = useState(50);
const [isDragging, setIsDragging] = useState(false);
const handleMove = (event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>) => {
if (!isDragging) return;
const rect = event.currentTarget.getBoundingClientRect();
const x = 'touches' in event ? event.touches[0].clientX : (event as React.MouseEvent).clientX;
const position = ((x - rect.left) / rect.width) * 100;
setSliderPosition(Math.min(100, Math.max(0, position)));
};
return (
<div className="relative group">
<h3 className="text-white font-bold mb-2 text-lg">{title}</h3>
<div
className="relative w-full aspect-[4/3] rounded-xl overflow-hidden cursor-ew-resize select-none border border-zinc-700"
onMouseDown={() => setIsDragging(true)}
onMouseUp={() => setIsDragging(false)}
onMouseLeave={() => setIsDragging(false)}
onMouseMove={handleMove}
onTouchStart={() => setIsDragging(true)}
onTouchEnd={() => setIsDragging(false)}
onTouchMove={handleMove}
>
<img src={after} alt="Depois" className="absolute inset-0 w-full h-full object-cover pointer-events-none" />
<div
className="absolute inset-0 w-full h-full overflow-hidden pointer-events-none border-r-2 border-primary bg-black/5"
style={{ width: `${sliderPosition}%` }}
>
<img src={before} alt="Antes" className="absolute inset-0 w-full h-full object-cover max-w-none" style={{ width: '100vw', maxWidth: '100%' }} /> {/* Trick to keep image fixed */}
{/* Fix for width calculation in simple implementation: better to just let it fit container */}
<div className="absolute inset-0 bg-black/0" /> {/* Layer to prevent drag issues */}
<img src={before} alt="Antes" className="absolute top-0 left-0 h-full object-cover max-w-none" style={{ width: '100%' }} />
</div>
{/* Handle */}
<div
className="absolute top-0 bottom-0 w-1 bg-primary cursor-ew-resize z-10 flex items-center justify-center"
style={{ left: `${sliderPosition}%` }}
>
<div className="w-8 h-8 bg-primary rounded-full flex items-center justify-center shadow-lg transform -translate-x-1/2">
<ArrowLeftRight size={16} className="text-white" />
</div>
</div>
{/* Labels */}
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs font-bold px-2 py-1 rounded pointer-events-none">ANTES</div>
<div className="absolute bottom-4 right-4 bg-primary text-white text-xs font-bold px-2 py-1 rounded pointer-events-none">DEPOIS</div>
</div>
{/* Alternative slider logic for better compatibility if custom drag fails often */}
<input
type="range"
min="0"
max="100"
value={sliderPosition}
onChange={(e) => setSliderPosition(Number(e.target.value))}
className="absolute inset-0 w-full h-full opacity-0 cursor-ew-resize z-20"
/>
</div>
);
};
// ---- SECTIONS ----
export const SpecialOffersSection = () => {
const { data, getWhatsAppLink } = useData();
const t = data.texts.specialOffers;
return (
<section id="special-offers" className="py-16 bg-zinc-900/50 border-b border-zinc-900 relative">
<Container>
<div className="flex items-center justify-between mb-8">
<div>
<div className="inline-flex items-center gap-2 mb-2">
<span className="relative flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-primary"></span>
</span>
<span className="text-primary font-bold text-xs uppercase tracking-wider">{t.badge}</span>
</div>
<h2 className="text-3xl font-bold text-white">{t.title}</h2>
</div>
<div className="hidden sm:block text-right">
<p className="text-gray-400 text-sm">{t.subtitle}</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{data.specialOffers.map((offer) => (
<div key={offer.id} className="bg-white rounded-lg p-4 shadow-lg hover:shadow-xl transition-shadow relative group">
<div className="relative mb-4">
<div className="absolute top-0 left-0 bg-red-600 text-white text-[10px] font-bold px-2 py-0.5 rounded-sm z-10 flex items-center gap-1 shadow-sm">
<span className="text-white">🛍 RETIRE NA LOJA</span>
</div>
<div className="absolute top-0 right-0 z-10">
<div className="w-8 h-8 flex items-center justify-center border border-blue-900 rounded-sm bg-white text-[8px] text-blue-900 font-bold leading-none text-center p-0.5">
INMETRO
</div>
</div>
<div className="aspect-square flex items-center justify-center p-2">
<img src={offer.image} alt={offer.title} className="max-w-full max-h-full object-cover rounded-md" />
</div>
</div>
<div className="flex gap-2 mb-3">
<div className="flex items-center bg-[#F37021] text-white rounded px-1.5 py-0.5 text-xs font-bold gap-1">
<Fuel size={12} fill="white" /> <span className="border-l border-white/50 pl-1">{offer.specs.fuel}</span>
</div>
<div className="flex items-center bg-[#F37021] text-white rounded px-1.5 py-0.5 text-xs font-bold gap-1">
<CloudRain size={12} fill="white" /> <span className="border-l border-white/50 pl-1">{offer.specs.grip}</span>
</div>
<div className="flex items-center bg-[#D4E000] text-black rounded px-1.5 py-0.5 text-xs font-bold gap-1">
<Volume2 size={12} /> <span className="border-l border-black/20 pl-1">{offer.specs.noise}</span>
</div>
</div>
<h3 className="text-gray-900 font-semibold text-sm mb-2 leading-tight min-h-[40px]">
{offer.title}
</h3>
<div className="flex items-center gap-1 mb-3">
<div className="flex text-yellow-400">
{[...Array(5)].map((_, i) => (
<Star
key={i}
size={14}
fill={i < Math.floor(offer.rating) ? "currentColor" : "none"}
className={i < Math.floor(offer.rating) ? "text-yellow-400" : "text-gray-300"}
/>
))}
</div>
<span className="text-gray-400 text-xs">({offer.reviews})</span>
</div>
<div className="mt-auto">
<div className="text-3xl font-bold text-gray-900 leading-none mb-1">
{offer.price}
</div>
<div className="text-gray-500 text-xs">
No PIX ou <span className="font-semibold text-gray-700">{offer.installment}</span>
</div>
</div>
<div className="absolute inset-x-0 bottom-0 p-4 opacity-0 group-hover:opacity-100 transition-opacity bg-white/95 backdrop-blur-sm flex items-center justify-center">
<Button size="sm" className="w-full" onClick={() => window.open(getWhatsAppLink(`Olá! Tenho interesse no pneu: ${offer.title}`), '_blank')}>Comprar Agora</Button>
</div>
</div>
))}
</div>
</Container>
</section>
);
};
export const AboutSection = () => {
const { data } = useData();
const t = data.texts.about;
return (
<section id="about" className="py-20 bg-black">
<Container>
<div className="grid lg:grid-cols-2 gap-12 items-center">
<div>
<SectionTitle subtitle={t.subtitle} title={t.title} center={false} />
<div className="grid sm:grid-cols-2 gap-4">
{data.promises.map((item, idx) => <FeatureItem key={idx} {...item} />)}
</div>
</div>
<div className="relative">
<img
src="https://images.unsplash.com/photo-1619642751034-765dfdf7c58e?auto=format&fit=crop&q=80&w=800"
alt="Mecânico"
className="rounded-lg shadow-2xl grayscale hover:grayscale-0 transition-all duration-500"
/>
<div className="absolute -bottom-6 -left-6 bg-white text-black p-6 rounded-lg shadow-xl max-w-[200px]">
<span className="block text-4xl font-black text-primary">{t.badgeYear}</span>
<span className="text-sm font-bold uppercase leading-tight block mt-1">{t.badgeText}</span>
</div>
</div>
</div>
</Container>
</section>
);
};
export const ServicesSection = () => {
const { data, getWhatsAppLink } = useData();
const t = data.texts.services;
return (
<section id="services" className="py-20 bg-zinc-950">
<Container>
<SectionTitle subtitle={t.subtitle} title={t.title} />
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{data.services.map((s) => <ServiceCard key={s.id} service={s} whatsappLink={getWhatsAppLink(`Olá! Gostaria de agendar o serviço: ${s.title}`)} />)}
</div>
</Container>
</section>
);
};
export const BigCTA = () => {
const { data, getWhatsAppLink } = useData();
const t = data.texts.bigCta;
const handleCall = () => {
window.open(getWhatsAppLink("Olá! Preciso de ajuda imediata/emergência."), '_blank');
};
return (
<section className="py-24 relative overflow-hidden flex items-center cursor-pointer group" onClick={handleCall}>
<div className="absolute inset-0 bg-black">
<img src="https://images.unsplash.com/photo-1492144534655-ae79c964c9d7?auto=format&fit=crop&q=80&w=1920" className="w-full h-full object-cover opacity-30" alt="Carro preto" />
<div className="absolute inset-0 bg-gradient-to-t from-black via-transparent to-black" />
</div>
<Container className="relative z-10 text-center">
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6">{t.title}</h2>
<div className="flex flex-col md:flex-row items-center justify-center gap-6">
<div className="text-4xl md:text-6xl font-black text-primary tracking-tighter group-hover:scale-105 transition-transform">
{data.settings.contactPhoneDisplay}
</div>
<span className="bg-white text-black px-3 py-1 rounded font-bold text-sm">{t.buttonText}</span>
</div>
<p className="text-gray-400 mt-4">{t.subtitle}</p>
</Container>
</section>
);
};
export const PackagesSection = () => {
const { data, getWhatsAppLink } = useData();
const t = data.texts.packages;
return (
<section id="packages" className="py-20 bg-black">
<Container>
<SectionTitle subtitle={t.subtitle} title={t.title} />
<div className="grid md:grid-cols-3 gap-8 items-center max-w-5xl mx-auto">
{data.packages.map((p, i) => <PackageCard key={i} pkg={p} whatsappLink={getWhatsAppLink(`Olá! Tenho interesse no pacote: ${p.name}`)} />)}
</div>
</Container>
</section>
);
};
export const GallerySection = () => {
const { data } = useData();
const t = data.texts.gallery;
return (
<section id="gallery" className="py-20 bg-zinc-900">
<Container>
<SectionTitle subtitle={t.subtitle} title={t.title} />
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{data.gallery.map((img, idx) => (
<div key={idx} className="aspect-square overflow-hidden rounded-lg group cursor-pointer">
<img src={img} alt="Galeria" className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" />
</div>
))}
</div>
</Container>
</section>
);
};
// --- NOVA SEÇÃO DE ANTES E DEPOIS ---
export const BeforeAfterSection = () => {
const { data } = useData();
const t = data.texts.beforeAfter;
return (
<section id="results" className="py-20 bg-black">
<Container>
<SectionTitle subtitle={t.subtitle} title={t.title} />
<div className="grid md:grid-cols-2 gap-8">
{data.beforeAfter.map((item) => (
<BeforeAfterSlider
key={item.id}
title={item.title}
before={item.imageBefore}
after={item.imageAfter}
/>
))}
</div>
</Container>
</section>
);
};
export const StatsSection = () => {
const { data } = useData();
return (
<section className="py-16 bg-primary">
<Container>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 text-center text-white">
{data.stats.map((s, i) => (
<div key={i}>
<div className="text-4xl md:text-5xl font-black mb-2">{s.value}</div>
<div className="text-white/80 font-medium uppercase text-sm tracking-wider">{s.label}</div>
</div>
))}
</div>
</Container>
</section>
);
};
export const WhyChooseSection = () => {
const { data } = useData();
const t = data.texts.whyChoose;
return (
<section className="py-20 bg-black overflow-hidden">
<Container>
<div className="grid lg:grid-cols-2 gap-16 items-center">
<div className="order-2 lg:order-1">
<SectionTitle subtitle={t.subtitle} title={t.title} center={false} />
<div className="space-y-6">
{data.whyChoose.map((item, i) => (
<div key={i} className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-zinc-800 flex items-center justify-center text-primary shrink-0">
<IconResolver name={item.icon} size={20} />
</div>
<p className="text-gray-200 font-medium">{item.text}</p>
</div>
))}
</div>
</div>
<div className="order-1 lg:order-2 relative">
<div className="absolute -inset-4 bg-primary rounded-full blur-[100px] opacity-20" />
<img
src="https://images.unsplash.com/photo-1605559424843-9e4c228bf1c2?auto=format&fit=crop&q=80&w=800"
alt="Carro Vermelho"
className="relative z-10 w-full rounded-xl shadow-2xl transform lg:translate-x-12"
/>
</div>
</div>
</Container>
</section>
);
};
export const TestimonialsSection = () => {
const { data } = useData();
const t = data.texts.testimonials;
return (
<section id="testimonials" className="py-20 bg-zinc-950">
<Container>
<SectionTitle subtitle={t.subtitle} title={t.title} />
<div className="grid md:grid-cols-3 gap-8">
{data.testimonials.map((t, i) => (
<div key={i} className="bg-zinc-900 p-8 rounded-xl border border-zinc-800">
<div className="flex gap-1 text-primary mb-4">
{[...Array(5)].map((_, i) => <Star key={i} size={16} fill="currentColor" />)}
</div>
<p className="text-gray-300 mb-6 italic">"{t.text}"</p>
<div className="flex items-center gap-4">
<img src={t.image} alt={t.name} className="w-12 h-12 rounded-full object-cover" />
<div>
<h4 className="text-white font-bold text-sm">{t.name}</h4>
<p className="text-gray-500 text-xs">{t.role}</p>
</div>
</div>
</div>
))}
</div>
</Container>
</section>
);
};
export const TeamSection = () => {
const { data } = useData();
const t = data.texts.team;
return (
<section className="py-20 bg-black">
<Container>
<SectionTitle subtitle={t.subtitle} title={t.title} />
<div className="grid sm:grid-cols-2 md:grid-cols-4 gap-6">
{data.team.map((member, i) => (
<div key={i} className="group text-center">
<div className="mb-4 overflow-hidden rounded-xl aspect-[3/4]">
<img src={member.image} alt={member.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 filter grayscale group-hover:grayscale-0" />
</div>
<h3 className="text-white font-bold">{member.name}</h3>
<p className="text-primary text-sm">{member.role}</p>
</div>
))}
</div>
</Container>
</section>
);
};
export const FaqSection = () => {
const { data } = useData();
const t = data.texts.faq;
return (
<section className="py-20 bg-zinc-900">
<Container>
<div className="max-w-3xl mx-auto">
<SectionTitle subtitle={t.subtitle} title={t.title} />
<div className="space-y-2">
{data.faqs.map((faq, i) => <AccordionItem key={i} faq={faq} />)}
</div>
</div>
</Container>
</section>
);
};
export const BlogSection = () => {
const { data } = useData();
const t = data.texts.blog;
return (
<section id="blog" className="py-20 bg-black">
<Container>
<SectionTitle subtitle={t.subtitle} title={t.title} />
<div className="grid md:grid-cols-3 gap-8">
{data.blog.map((post, i) => (
<div key={i} className="bg-zinc-900 rounded-xl overflow-hidden group flex flex-col h-full">
<div className="h-48 overflow-hidden relative shrink-0">
<img src={post.image} alt={post.title} className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" />
<div className="absolute top-4 left-4 bg-primary text-white text-xs font-bold px-2 py-1 rounded">
{post.date}
</div>
</div>
<div className="p-6 flex flex-col flex-1">
<h3 className="text-white font-bold text-xl mb-3 group-hover:text-primary transition-colors">{post.title}</h3>
<p className="text-gray-400 text-sm mb-4 line-clamp-2 flex-1">{post.excerpt}</p>
<Link to={`/blog/${post.id}`} className="text-white text-sm font-semibold border-b border-primary pb-0.5 hover:text-primary transition-colors inline-block w-fit">
Ler mais
</Link>
</div>
</div>
))}
</div>
</Container>
</section>
);
};
export const ContactSection = () => {
const { data } = useData();
const t = data.texts.contact;
return (
<section id="contact" className="py-20 bg-zinc-950">
<Container>
<div className="grid lg:grid-cols-2 gap-12">
<div>
<SectionTitle subtitle={t.subtitle} title={t.title} center={false} />
<p className="text-gray-400 mb-8">
{t.description}
</p>
<div className="space-y-6 mb-8">
<div className="flex items-start gap-4">
<div className="bg-zinc-900 p-3 rounded text-primary"><MapPin /></div>
<div>
<h4 className="text-white font-bold">Endereço</h4>
<p className="text-gray-400 text-sm">{data.settings.address}</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="bg-zinc-900 p-3 rounded text-primary"><Phone /></div>
<div>
<h4 className="text-white font-bold">Telefone</h4>
<p className="text-gray-400 text-sm">{data.settings.contactPhoneDisplay}</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="bg-zinc-900 p-3 rounded text-primary"><Mail /></div>
<div>
<h4 className="text-white font-bold">Email</h4>
<p className="text-gray-400 text-sm">{data.settings.contactEmail}</p>
</div>
</div>
</div>
</div>
<form action={`mailto:${data.settings.contactEmail}`} method="post" encType="text/plain" className="bg-zinc-900 p-8 rounded-2xl border border-zinc-800 space-y-4">
<div className="grid grid-cols-2 gap-4">
<input type="text" name="nome" placeholder="Nome" className="bg-black border border-zinc-800 rounded p-3 text-white focus:border-primary focus:outline-none w-full" />
<input type="text" name="telefone" placeholder="Telefone" className="bg-black border border-zinc-800 rounded p-3 text-white focus:border-primary focus:outline-none w-full" />
</div>
<input type="email" name="email" placeholder="Email" className="bg-black border border-zinc-800 rounded p-3 text-white focus:border-primary focus:outline-none w-full" />
<textarea rows={4} name="mensagem" placeholder="Mensagem" className="bg-black border border-zinc-800 rounded p-3 text-white focus:border-primary focus:outline-none w-full"></textarea>
<Button type="submit" className="w-full">Enviar Mensagem</Button>
</form>
</div>
</Container>
</section>
);
};
export const ClientsSection = () => {
const { data } = useData();
return (
<section className="py-12 bg-black border-t border-zinc-900">
<Container>
<p className="text-center text-gray-500 text-sm uppercase tracking-widest mb-8">Empresas que confiam na CAR</p>
<div className="flex flex-wrap justify-center gap-12 opacity-50 grayscale hover:grayscale-0 transition-all duration-500 items-center">
{data.clients.map((client, i) => (
client.url ? (
<img key={i} src={client.url} alt={client.name} className="h-12 object-contain" />
) : (
<span key={i} className="text-2xl font-black text-white">{client.name}</span>
)
))}
</div>
</Container>
</section>
);
};
// Renderiza o conteúdo de uma coluna baseado no tipo
const FooterColumnRenderer: React.FC<{ column: FooterColumn }> = ({ column }) => {
const { data } = useData();
if (column.type === 'services_dynamic') {
return (
<div>
<h4 className="text-white font-bold mb-4">{column.title}</h4>
<ul className="space-y-2 text-sm">
{data.services.slice(0, 4).map((s) => (
<li key={s.id}><Link to="/servicos" className="hover:text-primary">{s.title}</Link></li>
))}
</ul>
</div>
);
}
if (column.type === 'hours') {
return (
<div>
<h4 className="text-white font-bold mb-4">{column.title}</h4>
<ul className="space-y-2 text-sm">
<li>Seg - Sex: 08h - 18h</li>
<li>Sábado: 08h - 14h</li>
<li className="text-primary font-bold">Emergência 24h: {data.settings.contactPhoneDisplay}</li>
</ul>
</div>
);
}
if (column.type === 'custom' && column.links) {
return (
<div>
<h4 className="text-white font-bold mb-4">{column.title}</h4>
<ul className="space-y-2 text-sm">
{column.links.map((link, idx) => (
<li key={idx}>
{link.href.startsWith('/') ? (
<Link to={link.href} className="hover:text-primary">{link.label}</Link>
) : (
<a href={link.href} className="hover:text-primary">{link.label}</a>
)}
</li>
))}
</ul>
</div>
);
}
return null;
}
export const Footer = () => {
const { data } = useData();
return (
<footer className="bg-zinc-950 border-t border-zinc-900 py-12 text-gray-400">
<Container>
<div className="grid md:grid-cols-4 gap-8 mb-8">
{/* Logo & Descrição sempre primeiro */}
<div>
<Link to="/" className="flex items-center gap-2 mb-4">
{data.settings.logoUrl ? (
<img src={data.settings.logoUrl} alt={data.settings.siteName} className="h-10 object-contain" />
) : (
<div className="text-2xl font-black italic tracking-tighter text-white">
{data.settings.siteName}<span className="text-primary">.</span>
</div>
)}
</Link>
<p className="text-sm mb-6">
{data.footer.description}
</p>
{/* Social Icons Render */}
<div className="flex gap-4">
{data.settings.social?.instagram && (
<a href={data.settings.social.instagram} target="_blank" rel="noopener noreferrer" className="bg-zinc-900 p-2 rounded-full hover:bg-primary hover:text-white transition-all">
<Instagram size={18} />
</a>
)}
{data.settings.social?.facebook && (
<a href={data.settings.social.facebook} target="_blank" rel="noopener noreferrer" className="bg-zinc-900 p-2 rounded-full hover:bg-primary hover:text-white transition-all">
<Facebook size={18} />
</a>
)}
{data.settings.social?.twitter && (
<a href={data.settings.social.twitter} target="_blank" rel="noopener noreferrer" className="bg-zinc-900 p-2 rounded-full hover:bg-primary hover:text-white transition-all">
<Twitter size={18} />
</a>
)}
{data.settings.social?.youtube && (
<a href={data.settings.social.youtube} target="_blank" rel="noopener noreferrer" className="bg-zinc-900 p-2 rounded-full hover:bg-primary hover:text-white transition-all">
<Youtube size={18} />
</a>
)}
{data.settings.social?.linkedin && (
<a href={data.settings.social.linkedin} target="_blank" rel="noopener noreferrer" className="bg-zinc-900 p-2 rounded-full hover:bg-primary hover:text-white transition-all">
<Linkedin size={18} />
</a>
)}
</div>
</div>
{/* Renderiza Colunas Dinâmicas */}
{data.footer.columns.map((col) => (
<FooterColumnRenderer key={col.id} column={col} />
))}
</div>
<div className="border-t border-zinc-900 pt-8 flex flex-col md:flex-row justify-between items-center text-xs">
<p>&copy; 2024 {data.settings.siteName}. Todos os direitos reservados.</p>
<div className="flex gap-4 mt-4 md:mt-0 items-center">
{data.footer.bottomLinks.map((link, idx) => (
<a key={idx} href={link.href} className="hover:text-white">{link.label}</a>
))}
<Link to="/admin" className="hover:text-primary flex items-center gap-1 border-l border-zinc-800 pl-4 ml-2">
<Lock size={12} /> Área Administrativa
</Link>
</div>
</div>
</Container>
</footer>
);
};

108
components/Header.tsx Normal file
View File

@@ -0,0 +1,108 @@
import React, { useState, useEffect } from 'react';
import { Menu, X, Phone } from 'lucide-react';
import { Link, useLocation } from 'react-router-dom';
import { Button, Container } from './Shared';
import { useData } from '../contexts/DataContext';
export const Header: React.FC = () => {
const { data, getWhatsAppLink } = useData();
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const location = useLocation();
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 50);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const handleCtaClick = () => {
const { url } = data.header.ctaButton;
if (url && url.trim() !== '') {
window.open(url, '_blank');
} else {
window.open(getWhatsAppLink("Olá! Gostaria de agendar um horário."), '_blank');
}
};
return (
<header
className={`fixed w-full z-50 transition-all duration-300 ${
isScrolled ? 'bg-black/90 backdrop-blur-md py-4 shadow-lg border-b border-zinc-800' : 'bg-transparent py-6'
}`}
>
<Container>
<div className="flex items-center justify-between">
<Link to="/" className="flex items-center gap-2">
{data.settings.logoUrl ? (
<img src={data.settings.logoUrl} alt={data.settings.siteName} className="h-10 object-contain" />
) : (
<div className="text-2xl font-black italic tracking-tighter text-white">
{data.settings.siteName}<span className="text-primary">.</span>
</div>
)}
</Link>
{/* Desktop Nav */}
<nav className="hidden lg:flex items-center gap-8">
{data.header.items.map((item, index) => (
<Link
key={index}
to={item.href}
className={`text-sm font-medium transition-colors ${
location.pathname === item.href
? 'text-primary'
: 'text-gray-300 hover:text-primary'
}`}
>
{item.label}
</Link>
))}
</nav>
<div className="hidden lg:flex items-center gap-4">
<div className="flex items-center gap-2 text-white mr-4">
<Phone size={18} className="text-primary" />
<span className="font-semibold text-sm">{data.settings.contactPhoneDisplay}</span>
</div>
{data.header.ctaButton.show && (
<Button size="sm" onClick={handleCtaClick}>{data.header.ctaButton.text}</Button>
)}
</div>
{/* Mobile Toggle */}
<button
className="lg:hidden text-white"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
{isMobileMenuOpen ? <X size={28} /> : <Menu size={28} />}
</button>
</div>
{/* Mobile Menu */}
{isMobileMenuOpen && (
<div className="absolute top-full left-0 w-full bg-zinc-900 border-t border-zinc-800 p-6 lg:hidden flex flex-col gap-4 shadow-2xl">
{data.header.items.map((item, index) => (
<Link
key={index}
to={item.href}
className={`text-lg font-medium hover:text-primary ${
location.pathname === item.href ? 'text-primary' : 'text-gray-200'
}`}
onClick={() => setIsMobileMenuOpen(false)}
>
{item.label}
</Link>
))}
<hr className="border-zinc-800 my-2"/>
{data.header.ctaButton.show && (
<Button className="w-full" onClick={handleCtaClick}>{data.header.ctaButton.text}</Button>
)}
</div>
)}
</Container>
</header>
);
};

64
components/Hero.tsx Normal file
View File

@@ -0,0 +1,64 @@
import React from 'react';
import { ArrowRight, Settings } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button, Container } from './Shared';
import { useData } from '../contexts/DataContext';
export const Hero: React.FC = () => {
const { data } = useData();
const { hero, texts } = data;
return (
<section id="home" className="relative min-h-screen flex items-center justify-center pt-20 overflow-hidden">
{/* Background Image with Overlay */}
<div className="absolute inset-0 z-0">
<img
src={hero.bgImage}
alt="Oficina Mecânica"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-r from-black via-black/80 to-transparent" />
</div>
<Container className="relative z-10 w-full">
<div className="max-w-2xl">
{/* Badge atualizado com border-primary sólido */}
<div className="inline-flex items-center gap-2 bg-primary/10 border border-primary rounded-full px-4 py-1 mb-6 backdrop-blur-sm">
<Settings size={16} className="text-primary" />
<span className="text-primary text-sm font-semibold uppercase tracking-wider">Centro Automotivo Premium</span>
</div>
<h1 className="text-5xl md:text-7xl font-black text-white leading-tight mb-6">
<span dangerouslySetInnerHTML={{ __html: hero.title.replace(/\n/g, '<br/>') }} />
</h1>
<p className="text-xl text-gray-300 mb-8 border-l-4 border-primary pl-4">
{hero.subtitle}
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Button size="lg" className="shadow-[0_0_30px_-5px_rgba(var(--primary-color),0.4)]" onClick={() => window.open(`https://wa.me/${data.settings.whatsappNumber}`, '_blank')}>
{hero.buttonText} <ArrowRight size={20} />
</Button>
<Link to="/servicos">
<Button size="lg" variant="white" className="w-full sm:w-auto">
Ver Serviços
</Button>
</Link>
</div>
<div className="mt-12 flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:gap-8 text-gray-400 text-sm font-medium">
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
{texts.hero.feature1}
</div>
<div className="hidden sm:block">|</div>
<div>{texts.hero.feature2}</div>
<div className="hidden sm:block">|</div>
<div>{texts.hero.feature3}</div>
</div>
</div>
</Container>
</section>
);
};

176
components/Pages.tsx Normal file
View File

@@ -0,0 +1,176 @@
import React, { useEffect } from 'react';
import { Hero } from './Hero';
import {
AboutSection,
ServicesSection,
BigCTA,
PackagesSection,
GallerySection,
StatsSection,
WhyChooseSection,
TestimonialsSection,
TeamSection,
FaqSection,
BlogSection,
ContactSection,
ClientsSection,
SpecialOffersSection,
BeforeAfterSection // Importado
} from './AppContent';
import { useData } from '../contexts/DataContext';
import { Container, Button } from './Shared';
import { useParams, Link } from 'react-router-dom';
import { ArrowLeft, Calendar } from 'lucide-react';
// Componente para rolar para o topo ao mudar de página
const ScrollToTop = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return null;
};
export const HomePage = () => {
const { data } = useData();
const { visibility } = data;
return (
<>
<ScrollToTop />
{visibility.hero && <Hero />}
{visibility.specialOffers && <SpecialOffersSection />}
{visibility.about && <AboutSection />}
{visibility.services && <ServicesSection />}
{visibility.beforeAfter && <BeforeAfterSection />}
{visibility.bigCta && <BigCTA />}
{visibility.packages && <PackagesSection />}
{visibility.gallery && <GallerySection />}
{visibility.stats && <StatsSection />}
{visibility.whyChoose && <WhyChooseSection />}
{visibility.testimonials && <TestimonialsSection />}
{visibility.team && <TeamSection />}
{visibility.faq && <FaqSection />}
{visibility.blog && <BlogSection />}
{visibility.contact && <ContactSection />}
{visibility.clients && <ClientsSection />}
</>
);
};
export const AboutPage = () => (
<>
<ScrollToTop />
<div className="pt-20 bg-zinc-950">
<AboutSection />
<StatsSection />
<WhyChooseSection />
<TeamSection />
<ClientsSection />
</div>
</>
);
export const PromotionsPage = () => (
<>
<ScrollToTop />
<div className="pt-20 bg-zinc-950 min-h-screen">
<div className="py-12 bg-black text-center">
<Container>
<h1 className="text-4xl font-bold text-white mb-4">Promoções & Ofertas</h1>
<p className="text-gray-400">Confira nossas condições especiais para pneus e revisões.</p>
</Container>
</div>
<SpecialOffersSection />
<PackagesSection />
<BigCTA />
</div>
</>
);
export const ServicesPage = () => (
<>
<ScrollToTop />
<div className="pt-20 bg-zinc-950">
<ServicesSection />
<BigCTA />
<PackagesSection />
<GallerySection />
</div>
</>
);
export const BlogPage = () => (
<>
<ScrollToTop />
<div className="pt-20 bg-zinc-950 min-h-screen">
<div className="py-12 bg-black text-center border-b border-zinc-900">
<Container>
<h1 className="text-4xl font-bold text-white mb-4">Blog Automotivo</h1>
<p className="text-gray-400">Dicas, manutenção e novidades do mundo automotivo.</p>
</Container>
</div>
<BlogSection />
</div>
</>
);
export const BlogPostPage = () => {
const { id } = useParams();
const { data } = useData();
const post = data.blog.find(p => p.id === Number(id));
if (!post) {
return (
<div className="pt-32 pb-20 text-center min-h-screen">
<h2 className="text-2xl font-bold text-white mb-4">Artigo não encontrado</h2>
<Link to="/blog"><Button>Voltar para o Blog</Button></Link>
</div>
)
}
return (
<>
<ScrollToTop />
<article className="pt-24 pb-20 min-h-screen bg-zinc-950">
<Container className="max-w-4xl">
<Link to="/blog" className="inline-flex items-center gap-2 text-gray-400 hover:text-primary mb-8 transition-colors">
<ArrowLeft size={20} /> Voltar para o Blog
</Link>
<div className="aspect-video w-full overflow-hidden rounded-2xl mb-8">
<img src={post.image} alt={post.title} className="w-full h-full object-cover" />
</div>
<div className="flex items-center gap-2 text-primary text-sm font-bold mb-4 uppercase tracking-wider">
<Calendar size={16} />
{post.date}
</div>
<h1 className="text-3xl md:text-5xl font-black text-white mb-8 leading-tight">
{post.title}
</h1>
<div
className="prose prose-invert prose-lg max-w-none text-gray-300
prose-headings:text-white prose-a:text-primary prose-strong:text-white"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
<div className="mt-12 pt-12 border-t border-zinc-800">
<h3 className="text-white font-bold text-xl mb-6">Gostou da dica?</h3>
<BigCTA />
</div>
</Container>
</article>
</>
)
}
export const ContactPage = () => (
<>
<ScrollToTop />
<div className="pt-20 bg-zinc-950">
<ContactSection />
<FaqSection />
</div>
</>
);

75
components/Shared.tsx Normal file
View File

@@ -0,0 +1,75 @@
import React from 'react';
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'outline' | 'white' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
export const Button: React.FC<ButtonProps> = ({
children,
className,
variant = 'primary',
size = 'md',
...props
}) => {
const variants = {
primary: 'bg-primary text-white hover:brightness-110 border border-transparent',
outline: 'bg-transparent border-2 border-primary text-primary hover:bg-primary hover:text-white',
white: 'bg-white text-black hover:bg-gray-200 border border-transparent',
ghost: 'bg-transparent text-white hover:bg-white/10'
};
const sizes = {
sm: 'px-4 py-2 text-sm',
md: 'px-6 py-3 text-base',
lg: 'px-8 py-4 text-lg font-semibold'
};
return (
<button
className={cn(
'rounded-md transition-all duration-300 flex items-center justify-center gap-2',
variants[variant],
sizes[size],
className
)}
{...props}
>
{children}
</button>
);
};
export const SectionTitle: React.FC<{ subtitle: string; title: string; center?: boolean; light?: boolean }> = ({
subtitle,
title,
center = true,
light = true
}) => {
return (
<div className={cn('mb-12', center ? 'text-center' : 'text-left')}>
<span className="text-primary font-bold uppercase tracking-widest text-sm mb-2 block">
{subtitle}
</span>
<h2 className={cn('text-3xl md:text-4xl font-bold', light ? 'text-white' : 'text-gray-900')}>
{title}
</h2>
<div className={cn(
'h-1 w-20 bg-primary mt-4 rounded-full',
center ? 'mx-auto' : ''
)} />
</div>
);
};
export const Container: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className }) => (
<div className={cn('max-w-7xl mx-auto px-4 sm:px-6 lg:px-8', className)}>
{children}
</div>
);