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]>
2390 lines
102 KiB
TypeScript
2390 lines
102 KiB
TypeScript
import React, { useState, useEffect, lazy, Suspense } from 'react'
|
||
import { motion, AnimatePresence, MotionConfig, LazyMotion, domAnimation } from 'framer-motion'
|
||
import {
|
||
Backpack,
|
||
Heart,
|
||
MapPin,
|
||
Phone,
|
||
Shirt,
|
||
Users,
|
||
Building2,
|
||
BookOpenText,
|
||
Quote,
|
||
FileText,
|
||
X,
|
||
DollarSign,
|
||
Award,
|
||
Settings,
|
||
UserCheck,
|
||
School,
|
||
ClipboardList,
|
||
Calendar,
|
||
FileCheck,
|
||
AlertCircle,
|
||
Package,
|
||
Truck,
|
||
Plus,
|
||
Lock,
|
||
Database,
|
||
Check,
|
||
Clock,
|
||
Bell,
|
||
BellRing,
|
||
BarChart3,
|
||
TrendingUp,
|
||
Languages,
|
||
Brain,
|
||
Cpu,
|
||
Download,
|
||
WifiOff,
|
||
ChevronDown,
|
||
Eye,
|
||
Zap,
|
||
Target,
|
||
Activity,
|
||
} from 'lucide-react'
|
||
|
||
// Phase 3: AI Components (lazy — portal routes only)
|
||
const AIAssistancePortal = lazy(() => import('./components/AIAssistancePortal'))
|
||
|
||
// Phase 3B: Enterprise Components (lazy — portal routes only)
|
||
const AdvancedAnalyticsDashboard = lazy(() => import('./components/AdvancedAnalyticsDashboard'))
|
||
const MobileVolunteerApp = lazy(() => import('./components/MobileVolunteerApp'))
|
||
const StaffTrainingDashboard = lazy(() => import('./components/StaffTrainingDashboard'))
|
||
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'
|
||
import { NotificationProvider, useNotifications } from './contexts/NotificationContext'
|
||
|
||
// Lazy route chunks — home, donate, assistance (motion-heavy / form pages)
|
||
const HomePage = lazy(() => import('./routes/HomePageRoute'))
|
||
const DonatePage = lazy(() => import('./routes/DonatePageRoute'))
|
||
const AssistanceRequestPage = lazy(() => import('./routes/AssistanceRequestPageRoute'))
|
||
|
||
// Phase 4: Extracted Components
|
||
import { Navigation } from './components/Navigation'
|
||
import { SiteHeader } from './components/SiteHeader'
|
||
import { Footer } from './components/Footer'
|
||
import { BrandAssetsPage } from './pages/BrandPage'
|
||
import {
|
||
AboutPage,
|
||
ContactPage,
|
||
EventsPage,
|
||
MissionPage,
|
||
StoriesIntroBlock,
|
||
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 (
|
||
<div className="mx-auto max-w-7xl px-4 py-24 text-center text-neutral-600 dark:text-neutral-400">
|
||
{label}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function PortalRouteFallback() {
|
||
return (
|
||
<div className="mx-auto max-w-7xl px-4 py-24 text-center text-neutral-600 dark:text-neutral-400">
|
||
Loading portal…
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function LazyPortal({ children }: { children: React.ReactNode }) {
|
||
return <Suspense fallback={<PortalRouteFallback />}>{children}</Suspense>
|
||
}
|
||
|
||
function LazyRoute({ children, label }: { children: React.ReactNode; label?: string }) {
|
||
return <Suspense fallback={<RouteFallback label={label} />}>{children}</Suspense>
|
||
}
|
||
|
||
/**
|
||
* Miracles in Motion — Complete Non-Profit Website
|
||
* A comprehensive 501(c)3 organization website with modern design,
|
||
* donation processing, volunteer management, and impact tracking.
|
||
*/
|
||
|
||
/* ===================== Phase 2: Enhanced Context Systems ===================== */
|
||
|
||
/* ===================== Analytics Tracking ===================== */
|
||
function trackEvent(eventName: string, properties: Record<string, any> = {}) {
|
||
// In production, integrate with Google Analytics, Mixpanel, or similar
|
||
if (typeof window !== 'undefined' && (window as any).gtag) {
|
||
(window as any).gtag('event', eventName, properties)
|
||
}
|
||
console.log(`Analytics: ${eventName}`, properties)
|
||
}
|
||
|
||
function useAnalytics() {
|
||
const [analyticsData, setAnalyticsData] = useState<AnalyticsData>(() => ({
|
||
pageViews: [
|
||
{ page: 'Home', views: 2847, trend: 12.5 },
|
||
{ page: 'Donate', views: 1203, trend: 8.3 },
|
||
{ page: 'Volunteer', views: 856, trend: -2.1 },
|
||
{ page: 'Stories', views: 645, trend: 15.8 },
|
||
{ page: 'About', views: 432, trend: 5.2 }
|
||
],
|
||
donationMetrics: { amount: 45280, count: 186, recurring: 67 },
|
||
userEngagement: { sessions: 3241, avgDuration: 185, bounceRate: 0.34 },
|
||
conversionRates: { donation: 0.078, volunteer: 0.032, contact: 0.156 }
|
||
}))
|
||
|
||
const refreshAnalytics = () => {
|
||
// Simulate real-time data updates
|
||
setAnalyticsData(prev => ({
|
||
...prev,
|
||
pageViews: prev.pageViews.map(pv => ({
|
||
...pv,
|
||
views: pv.views + Math.floor(Math.random() * 10),
|
||
trend: (Math.random() - 0.5) * 20
|
||
})),
|
||
donationMetrics: {
|
||
...prev.donationMetrics,
|
||
amount: prev.donationMetrics.amount + Math.floor(Math.random() * 500),
|
||
count: prev.donationMetrics.count + Math.floor(Math.random() * 3)
|
||
}
|
||
}))
|
||
}
|
||
|
||
useEffect(() => {
|
||
const interval = setInterval(refreshAnalytics, 30000) // Update every 30 seconds
|
||
return () => clearInterval(interval)
|
||
}, [])
|
||
|
||
return { analyticsData, refreshAnalytics }
|
||
}
|
||
|
||
/* ===================== PWA Features ===================== */
|
||
function usePWA() {
|
||
const [isOnline, setIsOnline] = useState(navigator.onLine)
|
||
const [installPrompt, setInstallPrompt] = useState<any>(null)
|
||
const [isInstallable, setIsInstallable] = useState(false)
|
||
|
||
useEffect(() => {
|
||
const handleOnline = () => setIsOnline(true)
|
||
const handleOffline = () => setIsOnline(false)
|
||
|
||
window.addEventListener('online', handleOnline)
|
||
window.addEventListener('offline', handleOffline)
|
||
|
||
// PWA Install Prompt
|
||
const handleBeforeInstallPrompt = (e: any) => {
|
||
e.preventDefault()
|
||
setInstallPrompt(e)
|
||
setIsInstallable(true)
|
||
}
|
||
|
||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
||
|
||
return () => {
|
||
window.removeEventListener('online', handleOnline)
|
||
window.removeEventListener('offline', handleOffline)
|
||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
||
}
|
||
}, [])
|
||
|
||
const installApp = async () => {
|
||
if (!installPrompt) return false
|
||
|
||
installPrompt.prompt()
|
||
const { outcome } = await installPrompt.userChoice
|
||
|
||
if (outcome === 'accepted') {
|
||
trackEvent('pwa_installed')
|
||
setInstallPrompt(null)
|
||
setIsInstallable(false)
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
return { isOnline, isInstallable, installApp }
|
||
}
|
||
|
||
/* ===================== SEO Meta Tags Component ===================== */
|
||
function SEOHead({ title, description, image }: { title?: string, description?: string, image?: string }) {
|
||
useEffect(() => {
|
||
// Update document title
|
||
if (title) {
|
||
document.title = `${title} | Miracles in Motion`
|
||
}
|
||
|
||
// Update meta description
|
||
const metaDescription = document.querySelector('meta[name="description"]')
|
||
if (description && metaDescription) {
|
||
metaDescription.setAttribute('content', description)
|
||
}
|
||
|
||
// Update Open Graph tags
|
||
const updateOGTag = (property: string, content: string) => {
|
||
let tag = document.querySelector(`meta[property="${property}"]`)
|
||
if (!tag) {
|
||
tag = document.createElement('meta')
|
||
tag.setAttribute('property', property)
|
||
document.head.appendChild(tag)
|
||
}
|
||
tag.setAttribute('content', content)
|
||
}
|
||
|
||
updateOGTag('og:title', title || `${SITE.name} — ${SITE.brandMessage}`)
|
||
updateOGTag('og:description', description || 'Faith-centered nonprofit bringing hope, outreach, emergency assistance, and wellness support to families in Los Angeles County.')
|
||
updateOGTag('og:image', image || `${SITE.url}/og-image.png`)
|
||
updateOGTag('og:type', 'website')
|
||
}, [title, description, image])
|
||
|
||
return null
|
||
}
|
||
|
||
/* ===================== Types ===================== */
|
||
interface IconProps {
|
||
className?: string
|
||
}
|
||
|
||
interface AnalyticsData {
|
||
pageViews: { page: string; views: number; trend: number }[]
|
||
donationMetrics: { amount: number; count: number; recurring: number }
|
||
userEngagement: { sessions: number; avgDuration: number; bounceRate: number }
|
||
conversionRates: { donation: number; volunteer: number; contact: number }
|
||
}
|
||
|
||
interface CardProps {
|
||
title: string
|
||
icon: React.ComponentType<IconProps>
|
||
children: React.ReactNode
|
||
}
|
||
|
||
interface PolicySectionProps {
|
||
id: string
|
||
title: string
|
||
children: React.ReactNode
|
||
}
|
||
|
||
/* ===================== Shared UI ===================== */
|
||
export function useHashRoute() {
|
||
const parse = () => (window.location.hash?.slice(1) || "/")
|
||
const [route, setRoute] = useState(parse())
|
||
|
||
useEffect(() => {
|
||
const onHash = () => setRoute(parse())
|
||
window.addEventListener("hashchange", onHash)
|
||
return () => window.removeEventListener("hashchange", onHash)
|
||
}, [])
|
||
|
||
return route
|
||
}
|
||
|
||
/* ===================== Legacy Router Component (Repurposed) ===================== */
|
||
|
||
|
||
/* ===================== Shared UI ===================== */
|
||
function SkipToContent() {
|
||
return (
|
||
<a
|
||
href="#content"
|
||
className="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-[100] focus:rounded-xl focus:bg-white/90 focus:px-4 focus:py-2 focus:text-sm focus:shadow-xl dark:focus:bg-black/80"
|
||
>
|
||
Skip to content
|
||
</a>
|
||
)
|
||
}
|
||
|
||
// Nav component has been extracted to ./components/Navigation.tsx
|
||
|
||
// LogoMark component has been extracted to ./components/ui/LogoMark.tsx
|
||
|
||
/* ===================== 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>
|
||
{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 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" 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 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 name="availability" aria-label="Availability" className="input">
|
||
<option>Weekdays</option>
|
||
<option>Weeknights</option>
|
||
<option>Weekends</option>
|
||
</select>
|
||
</div>
|
||
<label className="flex items-start gap-3 text-sm">
|
||
<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>
|
||
{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}>
|
||
<p className="text-sm text-neutral-700 dark:text-neutral-300">Shifts are 2–3 hours. Training provided. Youth (14+) welcome with guardian consent.</p>
|
||
</Card>
|
||
<Card title="Group volunteering" icon={Users}>
|
||
<p className="text-sm text-neutral-700 dark:text-neutral-300">We host teams of 5–25 for corporate/service groups. <a className="underline" href="#/sponsors">Contact us</a> for dates.</p>
|
||
</Card>
|
||
</aside>
|
||
</div>
|
||
</PageShell>
|
||
)
|
||
}
|
||
|
||
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"] },
|
||
{ name: "Gold", amt: "$10,000", perks: ["All Silver", "Co-branded kit drive", "Annual report feature"] },
|
||
{ name: "Platinum", amt: "$25,000+", perks: ["All Gold", "Program naming opportunity", "Custom partnership plan"] },
|
||
]
|
||
|
||
return (
|
||
<PageShell title="Corporate Sponsorship" icon={Building2} eyebrow="Partner with purpose" cta={<a className="btn-secondary" href="#/brand">Brand assets</a>}>
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
<div className="space-y-6">
|
||
{tiers.map((t) => (
|
||
<div key={t.name} className="card">
|
||
<div className="flex items-start justify-between">
|
||
<div className="text-xl font-semibold">{t.name}</div>
|
||
<div className="text-lg">{t.amt}</div>
|
||
</div>
|
||
<ul className="mt-3 list-disc space-y-1 pl-5 text-sm text-neutral-700 dark:text-neutral-300">
|
||
{t.perks.map((p, i) => (<li key={i}>{p}</li>))}
|
||
</ul>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="card">
|
||
<div className="font-medium">Start a conversation</div>
|
||
{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" 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" 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" 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>
|
||
{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>
|
||
)
|
||
}
|
||
|
||
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" },
|
||
{ title: "Community showed up", tag: "Events", body: "Neighbors and volunteers came together at an outreach event to serve families with supplies, prayer, and heartfelt support.", by: "Event volunteer" },
|
||
]
|
||
|
||
return (
|
||
<PageShell title="Stories of Hope" icon={BookOpenText} eyebrow="Hope in motion" cta={<a href="#/testimonies" className="btn-secondary">Read testimonies</a>}>
|
||
<StoriesIntroBlock />
|
||
<div className="grid gap-6 md:grid-cols-3">
|
||
{stories.map((s, i) => (
|
||
<article key={i} className="card">
|
||
<div className="text-xs uppercase tracking-wide text-neutral-500">{s.tag}</div>
|
||
<h3 className="mt-1 text-lg font-semibold">{s.title}</h3>
|
||
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{s.body}</p>
|
||
<div className="mt-3 text-xs text-neutral-500">— {s.by}</div>
|
||
</article>
|
||
))}
|
||
</div>
|
||
<div className="card">
|
||
<div className="font-medium">Submit your story</div>
|
||
{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>
|
||
{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>
|
||
)
|
||
}
|
||
|
||
function TestimoniesPage() {
|
||
const items = [
|
||
{ who: "Parent", quote: "They reminded us we were not alone — hope came when we needed it most." },
|
||
{ who: "Community partner", quote: "Compassionate, fast, and rooted in faith. Our families felt seen." },
|
||
{ who: "Principal", quote: "Attendance improves when kids have what they need." },
|
||
]
|
||
|
||
return (
|
||
<PageShell title="Testimonies" icon={Quote} eyebrow="What people say">
|
||
<div className="grid gap-6 md:grid-cols-3">
|
||
{items.map((t, i) => (
|
||
<figure key={i} className="card">
|
||
<blockquote className="text-balance">"{t.quote}"</blockquote>
|
||
<figcaption className="mt-3 text-sm text-neutral-600 dark:text-neutral-300">— {t.who}</figcaption>
|
||
</figure>
|
||
))}
|
||
</div>
|
||
</PageShell>
|
||
)
|
||
}
|
||
|
||
function ImpactReportPage() {
|
||
return (
|
||
<PageShell title="Impact Report" icon={BarChart3} eyebrow="Our impact in the community" cta={<a href="#/donate" className="btn-primary">Donate to support more families</a>}>
|
||
<div className="max-w-4xl mx-auto space-y-8">
|
||
<p className="text-lg text-neutral-700 dark:text-neutral-300">
|
||
Miracles in Motion Foundation serves vulnerable individuals and families across Los Angeles County. For impact updates or data requests, contact <a className="underline" href={SITE.emailHref}>{SITE.email}</a>.
|
||
</p>
|
||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||
<div className="card text-center">
|
||
<div className="text-3xl font-bold text-primary-600 dark:text-primary-400">85%</div>
|
||
<div className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">of donations go directly to programs & outreach</div>
|
||
</div>
|
||
<div className="card text-center">
|
||
<div className="text-3xl font-bold text-primary-600 dark:text-primary-400">10%</div>
|
||
<div className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">program operations</div>
|
||
</div>
|
||
<div className="card text-center">
|
||
<div className="text-3xl font-bold text-primary-600 dark:text-primary-400">5%</div>
|
||
<div className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">administrative</div>
|
||
</div>
|
||
<div className="card text-center">
|
||
<div className="text-2xl font-bold text-primary-600 dark:text-primary-400">$48 / $72</div>
|
||
<div className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">average grant (supplies / clothing)</div>
|
||
</div>
|
||
</div>
|
||
<div className="card">
|
||
<h3 className="font-semibold text-lg mb-2">How we confirm support</h3>
|
||
<p className="text-neutral-700 dark:text-neutral-300 text-sm">
|
||
When you submit a request for assistance, we review your situation with care and respond as quickly as possible. All information is handled in line with our <a className="underline" href="#/legal#privacy">Privacy Policy</a> and is never shared for marketing.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</PageShell>
|
||
)
|
||
}
|
||
|
||
function LegalPage() {
|
||
return (
|
||
<PageShell title="Legal & Policies" icon={FileText} eyebrow="Trust & compliance" cta={<a href="#/donate" className="btn-secondary">Donate</a>}>
|
||
<PolicySection id="privacy" title="Privacy Policy">
|
||
<p>We collect only the data necessary to process donations, volunteer coordination, and email subscriptions. We do not sell or trade personal data. Assistance requests are used only to verify and deliver support; we do not share this data for marketing. You may request access, correction, or deletion at <a className="underline" href={mailto(EMAILS.privacy)}>{EMAILS.privacy}</a>.</p>
|
||
</PolicySection>
|
||
<PolicySection id="donor-privacy" title="Donor Privacy Policy">
|
||
<p>We never sell, share, or trade donor names or personal information with any other entity, nor send donor mailings on behalf of other organizations. This policy applies to all information received online and offline.</p>
|
||
</PolicySection>
|
||
<PolicySection id="terms" title="Terms of Use">
|
||
<ul className="list-disc pl-5 space-y-2">
|
||
<li>Content is provided "as is" for informational purposes; no warranties.</li>
|
||
<li>By submitting content (stories/testimonies), you grant us a non-exclusive license to edit and publish.</li>
|
||
<li>Unauthorized scraping or misuse of site content is prohibited.</li>
|
||
</ul>
|
||
</PolicySection>
|
||
<PolicySection id="cookies" title="Cookie Policy">
|
||
<p>We use essential cookies for site functionality and, with your consent, privacy-friendly analytics. You can change your choices via the cookie banner or by clearing site data.</p>
|
||
</PolicySection>
|
||
<PolicySection id="refunds" title="Refund Policy">
|
||
<p>Donations are generally non-refundable. If you believe a donation was made in error, contact <a className="underline" href={mailto(EMAILS.donate)}>{EMAILS.donate}</a> within 15 days for assistance.</p>
|
||
</PolicySection>
|
||
<PolicySection id="gift-acceptance" title="Gift Acceptance Policy">
|
||
<ul className="list-disc pl-5 space-y-2">
|
||
<li>We accept monetary gifts, in-kind donations for outreach, DAF grants, and publicly traded securities.</li>
|
||
<li>In-kind items must be new and appropriate for families in need. We reserve the right to decline gifts not aligned with mission or capacity.</li>
|
||
</ul>
|
||
</PolicySection>
|
||
<PolicySection id="volunteer-waiver" title="Volunteer Waiver & Liability Release">
|
||
<p>By volunteering, you agree to follow staff instructions; assume ordinary risks associated with volunteering; and release Miracles in Motion from liability for ordinary negligence. You consent to background checks where required and agree to our child-safeguarding rules (no unsupervised time with minors, no personal contact outside events). Under 18 requires guardian consent.</p>
|
||
</PolicySection>
|
||
<PolicySection id="child-safety" title="Child Safety & Safeguarding">
|
||
<ul className="list-disc pl-5 space-y-2">
|
||
<li>No one-on-one unsupervised interactions with minors.</li>
|
||
<li>Report suspected abuse/neglect immediately to authorities and notify staff.</li>
|
||
<li>Photography of minors requires prior written consent from a parent/guardian.</li>
|
||
</ul>
|
||
</PolicySection>
|
||
<PolicySection id="nondiscrimination" title="Non‑Discrimination Statement">
|
||
<p>We serve individuals and families regardless of race, color, religion, national origin, sex, sexual orientation, gender identity, disability, or any other protected status.</p>
|
||
</PolicySection>
|
||
<PolicySection id="accessibility" title="Accessibility Statement">
|
||
<p>We aim to meet WCAG 2.1 AA standards. If you encounter accessibility barriers, email <a className="underline" href={mailto(EMAILS.access)}>{EMAILS.access}</a>.</p>
|
||
</PolicySection>
|
||
<PolicySection id="financials" title="Financials & 990">
|
||
<p>{SITE.name} is a {SITE.legalStatus.toLowerCase()}. EIN {SITE.ein}. Annual reports and Form 990s are available upon request or linked here when published.</p>
|
||
</PolicySection>
|
||
<PolicySection id="sponsorship-terms" title="Sponsorship Terms & Logo Use">
|
||
<ul className="list-disc pl-5 space-y-2">
|
||
<li>Logo usage requires prior written approval and must follow our <a className="underline" href="#/brand">brand assets and guidelines</a>.</li>
|
||
<li>Sponsorship does not imply endorsement of products or services.</li>
|
||
<li>Benefits may be adjusted for equivalency based on availability.</li>
|
||
</ul>
|
||
</PolicySection>
|
||
<div className="card p-4 text-xs opacity-80">
|
||
<p>State disclosures: Certain states may require additional disclosures for charitable solicitations. Where applicable, our disclosures will be presented on the donation page and receipts.</p>
|
||
</div>
|
||
</PageShell>
|
||
)
|
||
}
|
||
|
||
/* ===================== Helper Components ===================== */
|
||
function PolicySection({ id, title, children }: PolicySectionProps) {
|
||
return (
|
||
<section id={id} className="card">
|
||
<h2 className="font-semibold tracking-tight">{title}</h2>
|
||
<div className="mt-3 space-y-3 text-sm text-neutral-700 dark:text-neutral-300">{children}</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function Card({ title, icon: Icon, children }: CardProps) {
|
||
return (
|
||
<div className="card">
|
||
<div className="flex items-center gap-3">
|
||
<div className="grid h-10 w-10 place-items-center rounded-xl bg-gradient-to-br from-primary-500 to-secondary-600 text-white shadow">
|
||
<Icon className="h-5 w-5" />
|
||
</div>
|
||
<div className="font-semibold tracking-tight">{title}</div>
|
||
</div>
|
||
<div className="mt-3">{children}</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Portals Overview Page
|
||
function PortalsPage() {
|
||
return (
|
||
<PageShell title="Staff & Partner Portals" icon={Building2} eyebrow="Secure access for team members">
|
||
<div className="max-w-4xl mx-auto">
|
||
<div className="grid gap-8 md:grid-cols-2 xl:grid-cols-4">
|
||
{/* Admin Portal */}
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-red-50 to-secondary-50 dark:from-red-900/20 dark:to-secondary-900/20 border-red-200 dark:border-red-800"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.1 }}
|
||
>
|
||
<div className="text-center mb-6">
|
||
<div className="mx-auto w-16 h-16 bg-red-600 text-white rounded-xl flex items-center justify-center mb-4">
|
||
<Settings className="h-8 w-8" />
|
||
</div>
|
||
<h3 className="text-xl font-semibold text-red-900 dark:text-red-100">Administration Portal</h3>
|
||
<p className="text-sm text-red-700 dark:text-red-300 mt-2">
|
||
Full system access for administrators and directors
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-sm text-red-800 dark:text-red-200 mb-6">
|
||
<div className="flex items-center gap-2">
|
||
<UserCheck className="h-4 w-4" />
|
||
<span>Manage all requests & approvals</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Database className="h-4 w-4" />
|
||
<span>System configuration & reports</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Users className="h-4 w-4" />
|
||
<span>User management & permissions</span>
|
||
</div>
|
||
</div>
|
||
|
||
<a
|
||
href="#/admin-portal"
|
||
className="block w-full text-center bg-red-600 text-white py-3 px-4 rounded-lg hover:bg-red-700 transition-colors font-medium focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
||
>
|
||
Access Admin Portal
|
||
</a>
|
||
</motion.div>
|
||
|
||
{/* Volunteer Portal */}
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-primary-50 to-sky-50 dark:from-primary-900/20 dark:to-sky-900/20 border-blue-200 dark:border-blue-800"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.2 }}
|
||
>
|
||
<div className="text-center mb-6">
|
||
<div className="mx-auto w-16 h-16 bg-blue-600 text-white rounded-xl flex items-center justify-center mb-4">
|
||
<UserCheck className="h-8 w-8" />
|
||
</div>
|
||
<h3 className="text-xl font-semibold text-blue-900 dark:text-blue-100">Volunteer Portal</h3>
|
||
<p className="text-sm text-blue-700 dark:text-blue-300 mt-2">
|
||
For employees, volunteers, and coordinators
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-sm text-blue-800 dark:text-blue-200 mb-6">
|
||
<div className="flex items-center gap-2">
|
||
<ClipboardList className="h-4 w-4" />
|
||
<span>View assigned tasks & deliveries</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Calendar className="h-4 w-4" />
|
||
<span>Schedule & availability management</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Package className="h-4 w-4" />
|
||
<span>Inventory & kit assembly</span>
|
||
</div>
|
||
</div>
|
||
|
||
<a
|
||
href="#/volunteer-portal"
|
||
className="block w-full text-center bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 transition-colors font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||
>
|
||
Access Volunteer Portal
|
||
</a>
|
||
</motion.div>
|
||
|
||
{/* Resource Center Portal */}
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border-green-200 dark:border-green-800"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.3 }}
|
||
>
|
||
<div className="text-center mb-6">
|
||
<div className="mx-auto w-16 h-16 bg-green-600 text-white rounded-xl flex items-center justify-center mb-4">
|
||
<School className="h-8 w-8" />
|
||
</div>
|
||
<h3 className="text-xl font-semibold text-green-900 dark:text-green-100">Resource Center Portal</h3>
|
||
<p className="text-sm text-green-700 dark:text-green-300 mt-2">
|
||
For community partners, agencies, and referral organizations
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-sm text-green-800 dark:text-green-200 mb-6">
|
||
<div className="flex items-center gap-2">
|
||
<ClipboardList className="h-4 w-4" />
|
||
<span>Submit assistance requests</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<FileCheck className="h-4 w-4" />
|
||
<span>Track request status & approvals</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Truck className="h-4 w-4" />
|
||
<span>Coordinate delivery & pickup</span>
|
||
</div>
|
||
</div>
|
||
|
||
<a
|
||
href="#/resource-portal"
|
||
className="block w-full text-center bg-green-600 text-white py-3 px-4 rounded-lg hover:bg-green-700 transition-colors font-medium focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
|
||
>
|
||
Access Resource Portal
|
||
</a>
|
||
</motion.div>
|
||
|
||
{/* AI Assistance Portal - Phase 3 */}
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-primary-50 to-primary-50 dark:from-primary-900/20 dark:to-primary-900/20 border-primary-200 dark:border-primary-800"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.4 }}
|
||
>
|
||
<div className="text-center mb-6">
|
||
<div className="mx-auto w-16 h-16 bg-primary-600 text-white rounded-xl flex items-center justify-center mb-4">
|
||
<Brain className="h-8 w-8" />
|
||
</div>
|
||
<h3 className="text-xl font-semibold text-primary-900 dark:text-primary-100">AI Assistance Portal</h3>
|
||
<p className="text-sm text-primary-700 dark:text-primary-300 mt-2">
|
||
AI-powered request matching and insights
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-sm text-primary-800 dark:text-primary-200 mb-6">
|
||
<div className="flex items-center gap-2">
|
||
<Cpu className="h-4 w-4" />
|
||
<span>Real-time AI request processing</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Target className="h-4 w-4" />
|
||
<span>Smart resource matching & allocation</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Activity className="h-4 w-4" />
|
||
<span>Predictive analytics & insights</span>
|
||
</div>
|
||
</div>
|
||
|
||
<a
|
||
href="#/ai-portal"
|
||
className="block w-full text-center bg-primary-600 text-white py-3 px-4 rounded-lg hover:bg-primary-700 transition-colors font-medium focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||
>
|
||
Access AI Portal
|
||
</a>
|
||
</motion.div>
|
||
</div>
|
||
|
||
{/* Phase 3B: Enterprise Features Section */}
|
||
<div className="mt-16">
|
||
<div className="text-center mb-8">
|
||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Enterprise Analytics & Management</h2>
|
||
<p className="text-gray-600 dark:text-gray-300 mt-2">Advanced tools for organizational optimization and impact tracking</p>
|
||
</div>
|
||
|
||
<div className="grid gap-8 md:grid-cols-2 xl:grid-cols-3">
|
||
{/* Advanced Analytics */}
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-indigo-50 to-primary-50 dark:from-indigo-900/20 dark:to-primary-900/20 border-indigo-200 dark:border-indigo-800"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.5 }}
|
||
>
|
||
<div className="text-center mb-6">
|
||
<div className="mx-auto w-16 h-16 bg-indigo-600 text-white rounded-xl flex items-center justify-center mb-4">
|
||
<BarChart3 className="h-8 w-8" />
|
||
</div>
|
||
<h3 className="text-xl font-semibold text-indigo-900 dark:text-indigo-100">Advanced Analytics</h3>
|
||
<p className="text-sm text-indigo-700 dark:text-indigo-300 mt-2">
|
||
Comprehensive impact tracking and predictive insights
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-sm text-indigo-800 dark:text-indigo-200 mb-6">
|
||
<div className="flex items-center gap-2">
|
||
<TrendingUp className="h-4 w-4" />
|
||
<span>Impact forecasting & trend analysis</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Target className="h-4 w-4" />
|
||
<span>Resource demand prediction</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Activity className="h-4 w-4" />
|
||
<span>Geographic performance mapping</span>
|
||
</div>
|
||
</div>
|
||
|
||
<a
|
||
href="#/advanced-analytics"
|
||
className="block w-full text-center bg-indigo-600 text-white py-3 px-4 rounded-lg hover:bg-indigo-700 transition-colors font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||
>
|
||
View Analytics
|
||
</a>
|
||
</motion.div>
|
||
|
||
{/* Mobile Volunteer App */}
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-emerald-900/20 dark:to-teal-900/20 border-emerald-200 dark:border-emerald-800"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.6 }}
|
||
>
|
||
<div className="text-center mb-6">
|
||
<div className="mx-auto w-16 h-16 bg-emerald-600 text-white rounded-xl flex items-center justify-center mb-4">
|
||
<Phone className="h-8 w-8" />
|
||
</div>
|
||
<h3 className="text-xl font-semibold text-emerald-900 dark:text-emerald-100">Mobile Volunteer Hub</h3>
|
||
<p className="text-sm text-emerald-700 dark:text-emerald-300 mt-2">
|
||
On-the-go assignment management for field volunteers
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-sm text-emerald-800 dark:text-emerald-200 mb-6">
|
||
<div className="flex items-center gap-2">
|
||
<MapPin className="h-4 w-4" />
|
||
<span>GPS-enabled assignment tracking</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Clock className="h-4 w-4" />
|
||
<span>Real-time status updates</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Bell className="h-4 w-4" />
|
||
<span>Push notifications & alerts</span>
|
||
</div>
|
||
</div>
|
||
|
||
<a
|
||
href="#/mobile-volunteer"
|
||
className="block w-full text-center bg-emerald-600 text-white py-3 px-4 rounded-lg hover:bg-emerald-700 transition-colors font-medium focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2"
|
||
>
|
||
Launch Mobile App
|
||
</a>
|
||
</motion.div>
|
||
|
||
{/* Staff Training System */}
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-orange-50 to-amber-50 dark:from-orange-900/20 dark:to-amber-900/20 border-orange-200 dark:border-orange-800"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.7 }}
|
||
>
|
||
<div className="text-center mb-6">
|
||
<div className="mx-auto w-16 h-16 bg-orange-600 text-white rounded-xl flex items-center justify-center mb-4">
|
||
<BookOpenText className="h-8 w-8" />
|
||
</div>
|
||
<h3 className="text-xl font-semibold text-orange-900 dark:text-orange-100">Staff Training Center</h3>
|
||
<p className="text-sm text-orange-700 dark:text-orange-300 mt-2">
|
||
Comprehensive AI platform training and adoption
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-sm text-orange-800 dark:text-orange-200 mb-6">
|
||
<div className="flex items-center gap-2">
|
||
<Award className="h-4 w-4" />
|
||
<span>Certification & competency tracking</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Users className="h-4 w-4" />
|
||
<span>Onboarding & mentorship programs</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<FileText className="h-4 w-4" />
|
||
<span>Interactive training modules</span>
|
||
</div>
|
||
</div>
|
||
|
||
<a
|
||
href="#/staff-training"
|
||
className="block w-full text-center bg-orange-600 text-white py-3 px-4 rounded-lg hover:bg-orange-700 transition-colors font-medium focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2"
|
||
>
|
||
Access Training
|
||
</a>
|
||
</motion.div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Access Information */}
|
||
<div className="mt-12 card bg-neutral-50 dark:bg-neutral-800/50">
|
||
<div className="text-center mb-6">
|
||
<Lock className="mx-auto h-8 w-8 text-neutral-600 dark:text-neutral-400 mb-3" />
|
||
<h3 className="text-lg font-semibold">Access Requirements</h3>
|
||
</div>
|
||
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
<div>
|
||
<h4 className="font-medium mb-3">New Users</h4>
|
||
<ul className="text-sm text-neutral-600 dark:text-neutral-400 space-y-1">
|
||
<li>• Contact your supervisor for account setup</li>
|
||
<li>• Provide official email address</li>
|
||
<li>• Complete background check (if required)</li>
|
||
<li>• Attend system orientation session</li>
|
||
</ul>
|
||
</div>
|
||
<div>
|
||
<h4 className="font-medium mb-3">Login Issues</h4>
|
||
<ul className="text-sm text-neutral-600 dark:text-neutral-400 space-y-1">
|
||
<li>• Use forgot password link on login page</li>
|
||
<li>• Contact IT support: <a className="underline" href={SITE.emailHref}>{SITE.email}</a></li>
|
||
<li>• Call main office: (818) 491-6884</li>
|
||
<li>• Check email for account activation</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</PageShell>
|
||
)
|
||
}
|
||
|
||
/* ===================== Phase 2: UI Components ===================== */
|
||
|
||
// Real-time Notification System
|
||
export function NotificationCenter() {
|
||
const { notifications, markAsRead, clearAll, unreadCount } = useNotifications()
|
||
const [isOpen, setIsOpen] = useState(false)
|
||
|
||
// Close dropdown when clicking outside
|
||
useEffect(() => {
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
const target = event.target as Element
|
||
if (isOpen && !target.closest('[data-notification-center]')) {
|
||
setIsOpen(false)
|
||
}
|
||
}
|
||
|
||
document.addEventListener('mousedown', handleClickOutside)
|
||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||
}, [isOpen])
|
||
|
||
return (
|
||
<div className="relative" data-notification-center>
|
||
<motion.button
|
||
onClick={() => setIsOpen(!isOpen)}
|
||
className="relative p-2 text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
{unreadCount > 0 ? <BellRing className="h-6 w-6" /> : <Bell className="h-6 w-6" />}
|
||
{unreadCount > 0 && (
|
||
<motion.span
|
||
className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center"
|
||
initial={{ scale: 0 }}
|
||
animate={{ scale: 1 }}
|
||
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
||
>
|
||
{unreadCount > 9 ? '9+' : unreadCount}
|
||
</motion.span>
|
||
)}
|
||
</motion.button>
|
||
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<motion.div
|
||
className="absolute top-full right-0 mt-2 w-80 bg-white dark:bg-neutral-900 rounded-xl shadow-xl border border-neutral-200 dark:border-neutral-700 z-50"
|
||
initial={{ opacity: 0, y: -10, scale: 0.95 }}
|
||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<div className="p-4 border-b border-neutral-200 dark:border-neutral-700">
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="font-semibold">Notifications</h3>
|
||
{notifications.length > 0 && (
|
||
<button
|
||
onClick={clearAll}
|
||
className="text-sm text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300"
|
||
>
|
||
Clear All
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="max-h-96 overflow-y-auto">
|
||
{notifications.length === 0 ? (
|
||
<div className="p-8 text-center text-neutral-500">
|
||
<Bell className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||
<p>No notifications</p>
|
||
</div>
|
||
) : (
|
||
notifications.map((notification) => (
|
||
<motion.div
|
||
key={notification.id}
|
||
className={`p-4 border-b border-neutral-100 dark:border-neutral-800 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 cursor-pointer ${
|
||
!notification.read ? 'bg-primary-50/50 dark:bg-primary-900/20' : ''
|
||
}`}
|
||
onClick={() => markAsRead(notification.id)}
|
||
whileHover={{ x: 4 }}
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
<div className={`p-1 rounded-full ${
|
||
notification.type === 'success' ? 'bg-green-100 text-green-600' :
|
||
notification.type === 'error' ? 'bg-red-100 text-red-600' :
|
||
notification.type === 'warning' ? 'bg-yellow-100 text-yellow-600' :
|
||
'bg-blue-100 text-blue-600'
|
||
}`}>
|
||
{notification.type === 'success' ? <Check className="h-4 w-4" /> :
|
||
notification.type === 'error' ? <AlertCircle className="h-4 w-4" /> :
|
||
notification.type === 'warning' ? <AlertCircle className="h-4 w-4" /> :
|
||
<Bell className="h-4 w-4" />}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<h4 className="font-medium text-sm">{notification.title}</h4>
|
||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">{notification.message}</p>
|
||
<p className="text-xs text-neutral-500 mt-2">
|
||
{new Date(notification.timestamp).toLocaleString()}
|
||
</p>
|
||
</div>
|
||
{!notification.read && (
|
||
<div className="w-2 h-2 bg-primary-500 rounded-full mt-2"></div>
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
))
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Language Selector Component
|
||
export function LanguageSelector() {
|
||
const { currentLanguage, languages, changeLanguage } = useLanguage()
|
||
const [isOpen, setIsOpen] = useState(false)
|
||
|
||
// Close dropdown when clicking outside
|
||
useEffect(() => {
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
const target = event.target as Element
|
||
if (isOpen && !target.closest('[data-language-selector]')) {
|
||
setIsOpen(false)
|
||
}
|
||
}
|
||
|
||
document.addEventListener('mousedown', handleClickOutside)
|
||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||
}, [isOpen])
|
||
|
||
return (
|
||
<div className="relative" data-language-selector>
|
||
<motion.button
|
||
onClick={() => setIsOpen(!isOpen)}
|
||
className="flex items-center gap-2 p-2 text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<Languages className="h-5 w-5" />
|
||
<span className="text-lg">{currentLanguage.flag}</span>
|
||
<ChevronDown className="h-4 w-4" />
|
||
</motion.button>
|
||
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<motion.div
|
||
className="absolute top-full right-0 mt-2 bg-white dark:bg-neutral-900 rounded-xl shadow-xl border border-neutral-200 dark:border-neutral-700 z-50 min-w-48"
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
>
|
||
{languages.map((language) => (
|
||
<motion.button
|
||
key={language.code}
|
||
onClick={() => {
|
||
changeLanguage(language.code)
|
||
setIsOpen(false)
|
||
}}
|
||
className={`w-full flex items-center gap-3 p-3 hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors first:rounded-t-xl last:rounded-b-xl ${
|
||
currentLanguage.code === language.code ? 'bg-primary-50 dark:bg-primary-900/20' : ''
|
||
}`}
|
||
whileHover={{ x: 4 }}
|
||
>
|
||
<span className="text-lg">{language.flag}</span>
|
||
<div className="text-left">
|
||
<div className="font-medium text-sm">{language.name}</div>
|
||
<div className="text-xs text-neutral-500">{language.nativeName}</div>
|
||
</div>
|
||
{currentLanguage.code === language.code && (
|
||
<Check className="h-4 w-4 text-primary-600 ml-auto" />
|
||
)}
|
||
</motion.button>
|
||
))}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// PWA Install Prompt
|
||
function PWAInstallPrompt() {
|
||
const { isInstallable, installApp } = usePWA()
|
||
const [showPrompt, setShowPrompt] = useState(false)
|
||
|
||
useEffect(() => {
|
||
if (isInstallable) {
|
||
const timer = setTimeout(() => setShowPrompt(true), 3000)
|
||
return () => clearTimeout(timer)
|
||
}
|
||
}, [isInstallable])
|
||
|
||
if (!showPrompt) return null
|
||
|
||
return (
|
||
<AnimatePresence>
|
||
<motion.div
|
||
className="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-80 bg-white dark:bg-neutral-900 rounded-xl shadow-xl border border-neutral-200 dark:border-neutral-700 z-50"
|
||
initial={{ opacity: 0, y: 100 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: 100 }}
|
||
>
|
||
<div className="p-4">
|
||
<div className="flex items-start gap-3">
|
||
<div className="p-2 bg-primary-100 dark:bg-primary-900/30 rounded-lg">
|
||
<Download className="h-5 w-5 text-primary-600 dark:text-primary-400" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h3 className="font-semibold text-sm">Install Miracles in Motion</h3>
|
||
<p className="text-xs text-neutral-600 dark:text-neutral-400 mt-1">
|
||
Get faster access and offline features by installing our app
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowPrompt(false)}
|
||
className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300"
|
||
aria-label="Dismiss install prompt"
|
||
>
|
||
<X className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
<div className="flex gap-2 mt-4">
|
||
<button
|
||
onClick={async () => {
|
||
const success = await installApp()
|
||
if (success) setShowPrompt(false)
|
||
}}
|
||
className="btn-primary text-sm px-4 py-2 flex-1"
|
||
>
|
||
Install
|
||
</button>
|
||
<button
|
||
onClick={() => setShowPrompt(false)}
|
||
className="btn-secondary text-sm px-4 py-2"
|
||
>
|
||
Later
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
)
|
||
}
|
||
|
||
/* ===================== Authentication Components ===================== */
|
||
function LoginForm({ requiredRole }: { requiredRole?: 'admin' | 'volunteer' | 'resource' }) {
|
||
const { login, isLoading } = useAuth()
|
||
const [formData, setFormData] = useState({ email: '', password: '' })
|
||
const [error, setError] = useState('')
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
setError('')
|
||
|
||
const success = await login(formData.email, formData.password)
|
||
if (!success) {
|
||
setError('Invalid credentials. Please try again.')
|
||
}
|
||
}
|
||
|
||
const getRoleHint = () => {
|
||
switch (requiredRole) {
|
||
case 'admin': return 'Use an email containing "admin" to access admin features'
|
||
case 'volunteer': return 'Use an email containing "volunteer" for volunteer access'
|
||
case 'resource': return 'Use any other email for resource center access'
|
||
default: return 'Enter your credentials to access the portal'
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-secondary-50 dark:from-gray-900 dark:to-gray-800 px-4">
|
||
<motion.div
|
||
className="w-full max-w-md"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.6 }}
|
||
>
|
||
<div className="text-center mb-8">
|
||
<motion.div
|
||
className="inline-flex items-center justify-center w-16 h-16 bg-primary-600 text-white rounded-full mb-4"
|
||
whileHover={{ scale: 1.05, rotateY: 180 }}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<Lock className="w-8 h-8" />
|
||
</motion.div>
|
||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||
{requiredRole ? `${requiredRole.charAt(0).toUpperCase() + requiredRole.slice(1)} Portal` : 'Portal Access'}
|
||
</h1>
|
||
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
||
Sign in to access your dashboard
|
||
</p>
|
||
</div>
|
||
|
||
<div className="card">
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<div>
|
||
<label htmlFor="email" className="block text-sm font-medium mb-2">
|
||
Email Address
|
||
</label>
|
||
<input
|
||
id="email"
|
||
type="email"
|
||
required
|
||
value={formData.email}
|
||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||
className="input w-full focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
placeholder="[email protected]"
|
||
style={{ minHeight: '44px' }} // Mobile touch optimization
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="password" className="block text-sm font-medium mb-2">
|
||
Password
|
||
</label>
|
||
<input
|
||
id="password"
|
||
type="password"
|
||
required
|
||
value={formData.password}
|
||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||
className="input w-full focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
placeholder="Enter your password"
|
||
style={{ minHeight: '44px' }} // Mobile touch optimization
|
||
/>
|
||
</div>
|
||
|
||
{error && (
|
||
<motion.div
|
||
className="text-red-600 dark:text-red-400 text-sm p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800"
|
||
initial={{ opacity: 0, x: -10 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
>
|
||
{error}
|
||
</motion.div>
|
||
)}
|
||
|
||
<div className="text-xs text-gray-500 dark:text-gray-400 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||
<strong>Demo Access:</strong> {getRoleHint()}
|
||
</div>
|
||
|
||
<motion.button
|
||
type="submit"
|
||
disabled={isLoading}
|
||
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
||
style={{ minHeight: '44px' }} // Mobile touch optimization
|
||
whileHover={{ scale: isLoading ? 1 : 1.02 }}
|
||
whileTap={{ scale: isLoading ? 1 : 0.98 }}
|
||
>
|
||
{isLoading ? (
|
||
<><Clock className="w-4 h-4 mr-2 animate-spin" /> Signing In...</>
|
||
) : (
|
||
<>Sign In</>
|
||
)}
|
||
</motion.button>
|
||
</form>
|
||
|
||
<div className="mt-6 text-center">
|
||
<a href="#/" className="text-sm text-primary-600 dark:text-primary-400 hover:underline">
|
||
← Back to Main Site
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function PortalWrapper({ children, requiredRole }: { children: React.ReactNode, requiredRole?: 'admin' | 'volunteer' | 'resource' }) {
|
||
const { user } = useAuth()
|
||
|
||
if (!user) {
|
||
return <LoginForm requiredRole={requiredRole} />
|
||
}
|
||
|
||
if (requiredRole && user.role !== requiredRole) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center px-4">
|
||
<div className="card max-w-md w-full text-center">
|
||
<AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||
<h2 className="text-xl font-bold mb-2">Access Denied</h2>
|
||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||
You don't have permission to access the {requiredRole} portal.
|
||
</p>
|
||
<div className="space-y-2">
|
||
<a href="#/" className="btn-primary">
|
||
Return to Main Site
|
||
</a>
|
||
<button
|
||
onClick={() => useAuth().logout()}
|
||
className="btn-secondary w-full"
|
||
>
|
||
Sign Out
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return <>{children}</>
|
||
}
|
||
|
||
// Admin Portal Dashboard
|
||
function AdminPortalPage() {
|
||
const { user, logout } = useAuth()
|
||
const [stats] = useState({
|
||
pendingRequests: 23,
|
||
activeVolunteers: 47,
|
||
deliveriesToday: 8,
|
||
monthlyBudget: 15000,
|
||
monthlySpent: 8250
|
||
})
|
||
|
||
useEffect(() => {
|
||
trackEvent('admin_portal_view', { user_id: user?.id, user_role: user?.role })
|
||
}, [])
|
||
|
||
return (
|
||
<PortalWrapper requiredRole="admin">
|
||
<SEOHead title="Admin Dashboard" description="Administrative portal for Miracles in Motion staff and administrators." />
|
||
<PageShell
|
||
title="Administration Dashboard"
|
||
icon={Settings}
|
||
eyebrow={`Welcome back, ${user?.name}`}
|
||
cta={
|
||
<button onClick={logout} className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
|
||
Sign Out
|
||
</button>
|
||
}
|
||
>
|
||
<div className="space-y-8">
|
||
{/* Quick Stats */}
|
||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||
<motion.div className="card bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }}>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-red-700 dark:text-red-300">Pending Requests</p>
|
||
<p className="text-2xl font-bold text-red-900 dark:text-red-100">{stats.pendingRequests}</p>
|
||
</div>
|
||
<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400" />
|
||
</div>
|
||
</motion.div>
|
||
|
||
<motion.div className="card bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }}>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-blue-700 dark:text-blue-300">Active Volunteers</p>
|
||
<p className="text-2xl font-bold text-blue-900 dark:text-blue-100">{stats.activeVolunteers}</p>
|
||
</div>
|
||
<UserCheck className="h-8 w-8 text-blue-600 dark:text-blue-400" />
|
||
</div>
|
||
</motion.div>
|
||
|
||
<motion.div className="card bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.3 }}>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-green-700 dark:text-green-300">Deliveries Today</p>
|
||
<p className="text-2xl font-bold text-green-900 dark:text-green-100">{stats.deliveriesToday}</p>
|
||
</div>
|
||
<Truck className="h-8 w-8 text-green-600 dark:text-green-400" />
|
||
</div>
|
||
</motion.div>
|
||
|
||
<motion.div className="card bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.4 }}>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-yellow-700 dark:text-yellow-300">Budget Used</p>
|
||
<p className="text-2xl font-bold text-yellow-900 dark:text-yellow-100">{Math.round((stats.monthlySpent / stats.monthlyBudget) * 100)}%</p>
|
||
</div>
|
||
<DollarSign className="h-8 w-8 text-yellow-600 dark:text-yellow-400" />
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
|
||
<div className="grid gap-8 lg:grid-cols-3">
|
||
{/* Recent Requests */}
|
||
<div className="lg:col-span-2">
|
||
<div className="card">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h3 className="text-lg font-semibold">Recent Assistance Requests</h3>
|
||
<button className="btn-secondary text-sm">View All</button>
|
||
</div>
|
||
<div className="space-y-4">
|
||
{[
|
||
{ student: 'Maria S.', school: 'San Fernando Valley', need: 'Emergency essentials', priority: 'High', time: '2 hours ago' },
|
||
{ student: 'James R.', school: 'South LA', need: 'Clothing & outreach', priority: 'Medium', time: '4 hours ago' },
|
||
{ student: 'Ana L.', school: 'East LA', need: 'Resource navigation', priority: 'Low', time: '1 day ago' }
|
||
].map((request, i) => (
|
||
<div key={i} className="flex items-center justify-between p-4 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<span className="font-medium">{request.student}</span>
|
||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||
request.priority === 'High' ? 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300' :
|
||
request.priority === 'Medium' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300' :
|
||
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300'
|
||
}`}>
|
||
{request.priority}
|
||
</span>
|
||
</div>
|
||
<p className="text-sm text-neutral-600 dark:text-neutral-400">{request.school}</p>
|
||
<p className="text-sm">{request.need}</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-xs text-neutral-500">{request.time}</p>
|
||
<button className="text-primary-600 dark:text-primary-400 text-sm mt-1 hover:underline">Review</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick Actions */}
|
||
<div>
|
||
<div className="card">
|
||
<h3 className="text-lg font-semibold mb-6">Quick Actions</h3>
|
||
<div className="space-y-3">
|
||
<button className="w-full btn-primary text-left justify-start">
|
||
<Plus className="mr-2 h-4 w-4" />
|
||
Create New User
|
||
</button>
|
||
<button className="w-full btn-secondary text-left justify-start">
|
||
<FileCheck className="mr-2 h-4 w-4" />
|
||
Approve Pending Requests
|
||
</button>
|
||
<button className="w-full btn-secondary text-left justify-start">
|
||
<Database className="mr-2 h-4 w-4" />
|
||
Generate Reports
|
||
</button>
|
||
<button className="w-full btn-secondary text-left justify-start">
|
||
<Settings className="mr-2 h-4 w-4" />
|
||
System Settings
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</PageShell>
|
||
</PortalWrapper>
|
||
)
|
||
}
|
||
|
||
// Volunteer Portal Dashboard
|
||
function VolunteerPortalPage() {
|
||
const { user, logout } = useAuth()
|
||
|
||
useEffect(() => {
|
||
trackEvent('volunteer_portal_view', { user_id: user?.id, user_role: user?.role })
|
||
}, [])
|
||
|
||
return (
|
||
<PortalWrapper requiredRole="volunteer">
|
||
<SEOHead title="Volunteer Dashboard" description="Volunteer portal for Miracles in Motion volunteers to manage assignments and schedules." />
|
||
<PageShell
|
||
title="Volunteer Dashboard"
|
||
icon={UserCheck}
|
||
eyebrow={`Hello, ${user?.name}`}
|
||
cta={
|
||
<button onClick={logout} className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
|
||
Sign Out
|
||
</button>
|
||
}
|
||
>
|
||
<div className="space-y-8">
|
||
{/* Today's Tasks */}
|
||
<div className="card bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
|
||
<div className="flex items-center gap-4 mb-6">
|
||
<Calendar className="h-8 w-8 text-blue-600 dark:text-blue-400" />
|
||
<div>
|
||
<h3 className="text-lg font-semibold text-blue-900 dark:text-blue-100">Today's Schedule</h3>
|
||
<p className="text-blue-700 dark:text-blue-300">Tuesday, March 14, 2024</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{[
|
||
{ time: '9:00 AM', task: 'Outreach kit assembly', location: 'Main warehouse', students: 12 },
|
||
{ time: '1:00 PM', task: 'Delivery route — Valley area', location: 'LA County', students: 5 },
|
||
{ time: '3:30 PM', task: 'Inventory — seasonal clothing', location: 'Storage room B', students: null }
|
||
].map((task, i) => (
|
||
<div key={i} className="flex items-center justify-between p-4 bg-white dark:bg-neutral-900 rounded-lg shadow-sm">
|
||
<div className="flex items-center gap-4">
|
||
<div className="text-center">
|
||
<div className="text-lg font-semibold text-blue-600 dark:text-blue-400">{task.time}</div>
|
||
</div>
|
||
<div>
|
||
<h4 className="font-medium">{task.task}</h4>
|
||
<p className="text-sm text-neutral-600 dark:text-neutral-400">{task.location}</p>
|
||
{task.students && <p className="text-xs text-green-600 dark:text-green-400">{task.students} families served</p>}
|
||
</div>
|
||
</div>
|
||
<button className="btn-secondary text-sm">
|
||
<Check className="mr-1 h-3 w-3" />
|
||
Complete
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-8 lg:grid-cols-2">
|
||
{/* Assigned Deliveries */}
|
||
<div className="card">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h3 className="text-lg font-semibold">Pending Deliveries</h3>
|
||
<Truck className="h-6 w-6 text-neutral-400" />
|
||
</div>
|
||
<div className="space-y-3">
|
||
{[
|
||
{ student: 'Sofia M.', items: 'Essentials kit', school: 'San Fernando Valley', deadline: 'Tomorrow' },
|
||
{ student: 'Carlos R.', items: 'Winter clothing', school: 'South LA', deadline: 'Friday' },
|
||
{ student: 'Emma K.', items: 'Wellness navigation', school: 'East LA', deadline: 'Next week' }
|
||
].map((delivery, i) => (
|
||
<div key={i} className="p-3 border border-neutral-200 dark:border-neutral-700 rounded-lg">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="font-medium">{delivery.student}</span>
|
||
<span className="text-xs text-neutral-500">{delivery.deadline}</span>
|
||
</div>
|
||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-1">{delivery.items}</p>
|
||
<p className="text-xs text-neutral-500">{delivery.school}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Volunteer Stats */}
|
||
<div className="card">
|
||
<h3 className="text-lg font-semibold mb-6">Your Impact This Month</h3>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Users className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||
<span>Families helped</span>
|
||
</div>
|
||
<span className="font-semibold text-primary-600 dark:text-primary-400">47</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Package className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||
<span>Kits Assembled</span>
|
||
</div>
|
||
<span className="font-semibold text-primary-600 dark:text-primary-400">23</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Truck className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||
<span>Deliveries Made</span>
|
||
</div>
|
||
<span className="font-semibold text-primary-600 dark:text-primary-400">15</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Clock className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||
<span>Hours Volunteered</span>
|
||
</div>
|
||
<span className="font-semibold text-primary-600 dark:text-primary-400">32</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</PageShell>
|
||
</PortalWrapper>
|
||
)
|
||
}
|
||
|
||
// Resource Center Portal Dashboard
|
||
function ResourcePortalPage() {
|
||
const { user, logout } = useAuth()
|
||
|
||
useEffect(() => {
|
||
trackEvent('resource_portal_view', { user_id: user?.id, user_role: user?.role })
|
||
}, [])
|
||
|
||
return (
|
||
<PortalWrapper requiredRole="resource">
|
||
<SEOHead title="Resource Portal" description="Partner portal for submitting and tracking assistance requests." />
|
||
<PageShell
|
||
title="Resource Center Portal"
|
||
icon={School}
|
||
eyebrow={`Welcome, ${user?.name}`}
|
||
cta={
|
||
<button onClick={logout} className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
|
||
Sign Out
|
||
</button>
|
||
}
|
||
>
|
||
<div className="space-y-8">
|
||
{/* Quick Submit */}
|
||
<div className="card bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
|
||
<div className="flex items-center gap-4 mb-6">
|
||
<Plus className="h-8 w-8 text-green-600 dark:text-green-400" />
|
||
<div>
|
||
<h3 className="text-lg font-semibold text-green-900 dark:text-green-100">Quick Request Submission</h3>
|
||
<p className="text-green-700 dark:text-green-300">Submit a new assistance request</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col sm:flex-row gap-4">
|
||
<a href="#/request-assistance" className="flex-1 btn-primary">
|
||
<ClipboardList className="mr-2 h-4 w-4" />
|
||
New Assistance Request
|
||
</a>
|
||
<button className="btn-secondary">
|
||
<FileCheck className="mr-2 h-4 w-4" />
|
||
Bulk Upload (CSV)
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-8 lg:grid-cols-3">
|
||
{/* Request Status */}
|
||
<div className="lg:col-span-2">
|
||
<div className="card">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h3 className="text-lg font-semibold">Your Recent Requests</h3>
|
||
<button className="btn-secondary text-sm">View All Requests</button>
|
||
</div>
|
||
<div className="space-y-4">
|
||
{[
|
||
{ id: 'REQ-2024-0342', student: 'Maria Santos', status: 'In Progress', need: 'Emergency essentials', submitted: '2 days ago', eta: 'Tomorrow' },
|
||
{ id: 'REQ-2024-0341', student: 'James Rodriguez', status: 'Approved', need: 'Clothing & outreach', submitted: '3 days ago', eta: 'Today' },
|
||
{ id: 'REQ-2024-0340', student: 'Ana Lopez', status: 'Delivered', need: 'Resource navigation', submitted: '1 week ago', eta: 'Completed' }
|
||
].map((request, i) => (
|
||
<div key={i} className="p-4 border border-neutral-200 dark:border-neutral-700 rounded-lg">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-3">
|
||
<span className="font-mono text-sm text-neutral-500">{request.id}</span>
|
||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||
request.status === 'Delivered' ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' :
|
||
request.status === 'Approved' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300' :
|
||
request.status === 'In Progress' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300' :
|
||
'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300'
|
||
}`}>
|
||
{request.status}
|
||
</span>
|
||
</div>
|
||
<span className="text-xs text-neutral-500">{request.submitted}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="font-medium">{request.student}</p>
|
||
<p className="text-sm text-neutral-600 dark:text-neutral-400">{request.need}</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-sm text-neutral-600 dark:text-neutral-400">ETA: {request.eta}</p>
|
||
<button className="text-primary-600 dark:text-primary-400 text-sm hover:underline">View Details</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stats & Resources */}
|
||
<div className="space-y-6">
|
||
<div className="card">
|
||
<h3 className="text-lg font-semibold mb-4">Monthly Summary</h3>
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm">Requests Submitted</span>
|
||
<span className="font-semibold text-green-600 dark:text-green-400">12</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm">Students Helped</span>
|
||
<span className="font-semibold text-green-600 dark:text-green-400">28</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm">Avg. Response Time</span>
|
||
<span className="font-semibold text-green-600 dark:text-green-400">18 hrs</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="card">
|
||
<h3 className="text-lg font-semibold mb-4">Quick Links</h3>
|
||
<div className="space-y-2">
|
||
<a href="#/request-assistance" className="block w-full btn-secondary text-left justify-start text-sm">
|
||
<ClipboardList className="mr-2 h-4 w-4" />
|
||
Submit Request
|
||
</a>
|
||
<button className="w-full btn-secondary text-left justify-start text-sm">
|
||
<FileCheck className="mr-2 h-4 w-4" />
|
||
Request History
|
||
</button>
|
||
<button className="w-full btn-secondary text-left justify-start text-sm">
|
||
<Calendar className="mr-2 h-4 w-4" />
|
||
Schedule Pickup
|
||
</button>
|
||
<button className="w-full btn-secondary text-left justify-start text-sm">
|
||
<Phone className="mr-2 h-4 w-4" />
|
||
Contact Support
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</PageShell>
|
||
</PortalWrapper>
|
||
)
|
||
}
|
||
|
||
// Advanced Analytics Dashboard
|
||
function AnalyticsDashboard() {
|
||
const { user, logout } = useAuth()
|
||
const { analyticsData, refreshAnalytics } = useAnalytics()
|
||
const { addNotification } = useNotifications()
|
||
|
||
useEffect(() => {
|
||
trackEvent('analytics_dashboard_view', { user_id: user?.id, user_role: user?.role })
|
||
}, [])
|
||
|
||
const handleRefresh = () => {
|
||
refreshAnalytics()
|
||
addNotification({
|
||
type: 'success',
|
||
title: 'Data Refreshed',
|
||
message: 'Analytics data has been updated with the latest information'
|
||
})
|
||
}
|
||
|
||
return (
|
||
<PortalWrapper requiredRole="admin">
|
||
<SEOHead title="Analytics Dashboard" description="Real-time analytics and insights for Miracles in Motion." />
|
||
<PageShell
|
||
title="Analytics Dashboard"
|
||
icon={BarChart3}
|
||
eyebrow={`Data Insights for ${user?.name}`}
|
||
cta={
|
||
<div className="flex gap-2">
|
||
<motion.button
|
||
onClick={handleRefresh}
|
||
className="btn-secondary flex items-center gap-2"
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<Activity className="h-4 w-4" /> Refresh
|
||
</motion.button>
|
||
<button onClick={logout} className="btn-secondary">
|
||
Sign Out
|
||
</button>
|
||
</div>
|
||
}
|
||
>
|
||
<div className="space-y-8">
|
||
{/* Key Metrics */}
|
||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-primary-50 to-primary-100 dark:from-primary-900/20 dark:to-primary-800/20 border-blue-200 dark:border-blue-800"
|
||
whileHover={{ scale: 1.02, y: -2 }}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-blue-600 dark:text-blue-400">Total Donations</p>
|
||
<p className="text-2xl font-bold text-blue-900 dark:text-blue-100">
|
||
${analyticsData.donationMetrics.amount.toLocaleString()}
|
||
</p>
|
||
</div>
|
||
<TrendingUp className="h-8 w-8 text-blue-500" />
|
||
</div>
|
||
<div className="mt-2 flex items-center text-sm">
|
||
<span className="text-green-600 dark:text-green-400 font-medium">+12.5%</span>
|
||
<span className="text-blue-600 dark:text-blue-400 ml-1">vs last month</span>
|
||
</div>
|
||
</motion.div>
|
||
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 border-green-200 dark:border-green-800"
|
||
whileHover={{ scale: 1.02, y: -2 }}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-green-600 dark:text-green-400">Active Volunteers</p>
|
||
<p className="text-2xl font-bold text-green-900 dark:text-green-100">247</p>
|
||
</div>
|
||
<Users className="h-8 w-8 text-green-500" />
|
||
</div>
|
||
<div className="mt-2 flex items-center text-sm">
|
||
<span className="text-green-600 dark:text-green-400 font-medium">+8.3%</span>
|
||
<span className="text-green-600 dark:text-green-400 ml-1">vs last month</span>
|
||
</div>
|
||
</motion.div>
|
||
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-primary-50 to-primary-100 dark:from-primary-900/20 dark:to-primary-800/20 border-primary-200 dark:border-primary-800"
|
||
whileHover={{ scale: 1.02, y: -2 }}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-primary-600 dark:text-primary-400">Families helped</p>
|
||
<p className="text-2xl font-bold text-primary-900 dark:text-primary-100">1,847</p>
|
||
</div>
|
||
<Target className="h-8 w-8 text-primary-500" />
|
||
</div>
|
||
<div className="mt-2 flex items-center text-sm">
|
||
<span className="text-green-600 dark:text-green-400 font-medium">+15.2%</span>
|
||
<span className="text-primary-600 dark:text-primary-400 ml-1">vs last month</span>
|
||
</div>
|
||
</motion.div>
|
||
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-orange-50 to-orange-100 dark:from-orange-900/20 dark:to-orange-800/20 border-orange-200 dark:border-orange-800"
|
||
whileHover={{ scale: 1.02, y: -2 }}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-orange-600 dark:text-orange-400">Conversion Rate</p>
|
||
<p className="text-2xl font-bold text-orange-900 dark:text-orange-100">
|
||
{(analyticsData.conversionRates.donation * 100).toFixed(1)}%
|
||
</p>
|
||
</div>
|
||
<Zap className="h-8 w-8 text-orange-500" />
|
||
</div>
|
||
<div className="mt-2 flex items-center text-sm">
|
||
<span className="text-green-600 dark:text-green-400 font-medium">+3.1%</span>
|
||
<span className="text-orange-600 dark:text-orange-400 ml-1">vs last month</span>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
|
||
{/* Page Views Chart */}
|
||
<div className="card">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h3 className="text-lg font-semibold">Page Performance</h3>
|
||
<div className="flex items-center gap-2 text-sm text-neutral-500">
|
||
<Eye className="h-4 w-4" />
|
||
Last 30 days
|
||
</div>
|
||
</div>
|
||
<div className="space-y-4">
|
||
{analyticsData.pageViews.map((page, index) => (
|
||
<motion.div
|
||
key={page.page}
|
||
className="flex items-center justify-between p-4 bg-neutral-50 dark:bg-neutral-800/50 rounded-lg"
|
||
initial={{ opacity: 0, x: -20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
transition={{ delay: index * 0.1 }}
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<div className={`w-3 h-8 rounded-full ${
|
||
page.trend > 0 ? 'bg-green-500' : page.trend < 0 ? 'bg-red-500' : 'bg-gray-400'
|
||
}`} />
|
||
<div>
|
||
<div className="font-medium">{page.page}</div>
|
||
<div className="text-sm text-neutral-500">{page.views.toLocaleString()} views</div>
|
||
</div>
|
||
</div>
|
||
<div className={`flex items-center gap-1 text-sm font-medium ${
|
||
page.trend > 0 ? 'text-green-600' : page.trend < 0 ? 'text-red-600' : 'text-gray-600'
|
||
}`}>
|
||
<TrendingUp className="h-4 w-4" />
|
||
{page.trend > 0 ? '+' : ''}{page.trend.toFixed(1)}%
|
||
</div>
|
||
</motion.div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Real-time Activity */}
|
||
<div className="grid gap-6 lg:grid-cols-2">
|
||
<div className="card">
|
||
<h3 className="text-lg font-semibold mb-4">Recent Activity</h3>
|
||
<div className="space-y-3">
|
||
{[
|
||
{ action: 'New donation', details: '$125 from Sarah M.', time: '2 minutes ago', icon: Heart },
|
||
{ action: 'Volunteer signup', details: 'John D. registered', time: '8 minutes ago', icon: Users },
|
||
{ action: 'Assistance request', details: 'San Fernando Valley', time: '15 minutes ago', icon: MapPin },
|
||
{ action: 'Story shared', details: 'Maria\'s success story', time: '1 hour ago', icon: BookOpenText }
|
||
].map((activity, index) => (
|
||
<motion.div
|
||
key={index}
|
||
className="flex items-center gap-3 p-3 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 rounded-lg transition-colors"
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: index * 0.1 }}
|
||
>
|
||
<div className="p-2 bg-primary-100 dark:bg-primary-900/30 rounded-lg">
|
||
<activity.icon className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<div className="font-medium text-sm">{activity.action}</div>
|
||
<div className="text-sm text-neutral-500">{activity.details}</div>
|
||
</div>
|
||
<div className="text-xs text-neutral-400">{activity.time}</div>
|
||
</motion.div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="card">
|
||
<h3 className="text-lg font-semibold mb-4">Impact Summary</h3>
|
||
<div className="grid gap-4">
|
||
<div className="flex items-center justify-between p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||
<div className="flex items-center gap-3">
|
||
<Backpack className="h-6 w-6 text-green-600" />
|
||
<span className="font-medium">Backpacks Distributed</span>
|
||
</div>
|
||
<span className="text-2xl font-bold text-green-600">342</span>
|
||
</div>
|
||
<div className="flex items-center justify-between p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||
<div className="flex items-center gap-3">
|
||
<Shirt className="h-6 w-6 text-blue-600" />
|
||
<span className="font-medium">Clothing Items</span>
|
||
</div>
|
||
<span className="text-2xl font-bold text-blue-600">789</span>
|
||
</div>
|
||
<div className="flex items-center justify-between p-4 bg-primary-50 dark:bg-primary-900/20 rounded-lg">
|
||
<div className="flex items-center gap-3">
|
||
<AlertCircle className="h-6 w-6 text-primary-600" />
|
||
<span className="font-medium">Emergency Responses</span>
|
||
</div>
|
||
<span className="text-2xl font-bold text-primary-600">156</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</PageShell>
|
||
</PortalWrapper>
|
||
)
|
||
}
|
||
|
||
// Phase 3: AI Portal Page
|
||
function AIPortalPage() {
|
||
const { user, logout } = useAuth()
|
||
|
||
useEffect(() => {
|
||
trackEvent('ai_portal_view', { user_id: user?.id, user_role: user?.role })
|
||
}, [])
|
||
|
||
return (
|
||
<PortalWrapper requiredRole="admin">
|
||
<SEOHead title="AI Assistance Portal" description="AI-powered request matching and insights for Miracles in Motion Foundation." />
|
||
<PageShell
|
||
title="AI Assistance Portal"
|
||
icon={Brain}
|
||
eyebrow={`AI-Powered Matching for ${user?.name}`}
|
||
cta={
|
||
<div className="flex gap-2">
|
||
<motion.button
|
||
className="btn-secondary flex items-center gap-2"
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<Cpu className="h-4 w-4" /> Model Status
|
||
</motion.button>
|
||
<button onClick={logout} className="btn-secondary">
|
||
Sign Out
|
||
</button>
|
||
</div>
|
||
}
|
||
>
|
||
<AIAssistancePortal userRole={user?.role === 'admin' ? 'admin' : 'coordinator'} />
|
||
</PageShell>
|
||
</PortalWrapper>
|
||
)
|
||
}
|
||
|
||
// Phase 3B: Enterprise Feature Pages
|
||
function AdvancedAnalyticsPage() {
|
||
const { user, logout } = useAuth()
|
||
|
||
useEffect(() => {
|
||
trackEvent('advanced_analytics_view', { user_id: user?.id, user_role: user?.role })
|
||
}, [])
|
||
|
||
return (
|
||
<PortalWrapper requiredRole="admin">
|
||
<SEOHead title="Advanced Analytics" description="Comprehensive impact analytics and predictive insights for nonprofit operations." />
|
||
<div className="relative">
|
||
<AdvancedAnalyticsDashboard />
|
||
<div className="fixed top-4 right-4 z-50">
|
||
<button onClick={logout} className="btn-secondary">
|
||
Sign Out
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</PortalWrapper>
|
||
)
|
||
}
|
||
|
||
function MobileVolunteerPage() {
|
||
const { user } = useAuth()
|
||
|
||
useEffect(() => {
|
||
trackEvent('mobile_volunteer_view', { user_id: user?.id })
|
||
}, [])
|
||
|
||
return (
|
||
<div className="relative">
|
||
<SEOHead title="Volunteer Mobile App" description="Mobile interface for volunteer assignment management and coordination." />
|
||
<MobileVolunteerApp />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function StaffTrainingPage() {
|
||
const { user, logout } = useAuth()
|
||
|
||
useEffect(() => {
|
||
trackEvent('staff_training_view', { user_id: user?.id, user_role: user?.role })
|
||
}, [])
|
||
|
||
return (
|
||
<PortalWrapper requiredRole="admin">
|
||
<SEOHead title="Staff Training & Adoption" description="Comprehensive training system for AI platform adoption and staff development." />
|
||
<div className="relative">
|
||
<StaffTrainingDashboard />
|
||
<div className="fixed top-4 right-4 z-50">
|
||
<button onClick={logout} className="btn-secondary">
|
||
Sign Out
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</PortalWrapper>
|
||
)
|
||
}
|
||
|
||
function NotFoundPage() {
|
||
return (
|
||
<section className="relative py-24">
|
||
<div className="mx-auto max-w-md px-4 text-center sm:px-6 lg:px-8">
|
||
<h1 className="text-6xl font-bold text-neutral-300 dark:text-neutral-700">404</h1>
|
||
<h2 className="mt-4 text-2xl font-semibold">Page not found</h2>
|
||
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
||
The page you're looking for doesn't exist.
|
||
</p>
|
||
<a href="#/" className="btn-primary mt-6">
|
||
Go home
|
||
</a>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function BackgroundDecor() {
|
||
return (
|
||
<div className="pointer-events-none fixed inset-0 overflow-hidden">
|
||
<div className="absolute -top-40 -right-40 h-80 w-80 rounded-full bg-gradient-to-br from-primary-400/20 to-secondary-600/20 blur-3xl" />
|
||
<div className="absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-gradient-to-tr from-secondary-400/20 to-primary-600/20 blur-3xl" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Footer component has been extracted to ./components/Footer.tsx
|
||
|
||
function StickyDonate() {
|
||
return (
|
||
<div className="fixed bottom-4 left-4 z-40 lg:hidden">
|
||
<a
|
||
href="#/donate"
|
||
className="flex h-14 w-14 items-center justify-center rounded-full bg-gradient-to-br from-primary-500 to-secondary-600 text-white shadow-lg shadow-primary-500/25 transition hover:scale-105"
|
||
aria-label="Donate"
|
||
>
|
||
<Heart className="h-6 w-6" />
|
||
</a>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const COOKIE_CONSENT_KEY = 'mim_cookie_consent'
|
||
|
||
function CookieBanner() {
|
||
const [show, setShow] = useState(() => {
|
||
if (typeof window === 'undefined') return true
|
||
return window.localStorage.getItem(COOKIE_CONSENT_KEY) === null
|
||
})
|
||
|
||
const persistAndClose = (choice: 'accept' | 'decline') => {
|
||
try {
|
||
window.localStorage.setItem(COOKIE_CONSENT_KEY, choice)
|
||
} catch (_) { /* ignore */ }
|
||
setShow(false)
|
||
}
|
||
|
||
if (!show) return null
|
||
|
||
return (
|
||
<div className="fixed bottom-20 left-4 right-4 z-40 card p-4 sm:bottom-4 md:max-w-md md:right-auto lg:bottom-4" role="dialog" aria-label="Cookie consent">
|
||
<p className="text-sm text-neutral-700 dark:text-neutral-200">
|
||
We use cookies to improve your experience. By continuing, you agree to our{' '}
|
||
<a href="#/legal" className="underline focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 rounded">cookie policy</a>.
|
||
</p>
|
||
<div className="mt-3 flex gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => persistAndClose('accept')}
|
||
className="btn-primary text-xs px-4 py-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
|
||
aria-label="Accept cookies"
|
||
>
|
||
Accept
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => persistAndClose('decline')}
|
||
className="btn-secondary text-xs px-4 py-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
|
||
aria-label="Decline non-essential cookies"
|
||
>
|
||
Decline
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/* ===================== Main App Component ===================== */
|
||
export default function App() {
|
||
return (
|
||
<MotionConfig reducedMotion="user">
|
||
<LazyMotion features={domAnimation} strict>
|
||
<LanguageProvider>
|
||
<NotificationProvider>
|
||
<AuthProvider>
|
||
<AppContent />
|
||
<PWAInstallPrompt />
|
||
</AuthProvider>
|
||
</NotificationProvider>
|
||
</LanguageProvider>
|
||
</LazyMotion>
|
||
</MotionConfig>
|
||
)
|
||
}
|
||
|
||
function AppContent() {
|
||
const [currentPath, setCurrentPath] = useState(window.location.hash.slice(1) || '/')
|
||
const [darkMode, setDarkMode] = useState(() => {
|
||
if (typeof window !== 'undefined') {
|
||
return localStorage.getItem('darkMode') === 'true' ||
|
||
(!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||
}
|
||
return false
|
||
})
|
||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||
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)
|
||
return () => window.removeEventListener('hashchange', handleHashChange)
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
document.documentElement.classList.toggle('dark', darkMode)
|
||
localStorage.setItem('darkMode', darkMode.toString())
|
||
}, [darkMode])
|
||
|
||
// Online/Offline notifications
|
||
useEffect(() => {
|
||
const handleOnline = () => {
|
||
addNotification({
|
||
type: 'success',
|
||
title: 'Back Online',
|
||
message: 'Your internet connection has been restored'
|
||
})
|
||
}
|
||
|
||
const handleOffline = () => {
|
||
addNotification({
|
||
type: 'warning',
|
||
title: 'Connection Lost',
|
||
message: "You're currently offline. Some features may be limited."
|
||
})
|
||
}
|
||
|
||
if (!isOnline) {
|
||
handleOffline()
|
||
}
|
||
|
||
window.addEventListener('online', handleOnline)
|
||
window.addEventListener('offline', handleOffline)
|
||
|
||
return () => {
|
||
window.removeEventListener('online', handleOnline)
|
||
window.removeEventListener('offline', handleOffline)
|
||
}
|
||
}, [isOnline, addNotification])
|
||
|
||
useEffect(() => {
|
||
document.title =
|
||
currentPath === '/'
|
||
? `${SITE.name} — ${SITE.brandMessage}`
|
||
: `Miracles in Motion — ${currentPath.replace('/', '').replace(/-/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase())}`
|
||
}, [currentPath])
|
||
|
||
const renderPage = () => {
|
||
switch (currentPath) {
|
||
case '/':
|
||
return <LazyRoute label="Loading home…"><HomePage /></LazyRoute>
|
||
case '/donate':
|
||
return <LazyRoute label="Loading donate…"><DonatePage /></LazyRoute>
|
||
case '/volunteers':
|
||
return <VolunteerPage />
|
||
case '/sponsors':
|
||
return <SponsorsPage />
|
||
case '/stories':
|
||
return <StoriesPage />
|
||
case '/testimonies':
|
||
return <TestimoniesPage />
|
||
case '/legal':
|
||
return <LegalPage />
|
||
case '/brand':
|
||
return <BrandAssetsPage />
|
||
case '/about':
|
||
return <AboutPage />
|
||
case '/mission':
|
||
return <MissionPage />
|
||
case '/what-we-do':
|
||
return <WhatWeDoPage />
|
||
case '/events':
|
||
return <EventsPage />
|
||
case '/contact':
|
||
return <ContactPage />
|
||
case '/impact':
|
||
return <ImpactReportPage />
|
||
case '/request-assistance':
|
||
return <LazyRoute label="Loading assistance form…"><AssistanceRequestPage /></LazyRoute>
|
||
case '/portals':
|
||
return <PortalsPage />
|
||
case '/admin-portal':
|
||
return <AdminPortalPage />
|
||
case '/volunteer-portal':
|
||
return <VolunteerPortalPage />
|
||
case '/resource-portal':
|
||
return <ResourcePortalPage />
|
||
case '/analytics':
|
||
return <AnalyticsDashboard />
|
||
case '/ai-portal':
|
||
return <LazyPortal><AIPortalPage /></LazyPortal>
|
||
case '/advanced-analytics':
|
||
return <LazyPortal><AdvancedAnalyticsPage /></LazyPortal>
|
||
case '/mobile-volunteer':
|
||
return <LazyPortal><MobileVolunteerPage /></LazyPortal>
|
||
case '/staff-training':
|
||
return <LazyPortal><StaffTrainingPage /></LazyPortal>
|
||
default:
|
||
return <NotFoundPage />
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className={`min-h-screen transition-colors duration-300 bg-neutral-50 text-neutral-900 antialiased selection:bg-primary-200/60 dark:bg-neutral-950 dark:text-neutral-50 ${darkMode ? 'dark' : ''}`}>
|
||
<BackgroundDecor />
|
||
<SkipToContent />
|
||
|
||
{/* Offline Indicator */}
|
||
{!isOnline && (
|
||
<div className="bg-yellow-500 text-white text-center py-2 text-sm font-medium">
|
||
<WifiOff className="inline h-4 w-4 mr-2" />
|
||
You're currently offline
|
||
</div>
|
||
)}
|
||
|
||
<SiteHeader>
|
||
<Navigation
|
||
darkMode={darkMode}
|
||
setDarkMode={setDarkMode}
|
||
mobileMenuOpen={isMobileMenuOpen}
|
||
setMobileMenuOpen={setIsMobileMenuOpen}
|
||
/>
|
||
</SiteHeader>
|
||
|
||
<main id="content" className="relative flex-1">
|
||
{renderPage()}
|
||
</main>
|
||
|
||
<Footer />
|
||
{![
|
||
'/admin-portal',
|
||
'/volunteer-portal',
|
||
'/resource-portal',
|
||
'/analytics',
|
||
'/ai-portal',
|
||
'/advanced-analytics',
|
||
'/mobile-volunteer',
|
||
'/staff-training'
|
||
].includes(currentPath) && (
|
||
<Suspense fallback={null}>
|
||
<MimChatAgent currentPath={currentPath} />
|
||
</Suspense>
|
||
)}
|
||
<StickyDonate />
|
||
<CookieBanner />
|
||
</div>
|
||
)
|
||
}
|