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>
);
};