P0: mim-api, live conversion paths, and CWV sprint to ~2.56s LCP.
Ship VMID 7811 API (assistance, contact, Stripe checkout), wire donate and forms to API with accessible success UI, static LCP shell with deferred React mount, and perf gate tooling (P0 72/2600ms; world-class 85/2500ms tracked separately). Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+114
-21
@@ -51,7 +51,7 @@ const AIAssistancePortal = lazy(() => import('./components/AIAssistancePortal'))
|
||||
const AdvancedAnalyticsDashboard = lazy(() => import('./components/AdvancedAnalyticsDashboard'))
|
||||
const MobileVolunteerApp = lazy(() => import('./components/MobileVolunteerApp'))
|
||||
const StaffTrainingDashboard = lazy(() => import('./components/StaffTrainingDashboard'))
|
||||
import MimChatAgent from './components/MimChatAgent'
|
||||
const MimChatAgent = lazy(() => import('./components/MimChatAgent'))
|
||||
import { AppPageShell as PageShell } from './components/layout/AppPageShell'
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||
import { LanguageProvider, useLanguage } from './contexts/LanguageContext'
|
||||
@@ -76,6 +76,9 @@ import {
|
||||
WhatWeDoPage,
|
||||
} from './pages/foundation'
|
||||
import { EMAILS, SITE, mailto } from './content/siteContent'
|
||||
import { FormSuccess } from './components/ui/FormSuccess'
|
||||
import { submitContactFromForm } from './lib/submitContactFromForm'
|
||||
import { MimApiError } from './lib/mimApi'
|
||||
|
||||
function RouteFallback({ label = 'Loading…' }: { label?: string }) {
|
||||
return (
|
||||
@@ -298,28 +301,51 @@ function SkipToContent() {
|
||||
/* ===================== Home Page ===================== */
|
||||
/* ===================== Pages ===================== */
|
||||
function VolunteerPage() {
|
||||
const [submitted, setSubmitted] = useState<string | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
|
||||
const onVolunteerSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setSubmitting(true)
|
||||
setFormError(null)
|
||||
try {
|
||||
const res = await submitContactFromForm(e.currentTarget, 'volunteer')
|
||||
setSubmitted(res.message)
|
||||
e.currentTarget.reset()
|
||||
} catch (err) {
|
||||
setFormError(err instanceof MimApiError ? err.message : 'Could not submit. Please email [email protected].')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageShell title="Volunteer" icon={Users} eyebrow="Serve locally">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="card">
|
||||
<div className="font-medium">Sign-up form</div>
|
||||
<form className="mt-4 grid grid-cols-1 gap-4" onSubmit={(e)=>{e.preventDefault(); alert("Thanks! We'll be in touch via email.");}}>
|
||||
{submitted ? (
|
||||
<FormSuccess title="Thank you" message={submitted} onDismiss={() => setSubmitted(null)} />
|
||||
) : (
|
||||
<form className="mt-4 grid grid-cols-1 gap-4" onSubmit={onVolunteerSubmit}>
|
||||
<input type="text" name="website" tabIndex={-1} autoComplete="off" className="sr-only" aria-hidden="true" />
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<input required aria-label="First name" placeholder="First name" className="input" />
|
||||
<input required aria-label="Last name" placeholder="Last name" className="input" />
|
||||
<input required name="firstName" aria-label="First name" placeholder="First name" className="input" />
|
||||
<input required name="lastName" aria-label="Last name" placeholder="Last name" className="input" />
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<input required type="email" aria-label="Email" placeholder="Email" className="input" />
|
||||
<input aria-label="Phone" placeholder="Phone" className="input" />
|
||||
<input required type="email" name="email" aria-label="Email" placeholder="Email" className="input" />
|
||||
<input name="phone" aria-label="Phone" placeholder="Phone" className="input" />
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<select aria-label="Interests" className="input">
|
||||
<select name="interest" aria-label="Interests" className="input">
|
||||
<option>Assemble kits</option>
|
||||
<option>Delivery driver</option>
|
||||
<option>Community partner</option>
|
||||
<option>Admin support</option>
|
||||
</select>
|
||||
<select aria-label="Availability" className="input">
|
||||
<select name="availability" aria-label="Availability" className="input">
|
||||
<option>Weekdays</option>
|
||||
<option>Weeknights</option>
|
||||
<option>Weekends</option>
|
||||
@@ -329,8 +355,12 @@ function VolunteerPage() {
|
||||
<input required type="checkbox" className="mt-1 h-4 w-4" />
|
||||
<span>I have read and agree to the <a className="underline" href="#/legal#volunteer-waiver">Volunteer Waiver & Liability Release</a>, including background check and child-safety requirements.</span>
|
||||
</label>
|
||||
<button className="btn-primary w-full justify-center" type="submit">Submit</button>
|
||||
{formError && <p className="text-sm text-red-600 dark:text-red-400" role="alert">{formError}</p>}
|
||||
<button className="btn-primary w-full justify-center" type="submit" disabled={submitting}>
|
||||
{submitting ? 'Sending…' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
<aside className="space-y-6">
|
||||
<Card title="What to expect" icon={Backpack}>
|
||||
@@ -346,6 +376,25 @@ function VolunteerPage() {
|
||||
}
|
||||
|
||||
function SponsorsPage() {
|
||||
const [submitted, setSubmitted] = useState<string | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
|
||||
const onSponsorSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setSubmitting(true)
|
||||
setFormError(null)
|
||||
try {
|
||||
const res = await submitContactFromForm(e.currentTarget, 'sponsor')
|
||||
setSubmitted(res.message)
|
||||
e.currentTarget.reset()
|
||||
} catch (err) {
|
||||
setFormError(err instanceof MimApiError ? err.message : 'Could not submit. Please email [email protected].')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const tiers = [
|
||||
{ name: "Bronze", amt: "$2,500", perks: ["Logo on website", "Social thank-you", "Quarterly impact note"] },
|
||||
{ name: "Silver", amt: "$5,000", perks: ["All Bronze", "Logo on event signage", "Volunteer day for your team"] },
|
||||
@@ -371,26 +420,34 @@ function SponsorsPage() {
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="font-medium">Start a conversation</div>
|
||||
<form className="mt-4 grid gap-4" onSubmit={(e)=>{e.preventDefault(); alert("Thanks! We'll reach out soon.");}}>
|
||||
<input className="input" placeholder="Company name" aria-label="Company name" required />
|
||||
{submitted ? (
|
||||
<FormSuccess title="Thank you" message={submitted} onDismiss={() => setSubmitted(null)} />
|
||||
) : (
|
||||
<form className="mt-4 grid gap-4" onSubmit={onSponsorSubmit}>
|
||||
<input type="text" name="website" tabIndex={-1} autoComplete="off" className="sr-only" aria-hidden="true" />
|
||||
<input className="input" name="companyName" placeholder="Company name" aria-label="Company name" required />
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<input className="input" placeholder="Contact name" aria-label="Contact name" required />
|
||||
<input type="email" className="input" placeholder="Email" aria-label="Email" required />
|
||||
<input className="input" name="contactName" placeholder="Contact name" aria-label="Contact name" required />
|
||||
<input type="email" name="email" className="input" placeholder="Email" aria-label="Email" required />
|
||||
</div>
|
||||
<select className="input" aria-label="Tier interest">
|
||||
<select className="input" name="tier" aria-label="Tier interest">
|
||||
<option>Bronze</option>
|
||||
<option>Silver</option>
|
||||
<option>Gold</option>
|
||||
<option>Platinum</option>
|
||||
<option>Custom</option>
|
||||
</select>
|
||||
<textarea className="input min-h-28 resize-none" placeholder="Tell us about your goals" aria-label="Message"></textarea>
|
||||
<textarea className="input min-h-28 resize-none" name="message" placeholder="Tell us about your goals" aria-label="Message"></textarea>
|
||||
<label className="flex items-start gap-3 text-xs opacity-80">
|
||||
<input type="checkbox" className="mt-1 h-4 w-4"/>
|
||||
I agree to logo-use guidelines and brand approvals per our <a className="underline" href="#/legal#sponsorship-terms">Sponsorship Terms</a>.
|
||||
</label>
|
||||
<button className="btn-primary w-full justify-center" type="submit">Send</button>
|
||||
{formError && <p className="text-sm text-red-600 dark:text-red-400" role="alert">{formError}</p>}
|
||||
<button className="btn-primary w-full justify-center" type="submit" disabled={submitting}>
|
||||
{submitting ? 'Sending…' : 'Send'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageShell>
|
||||
@@ -398,6 +455,25 @@ function SponsorsPage() {
|
||||
}
|
||||
|
||||
function StoriesPage() {
|
||||
const [submitted, setSubmitted] = useState<string | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
|
||||
const onStorySubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setSubmitting(true)
|
||||
setFormError(null)
|
||||
try {
|
||||
const res = await submitContactFromForm(e.currentTarget, 'story')
|
||||
setSubmitted(res.message)
|
||||
e.currentTarget.reset()
|
||||
} catch (err) {
|
||||
setFormError(err instanceof MimApiError ? err.message : 'Could not submit. Please email [email protected].')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const stories = [
|
||||
{ title: "A family found hope again", tag: "Outreach", body: "After a fire displaced their home, a mother and children received emergency support and compassionate guidance through recovery.", by: "Community partner" },
|
||||
{ title: "Strength after hardship", tag: "Advocacy", body: "A survivor connected with resources and encouragement — reminded that she matters and brighter days are ahead.", by: "Volunteer" },
|
||||
@@ -419,15 +495,24 @@ function StoriesPage() {
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="font-medium">Submit your story</div>
|
||||
<form className="mt-3 grid gap-3" onSubmit={(e)=>{e.preventDefault(); alert("Thanks for sharing! We'll review and publish with consent.");}}>
|
||||
<input className="input" placeholder="Your name (or Anonymous)" aria-label="Name" />
|
||||
<textarea className="input min-h-28 resize-none" placeholder="Your story (please omit private identifying details)" aria-label="Story" required />
|
||||
{submitted ? (
|
||||
<FormSuccess title="Thank you" message={submitted} onDismiss={() => setSubmitted(null)} />
|
||||
) : (
|
||||
<form className="mt-3 grid gap-3" onSubmit={onStorySubmit}>
|
||||
<input type="text" name="website" tabIndex={-1} autoComplete="off" className="sr-only" aria-hidden="true" />
|
||||
<input className="input" name="name" placeholder="Your name (or Anonymous)" aria-label="Name" />
|
||||
<input type="email" className="input" name="email" placeholder="Email (optional, for follow-up)" aria-label="Email" />
|
||||
<textarea className="input min-h-28 resize-none" name="story" placeholder="Your story (please omit private identifying details)" aria-label="Story" required />
|
||||
<label className="flex items-start gap-3 text-xs opacity-80">
|
||||
<input type="checkbox" className="mt-1 h-4 w-4" required />
|
||||
I grant permission to publish this story (edited for length/clarity). I have removed personally identifying information.
|
||||
</label>
|
||||
<button className="btn-primary w-full justify-center">Submit</button>
|
||||
{formError && <p className="text-sm text-red-600 dark:text-red-400" role="alert">{formError}</p>}
|
||||
<button className="btn-primary w-full justify-center" type="submit" disabled={submitting}>
|
||||
{submitting ? 'Sending…' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</PageShell>
|
||||
)
|
||||
@@ -2148,6 +2233,10 @@ function AppContent() {
|
||||
const { isOnline } = usePWA()
|
||||
const { addNotification } = useNotifications()
|
||||
|
||||
useEffect(() => {
|
||||
document.getElementById('mim-static-hero')?.classList.add('is-hidden')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleHashChange = () => setCurrentPath(window.location.hash.slice(1) || '/')
|
||||
window.addEventListener('hashchange', handleHashChange)
|
||||
@@ -2288,7 +2377,11 @@ function AppContent() {
|
||||
'/advanced-analytics',
|
||||
'/mobile-volunteer',
|
||||
'/staff-training'
|
||||
].includes(currentPath) && <MimChatAgent currentPath={currentPath} />}
|
||||
].includes(currentPath) && (
|
||||
<Suspense fallback={null}>
|
||||
<MimChatAgent currentPath={currentPath} />
|
||||
</Suspense>
|
||||
)}
|
||||
<StickyDonate />
|
||||
<CookieBanner />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user