diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -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? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..0070ad0 --- /dev/null +++ b/App.tsx @@ -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 ( +
+
+
+ +
+
+ ); +}; + +// Proteção de rota simples +const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const isAuth = localStorage.getItem('admin_auth') === 'true'; + return isAuth ? <>{children} : ; +}; + +function App() { + return ( + + {/* Rotas Públicas */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Rotas Administrativas */} + } /> + + + + } + /> + + {/* Rota Catch-all para redirecionar para Home se nada corresponder */} + } /> + + ); +} + +export default App; \ No newline at end of file diff --git a/README.md b/README.md index 2241000..35f6d97 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@
- GHBanner - -

Built with AI Studio

- -

The fastest path from prompt to production with Gemini.

- - Start building -
+ +# 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` diff --git a/components/Admin/Dashboard.tsx b/components/Admin/Dashboard.tsx new file mode 100644 index 0000000..638fcc7 --- /dev/null +++ b/components/Admin/Dashboard.tsx @@ -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) => ( + +); + +const InputGroup = ({ label, value, onChange, type = "text", textarea = false, hint }: any) => ( +
+
+ + {hint && {hint}} +
+ {textarea ? ( + + + +
+ + + ); +}; + +export const ClientsSection = () => { + const { data } = useData(); + return ( +
+ +

Empresas que confiam na CAR

+
+ {data.clients.map((client, i) => ( + client.url ? ( + {client.name} + ) : ( + {client.name} + ) + ))} +
+
+
+ ); +}; + +// 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 ( +
+

{column.title}

+ +
+ ); + } + + if (column.type === 'hours') { + return ( +
+

{column.title}

+ +
+ ); + } + + if (column.type === 'custom' && column.links) { + return ( +
+

{column.title}

+ +
+ ); + } + + return null; +} + +export const Footer = () => { + const { data } = useData(); + return ( + + ); +}; \ No newline at end of file diff --git a/components/Header.tsx b/components/Header.tsx new file mode 100644 index 0000000..52d2d24 --- /dev/null +++ b/components/Header.tsx @@ -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 ( +
+ +
+ + {data.settings.logoUrl ? ( + {data.settings.siteName} + ) : ( +
+ {data.settings.siteName}. +
+ )} + + + {/* Desktop Nav */} + + +
+
+ + {data.settings.contactPhoneDisplay} +
+ {data.header.ctaButton.show && ( + + )} +
+ + {/* Mobile Toggle */} + +
+ + {/* Mobile Menu */} + {isMobileMenuOpen && ( +
+ {data.header.items.map((item, index) => ( + setIsMobileMenuOpen(false)} + > + {item.label} + + ))} +
+ {data.header.ctaButton.show && ( + + )} +
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/components/Hero.tsx b/components/Hero.tsx new file mode 100644 index 0000000..fbf5298 --- /dev/null +++ b/components/Hero.tsx @@ -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 ( +
+ {/* Background Image with Overlay */} +
+ Oficina Mecânica +
+
+ + +
+ {/* Badge atualizado com border-primary sólido */} +
+ + Centro Automotivo Premium +
+ +

+ ') }} /> +

+ +

+ {hero.subtitle} +

+ +
+ + + + +
+ +
+
+ + {texts.hero.feature1} +
+
|
+
{texts.hero.feature2}
+
|
+
{texts.hero.feature3}
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/components/Pages.tsx b/components/Pages.tsx new file mode 100644 index 0000000..05dca7c --- /dev/null +++ b/components/Pages.tsx @@ -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 ( + <> + + {visibility.hero && } + {visibility.specialOffers && } + {visibility.about && } + {visibility.services && } + {visibility.beforeAfter && } + {visibility.bigCta && } + {visibility.packages && } + {visibility.gallery && } + {visibility.stats && } + {visibility.whyChoose && } + {visibility.testimonials && } + {visibility.team && } + {visibility.faq && } + {visibility.blog && } + {visibility.contact && } + {visibility.clients && } + + ); +}; + +export const AboutPage = () => ( + <> + +
+ + + + + +
+ +); + +export const PromotionsPage = () => ( + <> + +
+
+ +

Promoções & Ofertas

+

Confira nossas condições especiais para pneus e revisões.

+
+
+ + + +
+ +); + +export const ServicesPage = () => ( + <> + +
+ + + + +
+ +); + +export const BlogPage = () => ( + <> + +
+
+ +

Blog Automotivo

+

Dicas, manutenção e novidades do mundo automotivo.

+
+
+ +
+ +); + +export const BlogPostPage = () => { + const { id } = useParams(); + const { data } = useData(); + const post = data.blog.find(p => p.id === Number(id)); + + if (!post) { + return ( +
+

Artigo não encontrado

+ +
+ ) + } + + return ( + <> + +
+ + + Voltar para o Blog + + +
+ {post.title} +
+ +
+ + {post.date} +
+ +

+ {post.title} +

+ +
+ +
+

Gostou da dica?

+ +
+ +
+ + ) +} + +export const ContactPage = () => ( + <> + +
+ + +
+ +); \ No newline at end of file diff --git a/components/Shared.tsx b/components/Shared.tsx new file mode 100644 index 0000000..b5b4378 --- /dev/null +++ b/components/Shared.tsx @@ -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 { + variant?: 'primary' | 'outline' | 'white' | 'ghost'; + size?: 'sm' | 'md' | 'lg'; +} + +export const Button: React.FC = ({ + 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 ( + + ); +}; + +export const SectionTitle: React.FC<{ subtitle: string; title: string; center?: boolean; light?: boolean }> = ({ + subtitle, + title, + center = true, + light = true +}) => { + return ( +
+ + {subtitle} + +

+ {title} +

+
+
+ ); +}; + +export const Container: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className }) => ( +
+ {children} +
+); \ No newline at end of file diff --git a/contexts/DataContext.tsx b/contexts/DataContext.tsx new file mode 100644 index 0000000..0781e21 --- /dev/null +++ b/contexts/DataContext.tsx @@ -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 Automotivos", + 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; + isLoading: boolean; + getWhatsAppLink: (message?: string) => string; +} + +const DataContext = createContext(undefined); + +export const DataProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [data, setData] = useState(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 ( + + {children} + + ); +}; + +export const useData = () => { + const context = useContext(DataContext); + if (context === undefined) { + throw new Error('useData must be used within a DataProvider'); + } + return context; +}; \ No newline at end of file diff --git a/data.ts b/data.ts new file mode 100644 index 0000000..fa7d134 --- /dev/null +++ b/data.ts @@ -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: ` +

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:

+ +

1. Ruídos Estranhos

+

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.

+ +

2. Vibração no Pedal ou Volante

+

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.

+ +

3. Pedal "Baixo" ou "Mole"

+

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.

+ +

Na CAR Auto Center, realizamos uma inspeção completa do sistema de freios gratuitamente. Agende sua revisão hoje mesmo!

+ `, + 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: ` +

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.

+ +

Mantenha os Pneus Calibrados

+

Pneus murchos aumentam a resistência à rolagem, forçando o motor a trabalhar mais. Verifique a calibração semanalmente.

+ +

Evite Acelerações Bruscas

+

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.

+ +

Troque as Marchas no Tempo Certo

+

Esticar demais as marchas eleva o giro do motor desnecessariamente. Consulte o manual do proprietário para saber a rotação ideal de troca.

+ +

Manutenção em Dia

+

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.

+ `, + 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: ` +

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.

+ +

O que acontece se não trocar?

+

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.

+ +

Sintético vs. Mineral

+

Sempre respeite a especificação da montadora. Óleos sintéticos tendem a ter maior durabilidade e oferecer melhor proteção em altas temperaturas.

+ +

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.

+ `, + 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 Automotivos"; + +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: "#" } + ] +}; \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..005c8ee --- /dev/null +++ b/index.html @@ -0,0 +1,56 @@ + + + + + + CAR Auto Center - Manutenção e Reparos + + + + + + + +
+ + \ No newline at end of file diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..32f1b7f --- /dev/null +++ b/index.tsx @@ -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( + + + + + + + +); \ No newline at end of file diff --git a/lib/supabase.ts b/lib/supabase.ts new file mode 100644 index 0000000..0cd8ae1 --- /dev/null +++ b/lib/supabase.ts @@ -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 !== ''; +}; \ No newline at end of file diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..160f266 --- /dev/null +++ b/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "CAR Auto Center", + "description": "Centro automotivo completo com agendamento online e serviços 24h.", + "requestFramePermissions": [] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..784b948 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c6eed5 --- /dev/null +++ b/tsconfig.json @@ -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 + } +} \ No newline at end of file diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..c2ec800 --- /dev/null +++ b/types.ts @@ -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. +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ee5fb8d --- /dev/null +++ b/vite.config.ts @@ -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, '.'), + } + } + }; +});