From 4a35e75ad4c1630d9798fe7d0f8a87bb905a624d Mon Sep 17 00:00:00 2001 From: Marcelo Santos Date: Sun, 22 Mar 2026 13:48:16 +0000 Subject: [PATCH 1/2] feat(contact-form): implement contact form component and integrate with services --- src/App.tsx | 15 +- src/components/AboutSite.tsx | 11 +- src/components/ContactForm.tsx | 226 ++++++++++++++++++ src/components/Landing.tsx | 9 +- src/components/LegalPages.tsx | 24 +- src/components/ProjectsPage.tsx | 11 +- src/components/Report.tsx | 10 +- src/services/contactFormService.ts | 13 + src/services/index.ts | 1 + src/types/index.ts | 7 + .../functions/send-contact-form/config.toml | 2 + supabase/functions/send-contact-form/index.ts | 133 +++++++++++ 12 files changed, 446 insertions(+), 16 deletions(-) create mode 100644 src/components/ContactForm.tsx create mode 100644 src/services/contactFormService.ts create mode 100644 supabase/functions/send-contact-form/config.toml create mode 100644 supabase/functions/send-contact-form/index.ts diff --git a/src/App.tsx b/src/App.tsx index b039140..a60dca9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import AboutSite from './components/AboutSite'; import LegalPages from './components/LegalPages'; import ProjectsPage from './components/ProjectsPage'; import CookieConsentBanner from './components/CookieConsentBanner'; +import ContactForm from './components/ContactForm'; import { LeadData, DiagnosisResult } from './types'; import { generateDiagnosis, sendDiagnosticEmail, sendContactConfirmation, saveLead } from './services'; @@ -126,6 +127,9 @@ const App: React.FC = () => { const [error, setError] = useState(null); const [activeLegal, setActiveLegal] = useState('privacy'); const [aboutSection, setAboutSection] = useState('manifesto'); + const [showContactForm, setShowContactForm] = useState(false); + + const openContactForm = () => setShowContactForm(true); useEffect(() => { const route = parseLocationToState(window.location.pathname); @@ -287,16 +291,17 @@ const App: React.FC = () => { )} - {view === AppState.LANDING && } - {view === AppState.ABOUT && transitionTo(AppState.LANDING)} onStartWizard={startWizard} onOpenLegal={handleOpenLegal} onViewProjects={handleViewProjects} initialSection={aboutSection} onSectionNavigate={setAboutSection} />} - {view === AppState.PROJECTS && transitionTo(AppState.ABOUT)} onStartWizard={startWizard} />} + {view === AppState.LANDING && } + {view === AppState.ABOUT && transitionTo(AppState.LANDING)} onStartWizard={startWizard} onOpenLegal={handleOpenLegal} onViewProjects={handleViewProjects} initialSection={aboutSection} onSectionNavigate={setAboutSection} onOpenContactForm={openContactForm} />} + {view === AppState.PROJECTS && transitionTo(AppState.ABOUT)} onStartWizard={startWizard} onOpenContactForm={openContactForm} />} {view === AppState.WIZARD && } {view === AppState.LOADING && } - {view === AppState.REPORT && diagnosis && } - {view === AppState.LEGAL && transitionTo(AppState.ABOUT)} />} + {view === AppState.REPORT && diagnosis && } + {view === AppState.LEGAL && transitionTo(AppState.ABOUT)} onOpenContactForm={openContactForm} />} {view !== AppState.INTRO && handleOpenLegal('cookies')} />} + setShowContactForm(false)} /> ); }; diff --git a/src/components/AboutSite.tsx b/src/components/AboutSite.tsx index dd4d2e9..8b044db 100644 --- a/src/components/AboutSite.tsx +++ b/src/components/AboutSite.tsx @@ -18,6 +18,7 @@ interface AboutSiteProps { onViewProjects: () => void; initialSection?: 'manifesto' | 'impacto' | 'contato'; onSectionNavigate?: (section: 'manifesto' | 'impacto' | 'contato') => void; + onOpenContactForm: () => void; } const SECTIONS = [ @@ -49,7 +50,7 @@ const TEAM_MEMBERS: TeamMember[] = [ } ]; -const AboutSite: React.FC = ({ onBack, onStartWizard, onOpenLegal, onViewProjects, initialSection = 'manifesto', onSectionNavigate }) => { +const AboutSite: React.FC = ({ onBack, onStartWizard, onOpenLegal, onViewProjects, initialSection = 'manifesto', onSectionNavigate, onOpenContactForm }) => { const [activeSection, setActiveSection] = useState('manifesto'); const [selectedMember, setSelectedMember] = useState(null); const [eggActive, setEggActive] = useState(false); @@ -461,6 +462,14 @@ const AboutSite: React.FC = ({ onBack, onStartWizard, onOpenLega Iniciar Projeto rocket_launch + + void; +} + +type Status = 'idle' | 'loading' | 'success' | 'error'; + +const ContactForm: React.FC = ({ isOpen, onClose }) => { + const [status, setStatus] = useState('idle'); + const [errorMsg, setErrorMsg] = useState(''); + const [form, setForm] = useState({ name: '', email: '', message: '', phone: '' }); + const nameRef = useRef(null); + + const resetForm = () => { + setStatus('idle'); + setErrorMsg(''); + setForm({ name: '', email: '', message: '', phone: '' }); + }; + + useEffect(() => { + if (isOpen) { + setTimeout(() => nameRef.current?.focus(), 100); + } else { + const t = setTimeout(resetForm, 300); + return () => clearTimeout(t); + } + }, [isOpen]); + + useEffect(() => { + if (status === 'success') { + const t = setTimeout(onClose, 3500); + return () => clearTimeout(t); + } + }, [status, onClose]); + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) onClose(); + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [isOpen, onClose]); + + const isEmailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email); + const isValid = form.name.trim().length >= 2 && isEmailValid && form.message.trim().length >= 10; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!isValid || status === 'loading') return; + setStatus('loading'); + setErrorMsg(''); + + const payload: ContactFormData = { + name: form.name.trim(), + email: form.email.trim(), + message: form.message.trim(), + }; + if (form.phone.trim()) payload.phone = form.phone.trim(); + + try { + await sendContactFormMessage(payload); + setStatus('success'); + } catch { + setStatus('error'); + setErrorMsg('Algo correu mal. Tenta novamente ou escreve para hello@monynha.com'); + } + }; + + if (!isOpen) return null; + + return ( +
+ {/* Backdrop */} +