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:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
104
App.tsx
Normal file
104
App.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
||||||
|
import { Header } from './components/Header';
|
||||||
|
import { Login } from './components/Admin/Login';
|
||||||
|
import { Dashboard } from './components/Admin/Dashboard';
|
||||||
|
import { useData } from './contexts/DataContext';
|
||||||
|
import { Footer } from './components/AppContent';
|
||||||
|
|
||||||
|
// Páginas
|
||||||
|
import {
|
||||||
|
HomePage,
|
||||||
|
AboutPage,
|
||||||
|
ServicesPage,
|
||||||
|
PromotionsPage,
|
||||||
|
BlogPage,
|
||||||
|
BlogPostPage,
|
||||||
|
ContactPage
|
||||||
|
} from './components/Pages';
|
||||||
|
|
||||||
|
// Layout do site público com Tema Dinâmico e Estrutura Fixa
|
||||||
|
const PublicLayout = () => {
|
||||||
|
const { data, getWhatsAppLink } = useData();
|
||||||
|
|
||||||
|
// Aplica cor primária dinamicamente
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.style.setProperty('--primary-color', data.settings.primaryColor);
|
||||||
|
|
||||||
|
// Atualiza Favicon
|
||||||
|
if (data.settings.faviconUrl) {
|
||||||
|
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
|
||||||
|
if (link) {
|
||||||
|
link.href = data.settings.faviconUrl;
|
||||||
|
} else {
|
||||||
|
const newLink = document.createElement('link');
|
||||||
|
newLink.rel = 'icon';
|
||||||
|
newLink.href = data.settings.faviconUrl;
|
||||||
|
document.head.appendChild(newLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualiza Título
|
||||||
|
document.title = data.settings.siteName;
|
||||||
|
}, [data.settings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-zinc-950 min-h-screen font-sans text-gray-100 selection:bg-primary selection:text-white flex flex-col">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-1">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
<a
|
||||||
|
href={getWhatsAppLink("Olá! Gostaria de mais informações.")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="fixed bottom-8 right-8 z-50 bg-green-500 text-white p-4 rounded-full shadow-lg hover:scale-110 transition-transform hover:bg-green-600 flex items-center justify-center"
|
||||||
|
aria-label="Contato WhatsApp"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" fill="white" width="32" height="32">
|
||||||
|
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Proteção de rota simples
|
||||||
|
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const isAuth = localStorage.getItem('admin_auth') === 'true';
|
||||||
|
return isAuth ? <>{children}</> : <Navigate to="/admin" replace />;
|
||||||
|
};
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
{/* Rotas Públicas */}
|
||||||
|
<Route path="/" element={<PublicLayout />}>
|
||||||
|
<Route index element={<HomePage />} />
|
||||||
|
<Route path="sobre" element={<AboutPage />} />
|
||||||
|
<Route path="servicos" element={<ServicesPage />} />
|
||||||
|
<Route path="promocoes" element={<PromotionsPage />} />
|
||||||
|
<Route path="blog" element={<BlogPage />} />
|
||||||
|
<Route path="blog/:id" element={<BlogPostPage />} />
|
||||||
|
<Route path="contato" element={<ContactPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Rotas Administrativas */}
|
||||||
|
<Route path="/admin" element={<Login />} />
|
||||||
|
<Route
|
||||||
|
path="/admin/dashboard"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Dashboard />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Rota Catch-all para redirecionar para Home se nada corresponder */}
|
||||||
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
25
README.md
25
README.md
@@ -1,11 +1,20 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||||
|
|
||||||
<h1>Built with AI Studio</h2>
|
|
||||||
|
|
||||||
<p>The fastest path from prompt to production with Gemini.</p>
|
|
||||||
|
|
||||||
<a href="https://aistudio.google.com/apps">Start building</a>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
# Run and deploy your AI Studio app
|
||||||
|
|
||||||
|
This contains everything you need to run your app locally.
|
||||||
|
|
||||||
|
View your app in AI Studio: https://ai.studio/apps/drive/16UZt4XL5C3Sit2eoOCGMMWHc9ox45SJD
|
||||||
|
|
||||||
|
## Run Locally
|
||||||
|
|
||||||
|
**Prerequisites:** Node.js
|
||||||
|
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
`npm install`
|
||||||
|
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||||
|
3. Run the app:
|
||||||
|
`npm run dev`
|
||||||
|
|||||||
880
components/Admin/Dashboard.tsx
Normal file
880
components/Admin/Dashboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
76
components/Admin/Login.tsx
Normal file
76
components/Admin/Login.tsx
Normal 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
760
components/AppContent.tsx
Normal 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>© 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
108
components/Header.tsx
Normal 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
64
components/Hero.tsx
Normal 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
176
components/Pages.tsx
Normal 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
75
components/Shared.tsx
Normal 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>
|
||||||
|
);
|
||||||
123
contexts/DataContext.tsx
Normal file
123
contexts/DataContext.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
PROMISES, SERVICES, PACKAGES, SPECIAL_OFFERS,
|
||||||
|
GALLERY_IMAGES, STATS, WHY_CHOOSE_ITEMS, TESTIMONIALS,
|
||||||
|
TEAM, FAQS, BLOG_POSTS, DEFAULT_SETTINGS, DEFAULT_VISIBILITY, CLIENT_LOGOS, DEFAULT_TEXTS,
|
||||||
|
DEFAULT_HEADER, DEFAULT_FOOTER, BEFORE_AFTER_ITEMS
|
||||||
|
} from '../data';
|
||||||
|
import { supabase, isSupabaseConfigured } from '../lib/supabase';
|
||||||
|
import { GlobalSettings, SectionVisibility, SectionTexts, HeaderConfig, FooterConfig, BeforeAfterItem } from '../types';
|
||||||
|
|
||||||
|
// Estado inicial combina todos os dados
|
||||||
|
const INITIAL_DATA = {
|
||||||
|
settings: DEFAULT_SETTINGS,
|
||||||
|
visibility: DEFAULT_VISIBILITY,
|
||||||
|
hero: {
|
||||||
|
title: "Manutenção e Reparos <span class='text-primary'>Automotivos</span>",
|
||||||
|
subtitle: "24h de atendimento • Especialistas em todas as marcas • Garantia em todos os serviços.",
|
||||||
|
bgImage: "https://images.unsplash.com/photo-1625047509168-a7026f36de04?auto=format&fit=crop&q=80&w=1920",
|
||||||
|
buttonText: "Agendar Agora"
|
||||||
|
},
|
||||||
|
texts: DEFAULT_TEXTS, // Novos textos
|
||||||
|
header: DEFAULT_HEADER, // Config do Menu
|
||||||
|
footer: DEFAULT_FOOTER, // Config do Rodapé
|
||||||
|
promises: PROMISES,
|
||||||
|
services: SERVICES,
|
||||||
|
packages: PACKAGES,
|
||||||
|
specialOffers: SPECIAL_OFFERS,
|
||||||
|
gallery: GALLERY_IMAGES,
|
||||||
|
beforeAfter: BEFORE_AFTER_ITEMS, // Nova seção
|
||||||
|
stats: STATS,
|
||||||
|
whyChoose: WHY_CHOOSE_ITEMS,
|
||||||
|
testimonials: TESTIMONIALS,
|
||||||
|
team: TEAM,
|
||||||
|
faqs: FAQS,
|
||||||
|
blog: BLOG_POSTS,
|
||||||
|
clients: CLIENT_LOGOS
|
||||||
|
};
|
||||||
|
|
||||||
|
type DataType = typeof INITIAL_DATA;
|
||||||
|
|
||||||
|
interface DataContextType {
|
||||||
|
data: DataType;
|
||||||
|
updateData: (section: keyof DataType, newData: any) => Promise<void>;
|
||||||
|
isLoading: boolean;
|
||||||
|
getWhatsAppLink: (message?: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DataContext = createContext<DataContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const DataProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [data, setData] = useState<DataType>(INITIAL_DATA);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
// Carregar dados ao iniciar
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
if (isSupabaseConfigured()) {
|
||||||
|
const { data: dbData, error } = await supabase.from('site_content').select('*').single();
|
||||||
|
if (dbData && !error) {
|
||||||
|
// Merge with initial data to ensure new fields are present if DB is old
|
||||||
|
setData(prev => ({
|
||||||
|
...INITIAL_DATA,
|
||||||
|
...dbData.content,
|
||||||
|
texts: { ...INITIAL_DATA.texts, ...dbData.content.texts },
|
||||||
|
header: { ...INITIAL_DATA.header, ...dbData.content.header },
|
||||||
|
footer: { ...INITIAL_DATA.footer, ...dbData.content.footer }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const localData = localStorage.getItem('car_site_data');
|
||||||
|
if (localData) {
|
||||||
|
const parsedData = JSON.parse(localData);
|
||||||
|
setData(prev => ({
|
||||||
|
...INITIAL_DATA,
|
||||||
|
...parsedData,
|
||||||
|
texts: { ...INITIAL_DATA.texts, ...parsedData.texts },
|
||||||
|
header: { ...INITIAL_DATA.header, ...parsedData.header },
|
||||||
|
footer: { ...INITIAL_DATA.footer, ...parsedData.footer }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar dados", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateData = async (section: keyof DataType, newData: any) => {
|
||||||
|
const updatedFullData = { ...data, [section]: newData };
|
||||||
|
setData(updatedFullData);
|
||||||
|
|
||||||
|
if (isSupabaseConfigured()) {
|
||||||
|
await supabase.from('site_content').upsert({ id: 1, content: updatedFullData });
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('car_site_data', JSON.stringify(updatedFullData));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWhatsAppLink = (message: string = '') => {
|
||||||
|
const phone = data.settings.whatsappNumber.replace(/\D/g, '');
|
||||||
|
const encodedMessage = encodeURIComponent(message);
|
||||||
|
return `https://wa.me/${phone}?text=${encodedMessage}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataContext.Provider value={{ data, updateData, isLoading, getWhatsAppLink }}>
|
||||||
|
{children}
|
||||||
|
</DataContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useData = () => {
|
||||||
|
const context = useContext(DataContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useData must be used within a DataProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
373
data.ts
Normal file
373
data.ts
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import { NavItem, Service, PromiseItem, Package, Stat, Testimonial, TeamMember, FaqItem, BlogPost, SpecialOffer, WhyChooseItem, GlobalSettings, SectionVisibility, ClientLogo, SectionTexts, HeaderConfig, FooterConfig, BeforeAfterItem } from './types';
|
||||||
|
|
||||||
|
// Mantemos NAV_ITEMS como fallback ou para inicialização
|
||||||
|
export const NAV_ITEMS: NavItem[] = [
|
||||||
|
{ label: 'Início', href: '/' },
|
||||||
|
{ label: 'Sobre', href: '/sobre' },
|
||||||
|
{ label: 'Promoções', href: '/promocoes' },
|
||||||
|
{ label: 'Serviços', href: '/servicos' },
|
||||||
|
{ label: 'Blog', href: '/blog' },
|
||||||
|
{ label: 'Contato', href: '/contato' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PROMISES: PromiseItem[] = [
|
||||||
|
{ icon: 'Search', title: 'Transparência Total', description: 'Você acompanha cada etapa do reparo.' },
|
||||||
|
{ icon: 'ShieldCheck', title: 'Garantia Vitalícia', description: 'Garantia em serviços selecionados.' },
|
||||||
|
{ icon: 'Award', title: 'Equipe Certificada', description: 'Mecânicos treinados nas melhores montadoras.' },
|
||||||
|
{ icon: 'Clock', title: 'Entrega Pontual', description: 'Respeitamos seu tempo rigorosamente.' },
|
||||||
|
{ icon: 'Wrench', title: 'Peças Originais', description: 'Utilizamos apenas peças de reposição de qualidade.' },
|
||||||
|
{ icon: 'Users', title: 'Atendimento Premium', description: 'Sala de espera VIP e café enquanto aguarda.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SERVICES: Service[] = [
|
||||||
|
{ id: 1, title: 'Troca de Óleo', description: 'Lubrificantes de alta performance para maior durabilidade.', image: 'https://images.unsplash.com/photo-1487754180451-c456f719a1fc?auto=format&fit=crop&q=80&w=600' },
|
||||||
|
{ id: 2, title: 'Freios e ABS', description: 'Diagnóstico completo e troca de pastilhas e discos.', image: 'https://images.unsplash.com/photo-1486262715619-67b85e0b08d3?auto=format&fit=crop&q=80&w=600' },
|
||||||
|
{ id: 3, title: 'Motor e Câmbio', description: 'Reparos complexos, retífica e manutenção preventiva.', image: 'https://images.unsplash.com/photo-1619642751034-765dfdf7c58e?auto=format&fit=crop&q=80&w=600' },
|
||||||
|
{ id: 4, title: 'Ar-condicionado', description: 'Higienização, carga de gás e reparo de compressores.', image: 'https://images.unsplash.com/photo-1504222490245-4366323443c8?auto=format&fit=crop&q=80&w=600' },
|
||||||
|
{ id: 5, title: 'Bateria e Elétrica', description: 'Check-up elétrico completo e troca de baterias.', image: 'https://images.unsplash.com/photo-1503376763036-066120622c74?auto=format&fit=crop&q=80&w=600' },
|
||||||
|
{ id: 6, title: 'Suspensão e Pneus', description: 'Alinhamento 3D, balanceamento e amortecedores.', image: 'https://images.unsplash.com/photo-1580273916550-e323be2ed532?auto=format&fit=crop&q=80&w=600' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PACKAGES: Package[] = [
|
||||||
|
{
|
||||||
|
name: 'Básico',
|
||||||
|
price: 170,
|
||||||
|
features: ['Troca de Óleo (até 4L)', 'Filtro de Óleo', 'Check-up de 15 itens', 'Lavagem Externa Simples'],
|
||||||
|
recommended: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Padrão',
|
||||||
|
price: 250,
|
||||||
|
features: ['Tudo do Básico', 'Filtro de Ar', 'Alinhamento e Balanceamento', 'Rodízio de Pneus', 'Higienização de Ar'],
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Premium',
|
||||||
|
price: 350,
|
||||||
|
features: ['Tudo do Padrão', 'Cristalização de Vidros', 'Polimento de Faróis', 'Check-up Completo (50 itens)', 'Leva e Traz (Raio 10km)'],
|
||||||
|
recommended: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SPECIAL_OFFERS: SpecialOffer[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Kit 4 Pneus Aro 13 Westlake Zuper Eco Z-108 175/70R13 82T',
|
||||||
|
image: 'https://images.unsplash.com/photo-1578844251758-2f71da64522f?auto=format&fit=crop&q=80&w=400',
|
||||||
|
price: 'R$ 899,90',
|
||||||
|
installment: '12x de R$ 88,22 sem juros',
|
||||||
|
rating: 4.8,
|
||||||
|
reviews: 262,
|
||||||
|
specs: { fuel: 'E', grip: 'E', noise: '70dB' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Kit 4 Pneus Aro 14 Pirelli Cinturato P1 175/65R14 82T',
|
||||||
|
image: 'https://images.unsplash.com/photo-1578844251758-2f71da64522f?auto=format&fit=crop&q=80&w=400',
|
||||||
|
price: 'R$ 1.199,90',
|
||||||
|
installment: '12x de R$ 117,63 sem juros',
|
||||||
|
rating: 4.9,
|
||||||
|
reviews: 415,
|
||||||
|
specs: { fuel: 'C', grip: 'C', noise: '69dB' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Kit 4 Pneus Aro 15 Michelin Primacy 4 195/60R15 88V',
|
||||||
|
image: 'https://images.unsplash.com/photo-1578844251758-2f71da64522f?auto=format&fit=crop&q=80&w=400',
|
||||||
|
price: 'R$ 2.450,00',
|
||||||
|
installment: '12x de R$ 240,19 sem juros',
|
||||||
|
rating: 5.0,
|
||||||
|
reviews: 189,
|
||||||
|
specs: { fuel: 'B', grip: 'A', noise: '68dB' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Kit 4 Pneus Aro 16 Bridgestone Turanza 205/55R16 91V',
|
||||||
|
image: 'https://images.unsplash.com/photo-1578844251758-2f71da64522f?auto=format&fit=crop&q=80&w=400',
|
||||||
|
price: 'R$ 2.890,90',
|
||||||
|
installment: '12x de R$ 283,42 sem juros',
|
||||||
|
rating: 4.7,
|
||||||
|
reviews: 310,
|
||||||
|
specs: { fuel: 'E', grip: 'C', noise: '71dB' }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const GALLERY_IMAGES = [
|
||||||
|
'https://images.unsplash.com/photo-1492144534655-ae79c964c9d7?auto=format&fit=crop&q=80&w=500',
|
||||||
|
'https://images.unsplash.com/photo-1597505294881-133d26a1b212?auto=format&fit=crop&q=80&w=500',
|
||||||
|
'https://images.unsplash.com/photo-1615906655593-ad0386982a0f?auto=format&fit=crop&q=80&w=500',
|
||||||
|
'https://images.unsplash.com/photo-1494976388531-d1058494cdd8?auto=format&fit=crop&q=80&w=500',
|
||||||
|
'https://images.unsplash.com/photo-1530046339160-71153320c0f6?auto=format&fit=crop&q=80&w=500',
|
||||||
|
'https://images.unsplash.com/photo-1517524008697-84bbe3c3fd98?auto=format&fit=crop&q=80&w=500',
|
||||||
|
'https://images.unsplash.com/photo-1583121274602-3e2820c69888?auto=format&fit=crop&q=80&w=500',
|
||||||
|
'https://images.unsplash.com/photo-1517026575992-5e117c86699f?auto=format&fit=crop&q=80&w=500',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BEFORE_AFTER_ITEMS: BeforeAfterItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Martelinho de Ouro",
|
||||||
|
imageBefore: "https://images.unsplash.com/photo-1626856557876-2e8c14040956?auto=format&fit=crop&q=80&w=800",
|
||||||
|
imageAfter: "https://images.unsplash.com/photo-1626856557833-255017006841?auto=format&fit=crop&q=80&w=800"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Polimento Cristalizado",
|
||||||
|
imageBefore: "https://images.unsplash.com/photo-1601362840469-51e4d8d58785?auto=format&fit=crop&q=80&w=800",
|
||||||
|
imageAfter: "https://images.unsplash.com/photo-1601362840464-5ea2b733b82d?auto=format&fit=crop&q=80&w=800"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const STATS: Stat[] = [
|
||||||
|
{ label: 'Anos de Experiência', value: '20+' },
|
||||||
|
{ label: 'Carros Reparados', value: '3500+' },
|
||||||
|
{ label: 'Técnicos Especialistas', value: '15+' },
|
||||||
|
{ label: 'Clientes Satisfeitos', value: '3000+' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TESTIMONIALS: Testimonial[] = [
|
||||||
|
{ name: 'Carlos Mendes', role: 'Empresário', stars: 5, text: 'O atendimento foi impecável. Resolveram o problema do meu carro que outras três oficinas não conseguiram.', image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&q=80&w=100' },
|
||||||
|
{ name: 'Fernanda Lima', role: 'Médica', stars: 5, text: 'Rápido, limpo e transparente. Adorei receber as fotos das peças trocadas pelo WhatsApp.', image: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=100' },
|
||||||
|
{ name: 'Roberto Silva', role: 'Motorista Uber', stars: 5, text: 'Melhor custo-benefício da região. O pacote de revisão padrão salvou meu orçamento.', image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&q=80&w=100' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TEAM: TeamMember[] = [
|
||||||
|
{ name: 'João Ferreira', role: 'Engenheiro Mecânico Chefe', image: 'https://images.unsplash.com/photo-1560250097-0b93528c311a?auto=format&fit=crop&q=80&w=300' },
|
||||||
|
{ name: 'Ana Souza', role: 'Especialista em Elétrica', image: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=300' },
|
||||||
|
{ name: 'Marcos Oliveira', role: 'Técnico de Suspensão', image: 'https://images.unsplash.com/photo-1530268729831-4b0b9e170218?auto=format&fit=crop&q=80&w=300' },
|
||||||
|
{ name: 'Ricardo Santos', role: 'Gerente de Oficina', image: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=300' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FAQS: FaqItem[] = [
|
||||||
|
{ question: 'Quanto tempo leva uma revisão básica?', answer: 'Geralmente, uma revisão básica (troca de óleo e filtros) é realizada em cerca de 45 a 60 minutos.' },
|
||||||
|
{ question: 'Vocês trabalham com todas as seguradoras?', answer: 'Sim, atendemos as principais seguradoras do mercado para reparos de funilaria e pintura.' },
|
||||||
|
{ question: 'Oferecem garantia nos serviços?', answer: 'Todos os nossos serviços possuem garantia mínima de 3 meses, e algumas peças têm garantia vitalícia do fabricante.' },
|
||||||
|
{ question: 'Posso agendar online?', answer: 'Com certeza! Utilize o botão "Agendar Agora" no topo do site para escolher o melhor horário.' },
|
||||||
|
{ question: 'Aceitam quais formas de pagamento?', answer: 'Aceitamos cartões de crédito em até 10x, débito, PIX e dinheiro.' },
|
||||||
|
{ question: 'Buscam o carro em casa?', answer: 'Sim, possuímos serviço de Leva e Traz em um raio de 10km da oficina.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BLOG_POSTS: BlogPost[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Sinais que seu freio precisa de atenção',
|
||||||
|
excerpt: 'Chiados, pedal baixo ou vibrações? Saiba identificar os problemas antes que seja tarde.',
|
||||||
|
content: `
|
||||||
|
<p>O sistema de freios é, sem dúvida, o componente de segurança mais crítico do seu veículo. Ignorar os primeiros sinais de desgaste pode levar a reparos caros e, pior, a situações perigosas na estrada. Aqui estão os principais sinais de que seus freios precisam de uma visita à oficina:</p>
|
||||||
|
|
||||||
|
<h3>1. Ruídos Estranhos</h3>
|
||||||
|
<p>Se você ouvir um guincho agudo ao frear, é provável que o indicador de desgaste das pastilhas esteja tocando o disco. Já um som de moagem metálica indica que as pastilhas acabaram totalmente e o metal está raspando no metal - uma emergência imediata.</p>
|
||||||
|
|
||||||
|
<h3>2. Vibração no Pedal ou Volante</h3>
|
||||||
|
<p>Sentir o pedal do freio ou o volante tremer ao frear geralmente indica discos de freio empenados. Isso ocorre devido ao superaquecimento ou desgaste irregular.</p>
|
||||||
|
|
||||||
|
<h3>3. Pedal "Baixo" ou "Mole"</h3>
|
||||||
|
<p>Se você precisa pisar até o fundo para o carro parar, pode haver ar no sistema hidráulico, vazamento de fluido ou desgaste excessivo das pastilhas.</p>
|
||||||
|
|
||||||
|
<p>Na CAR Auto Center, realizamos uma inspeção completa do sistema de freios gratuitamente. Agende sua revisão hoje mesmo!</p>
|
||||||
|
`,
|
||||||
|
image: 'https://images.unsplash.com/photo-1486262715619-67b85e0b08d3?auto=format&fit=crop&q=80&w=800',
|
||||||
|
date: '12 Out 2023'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Como economizar combustível na cidade',
|
||||||
|
excerpt: 'Dicas práticas de direção e manutenção que podem reduzir seu consumo em até 20%.',
|
||||||
|
content: `
|
||||||
|
<p>Com o preço do combustível em alta, cada quilômetro conta. Felizmente, pequenas mudanças nos hábitos de direção e na manutenção do veículo podem gerar uma economia significativa no final do mês.</p>
|
||||||
|
|
||||||
|
<h3>Mantenha os Pneus Calibrados</h3>
|
||||||
|
<p>Pneus murchos aumentam a resistência à rolagem, forçando o motor a trabalhar mais. Verifique a calibração semanalmente.</p>
|
||||||
|
|
||||||
|
<h3>Evite Acelerações Bruscas</h3>
|
||||||
|
<p>Sair do sinal verde acelerando fundo consome muito mais combustível do que uma aceleração gradual. Tente manter uma velocidade constante sempre que possível.</p>
|
||||||
|
|
||||||
|
<h3>Troque as Marchas no Tempo Certo</h3>
|
||||||
|
<p>Esticar demais as marchas eleva o giro do motor desnecessariamente. Consulte o manual do proprietário para saber a rotação ideal de troca.</p>
|
||||||
|
|
||||||
|
<h3>Manutenção em Dia</h3>
|
||||||
|
<p>Velas desgastadas, filtros de ar sujos e óleo velho podem aumentar o consumo em até 10%. Mantenha suas revisões em dia com a CAR Auto Center.</p>
|
||||||
|
`,
|
||||||
|
image: 'https://images.unsplash.com/photo-1616422285623-13ff0162193c?auto=format&fit=crop&q=80&w=800',
|
||||||
|
date: '05 Nov 2023'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'A importância da troca de óleo regular',
|
||||||
|
excerpt: 'Entenda por que o óleo é o sangue do motor e o que acontece se você atrasar a troca.',
|
||||||
|
content: `
|
||||||
|
<p>O óleo do motor tem funções vitais: lubrificar, limpar, resfriar e vedar as partes internas do motor. Com o tempo, ele perde suas propriedades e acumula impurezas.</p>
|
||||||
|
|
||||||
|
<h3>O que acontece se não trocar?</h3>
|
||||||
|
<p>O óleo velho se transforma em uma borra espessa que entope os canais de lubrificação. Isso causa atrito excessivo, superaquecimento e pode levar à fundição do motor.</p>
|
||||||
|
|
||||||
|
<h3>Sintético vs. Mineral</h3>
|
||||||
|
<p>Sempre respeite a especificação da montadora. Óleos sintéticos tendem a ter maior durabilidade e oferecer melhor proteção em altas temperaturas.</p>
|
||||||
|
|
||||||
|
<p>Não arrisque o coração do seu carro. A troca de óleo é o serviço de manutenção preventiva mais barato e importante que você pode fazer.</p>
|
||||||
|
`,
|
||||||
|
image: 'https://images.unsplash.com/photo-1487754180451-c456f719a1fc?auto=format&fit=crop&q=80&w=800',
|
||||||
|
date: '20 Nov 2023'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const WHY_CHOOSE_ITEMS: WhyChooseItem[] = [
|
||||||
|
{ icon: 'Settings', text: 'Equipamentos de diagnóstico de última geração' },
|
||||||
|
{ icon: 'ShieldCheck', text: 'Certificação ISO 9001 de Qualidade' },
|
||||||
|
{ icon: 'Users', text: 'Transparência total com o cliente' },
|
||||||
|
{ icon: 'Clock', text: 'Atendimento 24h para emergências' },
|
||||||
|
{ icon: 'Droplet', text: 'Descarte ecológico de fluidos e peças' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CLIENT_LOGOS: ClientLogo[] = [
|
||||||
|
{ name: 'LEAR', url: '' },
|
||||||
|
{ name: 'SMART', url: '' },
|
||||||
|
{ name: 'BOSCH', url: '' },
|
||||||
|
{ name: 'PIRELLI', url: '' },
|
||||||
|
{ name: 'MOTUL', url: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS: GlobalSettings = {
|
||||||
|
siteName: 'CAR Auto Center',
|
||||||
|
logoUrl: '', // Se vazio, usa texto
|
||||||
|
faviconUrl: '',
|
||||||
|
primaryColor: '#FF6200',
|
||||||
|
whatsappNumber: '5511999999999',
|
||||||
|
contactEmail: 'contato@carautocenter.com.br',
|
||||||
|
contactPhoneDisplay: '(11) 99999-9999',
|
||||||
|
address: 'Av. Paulista, 1000 - Bela Vista, São Paulo - SP',
|
||||||
|
social: {
|
||||||
|
instagram: '#',
|
||||||
|
facebook: '#',
|
||||||
|
twitter: '#',
|
||||||
|
youtube: '',
|
||||||
|
linkedin: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aqui fazemos a injeção do HTML no título inicial para colorir "Automotivos"
|
||||||
|
const INITIAL_HERO_TITLE = "Manutenção e Reparos <span class='text-primary'>Automotivos</span>";
|
||||||
|
|
||||||
|
export const DEFAULT_VISIBILITY: SectionVisibility = {
|
||||||
|
hero: true,
|
||||||
|
specialOffers: true,
|
||||||
|
about: true,
|
||||||
|
services: true,
|
||||||
|
bigCta: true,
|
||||||
|
packages: true,
|
||||||
|
gallery: true,
|
||||||
|
beforeAfter: true, // Nova visibilidade padrão
|
||||||
|
stats: true,
|
||||||
|
whyChoose: true,
|
||||||
|
testimonials: true,
|
||||||
|
team: true,
|
||||||
|
faq: true,
|
||||||
|
blog: true,
|
||||||
|
contact: true,
|
||||||
|
clients: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_TEXTS: SectionTexts = {
|
||||||
|
hero: {
|
||||||
|
feature1: "Oficina Aberta Agora",
|
||||||
|
feature2: "Atendemos Blindados",
|
||||||
|
feature3: "Importados & Nacionais"
|
||||||
|
},
|
||||||
|
specialOffers: {
|
||||||
|
badge: "Super Ofertas",
|
||||||
|
title: "Promoção de Pneus",
|
||||||
|
subtitle: "Estoque limitado. Garanta o seu!"
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
subtitle: "Sobre Nós",
|
||||||
|
title: "O Que Prometemos Para Você",
|
||||||
|
badgeYear: "20+",
|
||||||
|
badgeText: "Anos de Experiência no Mercado"
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
subtitle: "O Que Fazemos",
|
||||||
|
title: "Nossos Serviços"
|
||||||
|
},
|
||||||
|
packages: {
|
||||||
|
subtitle: "Preços Claros",
|
||||||
|
title: "Pacotes de Revisão"
|
||||||
|
},
|
||||||
|
gallery: {
|
||||||
|
subtitle: "Nosso Trabalho",
|
||||||
|
title: "Galeria de Fotos"
|
||||||
|
},
|
||||||
|
beforeAfter: {
|
||||||
|
subtitle: "Resultados Reais",
|
||||||
|
title: "Antes e Depois"
|
||||||
|
},
|
||||||
|
whyChoose: {
|
||||||
|
subtitle: "Diferenciais",
|
||||||
|
title: "Por Que Escolher a CAR?"
|
||||||
|
},
|
||||||
|
testimonials: {
|
||||||
|
subtitle: "Avaliações",
|
||||||
|
title: "O Que Dizem Nossos Clientes"
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
subtitle: "Especialistas",
|
||||||
|
title: "Nossa Equipe"
|
||||||
|
},
|
||||||
|
faq: {
|
||||||
|
subtitle: "Dúvidas",
|
||||||
|
title: "Perguntas Frequentes"
|
||||||
|
},
|
||||||
|
blog: {
|
||||||
|
subtitle: "Notícias",
|
||||||
|
title: "Dicas e Artigos Recentes"
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
subtitle: "Fale Conosco",
|
||||||
|
title: "Entre em Contato",
|
||||||
|
description: "Preencha o formulário para agendamento ou dúvidas. Nossa equipe responderá em até 30 minutos."
|
||||||
|
},
|
||||||
|
bigCta: {
|
||||||
|
title: "Precisa de Ajuda Imediata?",
|
||||||
|
subtitle: "Atendimento de emergência e guincho 24 horas. Clique para chamar.",
|
||||||
|
buttonText: "24/7"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_HEADER: HeaderConfig = {
|
||||||
|
items: NAV_ITEMS,
|
||||||
|
ctaButton: {
|
||||||
|
text: "Agendar Agora",
|
||||||
|
url: "",
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_FOOTER: FooterConfig = {
|
||||||
|
description: "Sua oficina de confiança para manutenção e reparos automotivos de alta performance.",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: "col_services",
|
||||||
|
title: "Serviços",
|
||||||
|
type: "services_dynamic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "col_links",
|
||||||
|
title: "Links Rápidos",
|
||||||
|
type: "custom",
|
||||||
|
links: [
|
||||||
|
{ label: "Sobre Nós", href: "/sobre" },
|
||||||
|
{ label: "Pacotes & Promoções", href: "/promocoes" },
|
||||||
|
{ label: "Blog", href: "/blog" },
|
||||||
|
{ label: "Contato", href: "/contato" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "col_hours",
|
||||||
|
title: "Horário",
|
||||||
|
type: "hours"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
bottomLinks: [
|
||||||
|
{ label: "Política de Privacidade", href: "#" },
|
||||||
|
{ label: "Termos de Uso", href: "#" }
|
||||||
|
]
|
||||||
|
};
|
||||||
56
index.html
Normal file
56
index.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR" class="scroll-smooth">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>CAR Auto Center - Manutenção e Reparos</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;900&display=swap" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: 'var(--primary-color)',
|
||||||
|
secondary: '#1A1A1A',
|
||||||
|
dark: '#121212',
|
||||||
|
light: '#F4F4F5'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #FF6200;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #09090b; /* Zinc 950 */
|
||||||
|
color: #f4f4f5;
|
||||||
|
}
|
||||||
|
.text-glow {
|
||||||
|
text-shadow: 0 0 20px rgba(var(--primary-color), 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"react/": "https://esm.sh/react@^19.2.4/",
|
||||||
|
"react": "https://esm.sh/react@^19.2.4",
|
||||||
|
"react-dom/": "https://esm.sh/react-dom@^19.2.4/",
|
||||||
|
"clsx": "https://esm.sh/clsx@^2.1.1",
|
||||||
|
"lucide-react": "https://esm.sh/lucide-react@^0.564.0",
|
||||||
|
"tailwind-merge": "https://esm.sh/tailwind-merge@^3.4.1",
|
||||||
|
"react-router-dom": "https://esm.sh/react-router-dom@^7.13.0",
|
||||||
|
"@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@^2.95.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
index.tsx
Normal file
21
index.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
import App from './App';
|
||||||
|
import { DataProvider } from './contexts/DataContext';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
if (!rootElement) {
|
||||||
|
throw new Error("Could not find root element to mount to");
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<HashRouter>
|
||||||
|
<DataProvider>
|
||||||
|
<App />
|
||||||
|
</DataProvider>
|
||||||
|
</HashRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
18
lib/supabase.ts
Normal file
18
lib/supabase.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
// NOTA: Em produção, estas variáveis devem estar no .env
|
||||||
|
// Para teste imediato, se as variáveis não existirem, o sistema usará localStorage
|
||||||
|
|
||||||
|
// Safely access environment variables
|
||||||
|
const envUrl = (import.meta as any).env?.VITE_SUPABASE_URL;
|
||||||
|
const envKey = (import.meta as any).env?.VITE_SUPABASE_ANON_KEY;
|
||||||
|
|
||||||
|
// Fallback to placeholder values to prevent createClient from throwing "supabaseUrl is required" error
|
||||||
|
const supabaseUrl = envUrl || 'https://placeholder.supabase.co';
|
||||||
|
const supabaseAnonKey = envKey || 'placeholder';
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||||
|
|
||||||
|
export const isSupabaseConfigured = () => {
|
||||||
|
return !!envUrl && !!envKey && envUrl !== '' && envKey !== '';
|
||||||
|
};
|
||||||
5
metadata.json
Normal file
5
metadata.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "CAR Auto Center",
|
||||||
|
"description": "Centro automotivo completo com agendamento online e serviços 24h.",
|
||||||
|
"requestFramePermissions": []
|
||||||
|
}
|
||||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "car-auto-center",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.564.0",
|
||||||
|
"tailwind-merge": "^3.4.1",
|
||||||
|
"react-router-dom": "^7.13.0",
|
||||||
|
"@supabase/supabase-js": "^2.95.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.14.0",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"typescript": "~5.8.2",
|
||||||
|
"vite": "^6.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
223
types.ts
Normal file
223
types.ts
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
export interface NavItem {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Service {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromiseItem {
|
||||||
|
icon: string; // Changed from LucideIcon to string for JSON storage
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Package {
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
features: string[];
|
||||||
|
recommended?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Stat {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Testimonial {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
image: string;
|
||||||
|
text: string;
|
||||||
|
stars: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamMember {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FaqItem {
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogPost {
|
||||||
|
id: number; // Adicionado ID para roteamento
|
||||||
|
title: string;
|
||||||
|
image: string;
|
||||||
|
excerpt: string;
|
||||||
|
content: string; // Adicionado conteúdo completo
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpecialOffer {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
image: string;
|
||||||
|
price: string;
|
||||||
|
installment: string;
|
||||||
|
rating: number;
|
||||||
|
reviews: number;
|
||||||
|
specs: {
|
||||||
|
fuel: string;
|
||||||
|
grip: string;
|
||||||
|
noise: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientLogo {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WhyChooseItem {
|
||||||
|
icon: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Novo Tipo para Antes e Depois
|
||||||
|
export interface BeforeAfterItem {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
imageBefore: string;
|
||||||
|
imageAfter: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GlobalSettings {
|
||||||
|
siteName: string;
|
||||||
|
logoUrl: string; // URL for the logo image
|
||||||
|
faviconUrl: string;
|
||||||
|
primaryColor: string;
|
||||||
|
whatsappNumber: string; // Only numbers, e.g., 5511999999999
|
||||||
|
contactEmail: string; // For form submissions
|
||||||
|
contactPhoneDisplay: string; // Formatted phone for display
|
||||||
|
address: string;
|
||||||
|
social: {
|
||||||
|
instagram: string;
|
||||||
|
facebook: string;
|
||||||
|
twitter: string;
|
||||||
|
youtube: string;
|
||||||
|
linkedin: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SectionVisibility {
|
||||||
|
hero: boolean;
|
||||||
|
specialOffers: boolean;
|
||||||
|
about: boolean;
|
||||||
|
services: boolean;
|
||||||
|
bigCta: boolean;
|
||||||
|
packages: boolean;
|
||||||
|
gallery: boolean;
|
||||||
|
beforeAfter: boolean; // Nova seção
|
||||||
|
stats: boolean;
|
||||||
|
whyChoose: boolean;
|
||||||
|
testimonials: boolean;
|
||||||
|
team: boolean;
|
||||||
|
faq: boolean;
|
||||||
|
blog: boolean;
|
||||||
|
contact: boolean;
|
||||||
|
clients: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Novos tipos para textos editáveis
|
||||||
|
export interface SectionTexts {
|
||||||
|
hero: {
|
||||||
|
feature1: string;
|
||||||
|
feature2: string;
|
||||||
|
feature3: string;
|
||||||
|
};
|
||||||
|
specialOffers: {
|
||||||
|
badge: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
};
|
||||||
|
about: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
badgeYear: string;
|
||||||
|
badgeText: string;
|
||||||
|
};
|
||||||
|
services: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
packages: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
gallery: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
// Nova seção de textos
|
||||||
|
beforeAfter: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
whyChoose: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
testimonials: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
team: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
faq: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
blog: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
contact: {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
bigCta: {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
buttonText: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuração do Header
|
||||||
|
export interface HeaderConfig {
|
||||||
|
items: NavItem[];
|
||||||
|
ctaButton: {
|
||||||
|
text: string;
|
||||||
|
url: string; // Se vazio, usa a lógica padrão do WhatsApp
|
||||||
|
show: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuração do Footer
|
||||||
|
export interface FooterLink {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FooterColumn {
|
||||||
|
id: string; // Identificador único para ajudar na renderização/edição
|
||||||
|
title: string;
|
||||||
|
type: 'custom' | 'services_dynamic' | 'hours'; // Tipo de coluna
|
||||||
|
links?: FooterLink[]; // Apenas usado se type === 'custom'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FooterConfig {
|
||||||
|
description: string;
|
||||||
|
columns: FooterColumn[];
|
||||||
|
bottomLinks: FooterLink[]; // Links de privacidade, termos, etc.
|
||||||
|
}
|
||||||
23
vite.config.ts
Normal file
23
vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, '.', '');
|
||||||
|
return {
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
},
|
||||||
|
plugins: [react()],
|
||||||
|
define: {
|
||||||
|
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||||
|
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, '.'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user