Portals link in nav/footer/mobile; foundation-aligned request assistance, donate, legal, and SEO; expanded sitemap; families-focused impact labels; fix document.title hook placement. Co-authored-by: Cursor <[email protected]>
4581 lines
198 KiB
TypeScript
4581 lines
198 KiB
TypeScript
import React, { useState, useEffect, useRef, useContext, createContext } from 'react'
|
||
import { motion, useTransform, useScroll, AnimatePresence } from 'framer-motion'
|
||
import {
|
||
ArrowRight,
|
||
Backpack,
|
||
CheckCircle2,
|
||
Globe,
|
||
Heart,
|
||
Mail,
|
||
MapPin,
|
||
Phone,
|
||
Shirt,
|
||
Sparkles,
|
||
Star,
|
||
Users,
|
||
Building2,
|
||
BookOpenText,
|
||
Quote,
|
||
FileText,
|
||
X,
|
||
ExternalLink,
|
||
DollarSign,
|
||
Shield,
|
||
Award,
|
||
Settings,
|
||
UserCheck,
|
||
School,
|
||
ClipboardList,
|
||
Calendar,
|
||
FileCheck,
|
||
AlertCircle,
|
||
Home as HomeIcon,
|
||
CreditCard,
|
||
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
|
||
import AIAssistancePortal from './components/AIAssistancePortal'
|
||
|
||
// Phase 3B: Enterprise Components
|
||
import AdvancedAnalyticsDashboard from './components/AdvancedAnalyticsDashboard'
|
||
import MobileVolunteerApp from './components/MobileVolunteerApp'
|
||
import StaffTrainingDashboard from './components/StaffTrainingDashboard'
|
||
import MimChatAgent from './components/MimChatAgent'
|
||
|
||
// Phase 4: Extracted Components
|
||
import { Navigation } from './components/Navigation'
|
||
import { Footer } from './components/Footer'
|
||
import { BrandAssetsPage } from './pages/BrandPage'
|
||
import {
|
||
AboutPage,
|
||
ContactPage,
|
||
EventsPage,
|
||
MissionPage,
|
||
StoriesIntroBlock,
|
||
WhatWeDoPage,
|
||
} from './pages/foundation'
|
||
import { AwarenessCampaigns } from './components/home/AwarenessCampaigns'
|
||
import { CommunityGallery } from './components/home/CommunityGallery'
|
||
import { WhoWeServe } from './components/home/WhoWeServe'
|
||
import { COMMUNITY_PHOTOS, HOME_TESTIMONIAL, HOW_IT_WORKS, SITE, WHAT_WE_DO } from './content/siteContent'
|
||
|
||
/**
|
||
* 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 ===================== */
|
||
|
||
// Notification System Context
|
||
const NotificationContext = createContext<NotificationContextType | null>(null)
|
||
|
||
function NotificationProvider({ children }: { children: React.ReactNode }) {
|
||
const [notifications, setNotifications] = useState<Notification[]>([])
|
||
|
||
const addNotification = (notif: Omit<Notification, 'id' | 'timestamp' | 'read'>) => {
|
||
const newNotification: Notification = {
|
||
...notif,
|
||
id: Math.random().toString(36),
|
||
timestamp: new Date(),
|
||
read: false
|
||
}
|
||
setNotifications(prev => [newNotification, ...prev])
|
||
|
||
// Auto-remove success notifications after 5 seconds
|
||
if (notif.type === 'success') {
|
||
setTimeout(() => {
|
||
setNotifications(prev => prev.filter(n => n.id !== newNotification.id))
|
||
}, 5000)
|
||
}
|
||
}
|
||
|
||
const markAsRead = (id: string) => {
|
||
setNotifications(prev =>
|
||
prev.map(n => n.id === id ? { ...n, read: true } : n)
|
||
)
|
||
}
|
||
|
||
const clearAll = () => setNotifications([])
|
||
const unreadCount = notifications.filter(n => !n.read).length
|
||
|
||
return (
|
||
<NotificationContext.Provider value={{
|
||
notifications,
|
||
addNotification,
|
||
markAsRead,
|
||
clearAll,
|
||
unreadCount
|
||
}}>
|
||
{children}
|
||
</NotificationContext.Provider>
|
||
)
|
||
}
|
||
|
||
function useNotifications() {
|
||
const context = useContext(NotificationContext)
|
||
if (!context) throw new Error('useNotifications must be used within NotificationProvider')
|
||
return context
|
||
}
|
||
|
||
// Language/Internationalization Context
|
||
const LanguageContext = createContext<{
|
||
currentLanguage: Language
|
||
languages: Language[]
|
||
changeLanguage: (code: string) => void
|
||
t: (key: string) => string
|
||
} | null>(null)
|
||
|
||
const languages: Language[] = [
|
||
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇺🇸' },
|
||
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
||
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' }
|
||
]
|
||
|
||
const translations: Translations = {
|
||
'nav.home': { en: 'Home', es: 'Inicio', fr: 'Accueil' },
|
||
'nav.donate': { en: 'Donate', es: 'Donar', fr: 'Faire un don' },
|
||
'nav.volunteer': { en: 'Volunteer', es: 'Voluntario', fr: 'Bénévole' },
|
||
'hero.title': {
|
||
en: 'Restoring hope through compassion, community, and faith',
|
||
es: 'Equipando a los niños para el éxito: útiles escolares, ropa y más',
|
||
fr: 'Équiper les enfants pour réussir — fournitures scolaires, vêtements et plus'
|
||
},
|
||
'donate.title': { en: 'Donate Now', es: 'Donar Ahora', fr: 'Faire un don maintenant' },
|
||
'impact.families': { en: 'Families Supported', es: 'Familias Apoyadas', fr: 'Familles Soutenues' }
|
||
}
|
||
|
||
function LanguageProvider({ children }: { children: React.ReactNode }) {
|
||
const [currentLanguage, setCurrentLanguage] = useState<Language>(() => {
|
||
const saved = localStorage.getItem('mim_language')
|
||
if (saved) {
|
||
return languages.find(l => l.code === saved) || languages[0]
|
||
}
|
||
return languages[0]
|
||
})
|
||
|
||
const changeLanguage = (code: string) => {
|
||
const language = languages.find(l => l.code === code)
|
||
if (language) {
|
||
setCurrentLanguage(language)
|
||
localStorage.setItem('mim_language', code)
|
||
trackEvent('language_changed', { from: currentLanguage.code, to: code })
|
||
}
|
||
}
|
||
|
||
const t = (key: string): string => {
|
||
return translations[key]?.[currentLanguage.code] || key
|
||
}
|
||
|
||
return (
|
||
<LanguageContext.Provider value={{ currentLanguage, languages, changeLanguage, t }}>
|
||
{children}
|
||
</LanguageContext.Provider>
|
||
)
|
||
}
|
||
|
||
function useLanguage() {
|
||
const context = useContext(LanguageContext)
|
||
if (!context) throw new Error('useLanguage must be used within LanguageProvider')
|
||
return context
|
||
}
|
||
|
||
/* ===================== Authentication Context ===================== */
|
||
const AuthContext = createContext<AuthContextType | null>(null)
|
||
|
||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||
const [user, setUser] = useState<AuthUser | null>(null)
|
||
const [isLoading, setIsLoading] = useState(false)
|
||
|
||
const login = async (email: string, password: string): Promise<boolean> => {
|
||
setIsLoading(true)
|
||
try {
|
||
// Simulate authentication - in production, this would call your API
|
||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||
|
||
// Simple demo validation (in production, validate against secure backend)
|
||
if (password.length < 3) {
|
||
return false
|
||
}
|
||
|
||
// Mock user data based on email domain
|
||
const mockUser: AuthUser = {
|
||
id: Math.random().toString(36),
|
||
email,
|
||
role: email.includes('admin') ? 'admin' : email.includes('volunteer') ? 'volunteer' : 'resource',
|
||
name: email.split('@')[0].replace('.', ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
||
lastLogin: new Date(),
|
||
permissions: email.includes('admin') ? ['read', 'write', 'delete', 'manage'] : ['read', 'write']
|
||
}
|
||
|
||
setUser(mockUser)
|
||
localStorage.setItem('mim_user', JSON.stringify(mockUser))
|
||
return true
|
||
} catch (error) {
|
||
console.error('Login failed:', error)
|
||
return false
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
const logout = () => {
|
||
setUser(null)
|
||
localStorage.removeItem('mim_user')
|
||
}
|
||
|
||
// Restore user session on app load
|
||
useEffect(() => {
|
||
const savedUser = localStorage.getItem('mim_user')
|
||
if (savedUser) {
|
||
try {
|
||
setUser(JSON.parse(savedUser))
|
||
} catch (error) {
|
||
localStorage.removeItem('mim_user')
|
||
}
|
||
}
|
||
}, [])
|
||
|
||
return (
|
||
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
|
||
{children}
|
||
</AuthContext.Provider>
|
||
)
|
||
}
|
||
|
||
function useAuth() {
|
||
const context = useContext(AuthContext)
|
||
if (!context) {
|
||
throw new Error('useAuth must be used within AuthProvider')
|
||
}
|
||
return context
|
||
}
|
||
|
||
|
||
|
||
/* ===================== Enhanced Impact Calculator ===================== */
|
||
function calculateDonationImpact(amount: number): ImpactCalculation {
|
||
const families = Math.floor(amount / 50)
|
||
const students = families // legacy display fields — shown as families served
|
||
const backpacks = Math.floor(amount / 40)
|
||
const clothing = Math.floor(amount / 45)
|
||
const emergency = Math.floor(amount / 75)
|
||
|
||
return {
|
||
students,
|
||
families,
|
||
backpacks,
|
||
clothing,
|
||
emergency,
|
||
annual: {
|
||
students: Math.floor((amount * 12) / 50),
|
||
families: Math.floor((amount * 12) / 50),
|
||
totalImpact: `${Math.floor((amount * 12) / 50)} families supported annually`,
|
||
},
|
||
}
|
||
}
|
||
|
||
/* ===================== 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)
|
||
}
|
||
|
||
/* ===================== Phase 2: Payment Integration System ===================== */
|
||
const paymentMethods: PaymentMethod[] = [
|
||
{
|
||
id: 'stripe',
|
||
type: 'card',
|
||
name: 'Credit/Debit Card',
|
||
icon: '💳',
|
||
description: 'Secure payment via Stripe',
|
||
processingFee: 2.9
|
||
},
|
||
{
|
||
id: 'paypal',
|
||
type: 'paypal',
|
||
name: 'PayPal',
|
||
icon: '🌐',
|
||
description: 'Pay with PayPal account',
|
||
processingFee: 2.9
|
||
},
|
||
{
|
||
id: 'venmo',
|
||
type: 'venmo',
|
||
name: 'Venmo',
|
||
icon: '📱',
|
||
description: 'Quick mobile payment',
|
||
processingFee: 1.9
|
||
},
|
||
{
|
||
id: 'bank',
|
||
type: 'bank',
|
||
name: 'Bank Transfer',
|
||
icon: '🏦',
|
||
description: 'Direct bank transfer (ACH)',
|
||
processingFee: 0.8
|
||
}
|
||
]
|
||
|
||
class PaymentProcessor {
|
||
static async processPayment(amount: number, method: PaymentMethod, _donorInfo: any): Promise<{
|
||
success: boolean
|
||
transactionId?: string
|
||
error?: string
|
||
}> {
|
||
// Simulate payment processing
|
||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||
|
||
// Simulate different success rates by payment method
|
||
const successRate = method.type === 'bank' ? 0.95 : method.type === 'card' ? 0.92 : 0.98
|
||
const success = Math.random() < successRate
|
||
|
||
if (success) {
|
||
const transactionId = `txn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||
|
||
// Track successful payment
|
||
trackEvent('donation_completed', {
|
||
amount,
|
||
method: method.id,
|
||
transactionId,
|
||
processingFee: amount * (method.processingFee / 100)
|
||
})
|
||
|
||
return { success: true, transactionId }
|
||
} else {
|
||
const errors = [
|
||
'Payment declined by bank',
|
||
'Insufficient funds',
|
||
'Invalid card information',
|
||
'Network timeout - please try again'
|
||
]
|
||
return { success: false, error: errors[Math.floor(Math.random() * errors.length)] }
|
||
}
|
||
}
|
||
|
||
static calculateFees(amount: number, method: PaymentMethod) {
|
||
const fee = amount * (method.processingFee / 100)
|
||
return {
|
||
fee: Math.round(fee * 100) / 100,
|
||
net: Math.round((amount - fee) * 100) / 100,
|
||
feePercentage: method.processingFee
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ===================== Advanced Analytics System ===================== */
|
||
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 || '/og-image.jpg')
|
||
updateOGTag('og:type', 'website')
|
||
}, [title, description, image])
|
||
|
||
return null
|
||
}
|
||
|
||
/* ===================== Types ===================== */
|
||
interface IconProps {
|
||
className?: string
|
||
}
|
||
|
||
interface AuthUser {
|
||
id: string
|
||
email: string
|
||
role: 'admin' | 'volunteer' | 'resource'
|
||
name: string
|
||
lastLogin: Date
|
||
permissions: string[]
|
||
}
|
||
|
||
interface AuthContextType {
|
||
user: AuthUser | null
|
||
login: (email: string, password: string) => Promise<boolean>
|
||
logout: () => void
|
||
isLoading: boolean
|
||
}
|
||
|
||
interface ImpactCalculation {
|
||
students: number
|
||
families: number
|
||
backpacks: number
|
||
clothing: number
|
||
emergency: number
|
||
annual: {
|
||
students: number
|
||
families: number
|
||
totalImpact: string
|
||
}
|
||
}
|
||
|
||
// Phase 2 Interfaces
|
||
interface Notification {
|
||
id: string
|
||
type: 'success' | 'info' | 'warning' | 'error'
|
||
title: string
|
||
message: string
|
||
timestamp: Date
|
||
read: boolean
|
||
actions?: { label: string; action: () => void }[]
|
||
}
|
||
|
||
interface NotificationContextType {
|
||
notifications: Notification[]
|
||
addNotification: (notification: Omit<Notification, 'id' | 'timestamp' | 'read'>) => void
|
||
markAsRead: (id: string) => void
|
||
clearAll: () => void
|
||
unreadCount: number
|
||
}
|
||
|
||
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 PaymentMethod {
|
||
id: string
|
||
type: 'card' | 'paypal' | 'venmo' | 'bank'
|
||
name: string
|
||
icon: string
|
||
description: string
|
||
processingFee: number
|
||
}
|
||
|
||
interface Language {
|
||
code: string
|
||
name: string
|
||
nativeName: string
|
||
flag: string
|
||
}
|
||
|
||
interface Translations {
|
||
[key: string]: {
|
||
[langCode: string]: string
|
||
}
|
||
}
|
||
|
||
interface FeatureCardProps {
|
||
icon: React.ComponentType<IconProps>
|
||
title: string
|
||
body: string
|
||
}
|
||
|
||
interface SectionHeaderProps {
|
||
eyebrow?: string
|
||
title: string
|
||
subtitle?: string
|
||
}
|
||
|
||
interface CalloutProps {
|
||
title: string
|
||
body: string
|
||
href: string
|
||
accent: string
|
||
icon: React.ComponentType<IconProps>
|
||
index?: number
|
||
}
|
||
|
||
interface PageShellProps {
|
||
title: string
|
||
icon: React.ComponentType<IconProps>
|
||
eyebrow?: string
|
||
children: React.ReactNode
|
||
cta?: React.ReactNode
|
||
}
|
||
|
||
interface CardProps {
|
||
title: string
|
||
icon: React.ComponentType<IconProps>
|
||
children: React.ReactNode
|
||
}
|
||
|
||
interface PolicySectionProps {
|
||
id: string
|
||
title: string
|
||
children: React.ReactNode
|
||
}
|
||
|
||
/* ===================== 3D Parallax Components ===================== */
|
||
|
||
// 3D Parallax Container Component
|
||
interface ParallaxContainerProps {
|
||
children: React.ReactNode
|
||
depth?: number
|
||
className?: string
|
||
}
|
||
|
||
function ParallaxContainer({ children, depth = 1, className = '' }: ParallaxContainerProps) {
|
||
const ref = useRef<HTMLDivElement>(null)
|
||
const { scrollYProgress } = useScroll({
|
||
target: ref,
|
||
offset: ['start end', 'end start']
|
||
})
|
||
|
||
const y = useTransform(scrollYProgress, [0, 1], [0, -50 * depth])
|
||
const rotateX = useTransform(scrollYProgress, [0, 1], [0, 5 * depth])
|
||
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 1.1])
|
||
|
||
return (
|
||
<motion.div
|
||
ref={ref}
|
||
style={{ y, rotateX, scale }}
|
||
className={`transform-gpu ${className}`}
|
||
initial={{ opacity: 0 }}
|
||
whileInView={{ opacity: 1 }}
|
||
viewport={{ once: true, margin: '-100px' }}
|
||
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||
>
|
||
{children}
|
||
</motion.div>
|
||
)
|
||
}
|
||
|
||
// Enhanced 3D Floating Particles Background
|
||
function FloatingParticles() {
|
||
const particles = Array.from({ length: 20 }, (_, i) => i)
|
||
|
||
return (
|
||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||
{particles.map((i) => {
|
||
const size = Math.random() * 2 + 0.5
|
||
const depth = Math.random() * 3 + 1
|
||
return (
|
||
<motion.div
|
||
key={i}
|
||
className="absolute rounded-full bg-gradient-to-br from-primary-300/8 to-secondary-300/8 backdrop-blur-sm"
|
||
style={{
|
||
width: `${size}px`,
|
||
height: `${size}px`,
|
||
left: `${Math.random() * 100}%`,
|
||
top: `${Math.random() * 100}%`,
|
||
transformStyle: 'preserve-3d'
|
||
}}
|
||
animate={{
|
||
y: [-25, -100, -25],
|
||
x: [-10, 10, -10],
|
||
z: [0, depth * 15, 0],
|
||
opacity: [0.2, 0.6, 0.2],
|
||
scale: [0.9, 1.1, 0.9]
|
||
}}
|
||
transition={{
|
||
duration: Math.random() * 18 + 10,
|
||
repeat: Infinity,
|
||
delay: Math.random() * 4,
|
||
ease: "easeInOut"
|
||
}}
|
||
/>
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 3D Geometric Shapes Component
|
||
function Floating3DShapes() {
|
||
const shapes = [
|
||
{ type: 'cube', color: 'from-primary-500/15 to-primary-600/15', size: 12 },
|
||
{ type: 'sphere', color: 'from-secondary-500/15 to-secondary-600/15', size: 16 },
|
||
{ type: 'pyramid', color: 'from-primary-400/15 to-secondary-500/15', size: 14 },
|
||
]
|
||
|
||
return (
|
||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||
{shapes.map((shape, i) => Array.from({ length: 3 }, (_, j) => (
|
||
<motion.div
|
||
key={`${shape.type}-${i}-${j}`}
|
||
className={`absolute bg-gradient-to-br ${shape.color} backdrop-blur-sm ${
|
||
shape.type === 'cube' ? 'rounded-sm' :
|
||
shape.type === 'sphere' ? 'rounded-full' : 'rounded-sm'
|
||
}`}
|
||
style={{
|
||
width: `${shape.size}px`,
|
||
height: `${shape.size}px`,
|
||
left: `${(i * 30 + j * 15 + 10) % 90}%`,
|
||
top: `${(i * 25 + j * 20 + 15) % 80}%`,
|
||
transformStyle: 'preserve-3d',
|
||
filter: 'blur(0.5px)'
|
||
}}
|
||
animate={{
|
||
rotateX: [0, 360],
|
||
rotateY: [0, 360],
|
||
rotateZ: shape.type === 'pyramid' ? [0, 180, 360] : 0,
|
||
z: [0, 60, 0],
|
||
y: [-20, 20, -20]
|
||
}}
|
||
transition={{
|
||
duration: 20 + i * 5,
|
||
repeat: Infinity,
|
||
delay: j * 2,
|
||
ease: "linear"
|
||
}}
|
||
/>
|
||
))
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/* ===================== Router ===================== */
|
||
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 ===================== */
|
||
function HomePage() {
|
||
return (
|
||
<>
|
||
<Hero />
|
||
<WhoWeServe />
|
||
<AwarenessCampaigns />
|
||
<PartnerMarquee />
|
||
<Programs />
|
||
<CommunityGallery />
|
||
<Impact />
|
||
<HowItWorks />
|
||
<Testimonial />
|
||
<GetInvolved />
|
||
<CTA />
|
||
</>
|
||
)
|
||
}
|
||
|
||
function Hero() {
|
||
const containerRef = useRef<HTMLDivElement>(null)
|
||
const { scrollYProgress } = useScroll({
|
||
target: containerRef,
|
||
offset: ['start start', 'end start']
|
||
})
|
||
|
||
const backgroundY = useTransform(scrollYProgress, [0, 1], ['0%', '30%'])
|
||
|
||
return (
|
||
<section ref={containerRef} className="hero-section relative isolate overflow-hidden">
|
||
{/* Enhanced 3D Background Layers */}
|
||
<motion.div
|
||
className="absolute inset-0 bg-gradient-to-br from-[var(--mim-cream)] via-white to-secondary-50/40 dark:from-gray-900/30 dark:via-gray-800/30 dark:to-gray-900/30"
|
||
style={{ y: backgroundY }}
|
||
>
|
||
<FloatingParticles />
|
||
|
||
{/* Animated depth gradient */}
|
||
<motion.div
|
||
className="absolute inset-0 opacity-20"
|
||
animate={{
|
||
background: [
|
||
"radial-gradient(800px 400px at 20% 30%, rgba(147, 51, 234, 0.1), transparent)",
|
||
"radial-gradient(600px 300px at 80% 60%, rgba(59, 130, 246, 0.1), transparent)",
|
||
"radial-gradient(700px 350px at 50% 20%, rgba(147, 51, 234, 0.1), transparent)"
|
||
]
|
||
}}
|
||
transition={{ duration: 15, repeat: Infinity, ease: "easeInOut" }}
|
||
/>
|
||
</motion.div>
|
||
|
||
<motion.div
|
||
className="absolute inset-0"
|
||
style={{ y: useTransform(scrollYProgress, [0, 1], ['0%', '30%']) }}
|
||
>
|
||
<Floating3DShapes />
|
||
</motion.div>
|
||
|
||
<motion.div
|
||
className="mx-auto grid max-w-7xl items-start gap-10 px-4 pb-14 pt-6 sm:px-6 sm:pt-8 lg:grid-cols-12 lg:gap-10 lg:px-8 lg:pb-16 lg:pt-10 relative z-10"
|
||
>
|
||
<div className="lg:col-span-7">
|
||
<ParallaxContainer depth={0.3}>
|
||
<p className="text-sm font-medium tracking-wide text-secondary-700 dark:text-secondary-300">
|
||
{SITE.tagline}
|
||
</p>
|
||
<motion.h1
|
||
initial={{ opacity: 0, y: 12 }}
|
||
whileInView={{ opacity: 1, y: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.6 }}
|
||
className="mt-3 text-balance font-serif text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl transform-gpu"
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
{SITE.brandMessage}
|
||
</motion.h1>
|
||
<p className="mt-5 max-w-xl text-lg leading-7 text-neutral-700 dark:text-neutral-300">
|
||
Bringing hope, compassion, and support to individuals and families facing life’s most difficult moments — through faith, community outreach, and compassionate care in Los Angeles County.
|
||
</p>
|
||
<div className="mt-8 flex flex-wrap items-center gap-3">
|
||
<Magnetic>
|
||
<a href="#/donate" className="btn-primary">
|
||
<Heart className="mr-2 h-4 w-4" /> Donate now
|
||
</a>
|
||
</Magnetic>
|
||
<a href="#/volunteers" className="btn-secondary">
|
||
Volunteer <ArrowRight className="ml-1 h-4 w-4" />
|
||
</a>
|
||
<a href="#/about" className="rounded-full border border-transparent px-4 py-2 text-sm text-neutral-700 underline-offset-4 hover:underline dark:text-neutral-300">
|
||
About us
|
||
</a>
|
||
</div>
|
||
<ul className="mt-8 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||
<li className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-secondary-600"/> {SITE.legalStatus}</li>
|
||
<li className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-secondary-600"/> EIN {SITE.ein}</li>
|
||
<li className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-secondary-600"/> Los Angeles County</li>
|
||
</ul>
|
||
</ParallaxContainer>
|
||
</div>
|
||
|
||
<div className="lg:col-span-5">
|
||
<ParallaxContainer depth={0.5} className="relative">
|
||
<HeroShowcase />
|
||
</ParallaxContainer>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* 3D Scroll Indicator */}
|
||
<motion.div
|
||
className="absolute bottom-12 left-1/2 -translate-x-1/2 hidden lg:block z-20"
|
||
animate={{
|
||
y: [0, 8, 0],
|
||
rotateX: [0, 10, 0]
|
||
}}
|
||
transition={{ duration: 2.5, repeat: Infinity, ease: "easeInOut" }}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<div className="flex flex-col items-center gap-2 text-neutral-500 dark:text-neutral-400">
|
||
<span className="text-xs font-medium opacity-75">Scroll to explore</span>
|
||
<motion.div
|
||
animate={{ y: [0, 4, 0] }}
|
||
transition={{ duration: 1.8, repeat: Infinity, ease: "easeInOut" }}
|
||
>
|
||
<ArrowRight className="h-4 w-4 rotate-90 opacity-60" />
|
||
</motion.div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Enhanced 3D Icon Elements with More Visibility */}
|
||
<div className="absolute inset-0 pointer-events-none z-0">
|
||
{/* Top layer - subtle and non-interfering */}
|
||
<motion.div
|
||
className="absolute top-1/4 left-[5%] text-primary-300/40 dark:text-primary-600/40 hidden xl:block"
|
||
animate={{
|
||
y: [-12, 12, -12],
|
||
rotateY: [0, 360],
|
||
z: [0, 30, 0],
|
||
scale: [0.9, 1.1, 0.9]
|
||
}}
|
||
transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }}
|
||
style={{
|
||
transformStyle: 'preserve-3d',
|
||
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.05))'
|
||
}}
|
||
>
|
||
<Heart className="h-6 w-6" />
|
||
</motion.div>
|
||
|
||
{/* Additional floating heart */}
|
||
<motion.div
|
||
className="absolute top-1/3 right-1/4 text-rose-300/70 dark:text-rose-400/70 hidden xl:block"
|
||
animate={{
|
||
y: [10, -10, 10],
|
||
rotateZ: [-15, 15, -15],
|
||
scale: [1, 1.3, 1]
|
||
}}
|
||
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut", delay: 1 }}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<Heart className="h-5 w-5" />
|
||
</motion.div>
|
||
|
||
{/* Right side - subtle sparkles */}
|
||
<motion.div
|
||
className="absolute top-1/3 right-[8%] text-secondary-300/40 dark:text-secondary-600/40 hidden xl:block"
|
||
animate={{
|
||
y: [15, -15, 15],
|
||
rotateX: [0, 180, 360],
|
||
z: [0, 25, 0],
|
||
scale: [0.95, 1.2, 0.95]
|
||
}}
|
||
transition={{ duration: 7, repeat: Infinity, ease: "easeInOut", delay: 1.5 }}
|
||
style={{
|
||
transformStyle: 'preserve-3d',
|
||
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.05))'
|
||
}}
|
||
>
|
||
<Sparkles className="h-5 w-5" />
|
||
</motion.div>
|
||
|
||
{/* Additional floating sparkle */}
|
||
<motion.div
|
||
className="absolute top-1/2 right-[20%] text-yellow-300/60 dark:text-yellow-400/60 hidden lg:block"
|
||
animate={{
|
||
rotate: [0, 360],
|
||
y: [-8, 8, -8],
|
||
scale: [0.7, 1.1, 0.7]
|
||
}}
|
||
transition={{ duration: 8, repeat: Infinity, ease: "easeInOut", delay: 3 }}
|
||
>
|
||
<Sparkles className="h-4 w-4" />
|
||
</motion.div>
|
||
|
||
{/* Bottom area - enhanced users icon */}
|
||
<motion.div
|
||
className="absolute bottom-1/4 left-[15%] text-primary-400/70 dark:text-primary-500/70 hidden lg:block"
|
||
animate={{
|
||
y: [-12, 12, -12],
|
||
rotateZ: [0, 180, 360],
|
||
z: [0, 45, 0],
|
||
scale: [0.8, 1.2, 0.8]
|
||
}}
|
||
transition={{ duration: 7, repeat: Infinity, ease: "easeInOut", delay: 3 }}
|
||
style={{
|
||
transformStyle: 'preserve-3d',
|
||
filter: 'drop-shadow(0 3px 6px rgba(0,0,0,0.1))'
|
||
}}
|
||
>
|
||
<Users className="h-6 w-6" />
|
||
</motion.div>
|
||
|
||
{/* Additional community icon */}
|
||
<motion.div
|
||
className="absolute bottom-1/3 right-[8%] text-blue-300/60 dark:text-blue-400/60 hidden xl:block"
|
||
animate={{
|
||
x: [-5, 5, -5],
|
||
y: [8, -8, 8],
|
||
rotateY: [0, 180, 360]
|
||
}}
|
||
transition={{ duration: 9, repeat: Infinity, ease: "easeInOut", delay: 4.5 }}
|
||
>
|
||
<Users className="h-4 w-4" />
|
||
</motion.div>
|
||
|
||
{/* Additional subtle elements */}
|
||
<motion.div
|
||
className="absolute top-1/2 right-[8%] text-primary-100/40 dark:text-primary-800/40 hidden xl:block"
|
||
animate={{
|
||
rotate: [0, 360],
|
||
scale: [0.8, 1.2, 0.8],
|
||
z: [0, 20, 0]
|
||
}}
|
||
transition={{ duration: 12, repeat: Infinity, ease: "easeInOut", delay: 4 }}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<Star className="h-4 w-4" />
|
||
</motion.div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function HeroShowcase() {
|
||
const featured = COMMUNITY_PHOTOS.slice(0, 4)
|
||
return (
|
||
<div className="relative mx-auto max-w-sm lg:max-w-md xl:max-w-lg">
|
||
<div className="absolute -inset-6 -z-10 rounded-3xl bg-gradient-to-tr from-primary-500/15 via-secondary-500/15 to-secondary-600/15 blur-2xl" />
|
||
<div className="grid grid-cols-2 gap-3 sm:gap-4">
|
||
{featured.map((photo, index) => (
|
||
<img
|
||
key={photo.src}
|
||
src={photo.src}
|
||
alt={photo.alt}
|
||
width={600}
|
||
height={800}
|
||
fetchPriority={index === 0 ? 'high' : 'auto'}
|
||
loading={index === 0 ? 'eager' : 'lazy'}
|
||
decoding="async"
|
||
className={`rounded-2xl border border-white/60 object-cover shadow-lg dark:border-white/10 ${
|
||
index === 0 ? 'col-span-2 aspect-[16/10] w-full' : 'aspect-[4/5] w-full'
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function PartnerMarquee() {
|
||
const partners =
|
||
'Los Angeles County • Faith communities • Outreach volunteers • Crisis & fire relief • Domestic violence advocacy • Youth & family support • Resource navigation'
|
||
return (
|
||
<section aria-label="Community partners" className="relative">
|
||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||
<div className="card">
|
||
<div className="flex items-center gap-2 px-2 text-xs uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
|
||
<Sparkles className="h-4 w-4" /> Serving our community together
|
||
</div>
|
||
<div className="mt-2 overflow-hidden">
|
||
<div className="animate-marquee whitespace-nowrap py-2 text-neutral-600 dark:text-neutral-300">
|
||
{Array.from({ length: 2 }).map((_, i) => (
|
||
<span key={i} className="mx-6 inline-flex items-center gap-2 text-sm opacity-80">
|
||
<span className="h-2 w-2 rounded-full bg-gradient-to-r from-primary-500 to-secondary-600" />
|
||
{partners}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function Programs() {
|
||
const icons = [Users, Heart, HomeIcon, Globe, Shield, Star] as const
|
||
const items = WHAT_WE_DO.slice(0, 3).map((p, idx) => ({
|
||
icon: icons[idx] ?? Heart,
|
||
title: p.title,
|
||
body: p.body,
|
||
}))
|
||
|
||
return (
|
||
<section id="programs" className="relative py-20 overflow-hidden">
|
||
{/* Background 3D Elements */}
|
||
<div className="absolute inset-0 pointer-events-none">
|
||
<motion.div
|
||
className="absolute top-10 right-10 w-32 h-32 bg-gradient-to-br from-primary-100/20 to-secondary-100/20 rounded-full blur-xl"
|
||
animate={{
|
||
y: [-20, 20, -20],
|
||
x: [-10, 10, -10],
|
||
scale: [0.8, 1.1, 0.8]
|
||
}}
|
||
transition={{ duration: 15, repeat: Infinity, ease: "easeInOut" }}
|
||
/>
|
||
<motion.div
|
||
className="absolute bottom-20 left-10 w-24 h-24 bg-gradient-to-br from-secondary-100/20 to-primary-100/20 rounded-full blur-xl"
|
||
animate={{
|
||
y: [15, -15, 15],
|
||
x: [8, -8, 8],
|
||
scale: [1, 0.9, 1]
|
||
}}
|
||
transition={{ duration: 12, repeat: Infinity, ease: "easeInOut", delay: 2 }}
|
||
/>
|
||
</div>
|
||
|
||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 relative z-10">
|
||
<ParallaxContainer depth={0.2}>
|
||
<SectionHeader eyebrow="What we do" title="Compassionate support when it matters most" subtitle="Outreach, advocacy, emergency assistance, and resource navigation for vulnerable populations." />
|
||
</ParallaxContainer>
|
||
<div className="mt-10 grid gap-6 md:grid-cols-3">
|
||
{items.map((i, idx) => (
|
||
<ParallaxContainer key={idx} depth={0.3 + idx * 0.1}>
|
||
<FeatureCard icon={i.icon} title={i.title} body={i.body} />
|
||
</ParallaxContainer>
|
||
))}
|
||
</div>
|
||
<div className="mt-10 text-center">
|
||
<a href="#/what-we-do" className="btn-secondary">
|
||
See all programs <ArrowRight className="ml-1 inline h-4 w-4" />
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function FeatureCard({ icon: Icon, title, body }: FeatureCardProps) {
|
||
return (
|
||
<motion.div
|
||
className="group card card-hover cursor-pointer"
|
||
whileHover={{
|
||
scale: 1.02,
|
||
rotateY: 5,
|
||
z: 30,
|
||
boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)"
|
||
}}
|
||
initial={{ rotateY: 0, z: 0 }}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||
>
|
||
<motion.div
|
||
className="absolute left-1/2 top-0 -z-10 h-40 w-40 -translate-x-1/2 rounded-full bg-gradient-to-br from-primary-500/20 to-secondary-600/20 blur-2xl"
|
||
whileHover={{ scale: 1.2, opacity: 0.8 }}
|
||
transition={{ duration: 0.3 }}
|
||
/>
|
||
<div className="flex items-center gap-3">
|
||
<motion.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"
|
||
whileHover={{
|
||
scale: 1.1,
|
||
rotateY: 180,
|
||
boxShadow: "0 8px 16px rgba(0,0,0,0.2)"
|
||
}}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
transition={{ duration: 0.4 }}
|
||
>
|
||
<Icon className="h-5 w-5" />
|
||
</motion.div>
|
||
<div className="font-semibold tracking-tight">{title}</div>
|
||
</div>
|
||
<p className="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">{body}</p>
|
||
<a
|
||
href="#/what-we-do"
|
||
className="mt-5 inline-flex items-center text-sm text-primary-700 transition hover:underline dark:text-primary-300"
|
||
>
|
||
Learn more <ArrowRight className="ml-1 h-4 w-4" />
|
||
</a>
|
||
</motion.div>
|
||
)
|
||
}
|
||
|
||
function Impact() {
|
||
const pillars = [
|
||
{ label: "Community outreach", desc: "Direct support for underserved families" },
|
||
{ label: "Emergency assistance", desc: "Crisis, fire, and disaster relief" },
|
||
{ label: "Advocacy & navigation", desc: "Connecting people to hope and resources" },
|
||
{ label: "Faith & restoration", desc: "Dignity, empathy, and compassionate care" },
|
||
]
|
||
|
||
return (
|
||
<section id="impact" className="relative py-24 overflow-hidden">
|
||
{/* Enhanced Background Elements */}
|
||
<div className="absolute inset-0 pointer-events-none">
|
||
<motion.div
|
||
className="absolute top-1/4 left-0 w-40 h-40 bg-gradient-to-br from-primary-200/10 via-secondary-200/10 to-primary-300/10 rounded-full blur-2xl"
|
||
animate={{
|
||
x: [-50, 50, -50],
|
||
y: [-25, 25, -25],
|
||
scale: [0.8, 1.2, 0.8],
|
||
rotate: [0, 180, 360]
|
||
}}
|
||
transition={{ duration: 20, repeat: Infinity, ease: "easeInOut" }}
|
||
/>
|
||
<motion.div
|
||
className="absolute bottom-1/4 right-0 w-56 h-56 bg-gradient-to-bl from-secondary-200/10 via-primary-200/10 to-secondary-300/10 rounded-full blur-2xl"
|
||
animate={{
|
||
x: [30, -30, 30],
|
||
y: [20, -20, 20],
|
||
scale: [1, 0.9, 1],
|
||
rotate: [360, 180, 0]
|
||
}}
|
||
transition={{ duration: 25, repeat: Infinity, ease: "easeInOut", delay: 3 }}
|
||
/>
|
||
</div>
|
||
|
||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 relative z-10">
|
||
<ParallaxContainer depth={0.2}>
|
||
<SectionHeader eyebrow="Our heart" title="Healing begins when people feel seen" subtitle={SITE.closingLine} />
|
||
</ParallaxContainer>
|
||
|
||
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||
{pillars.map((s, i) => (
|
||
<ParallaxContainer key={i} depth={0.3 + i * 0.05}>
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 30 }}
|
||
whileInView={{ opacity: 1, y: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.8, delay: i * 0.15 }}
|
||
className="card h-full text-center"
|
||
>
|
||
<div className="font-semibold tracking-tight text-primary-800 dark:text-primary-100">{s.label}</div>
|
||
<p className="mt-2 text-sm text-neutral-600 dark:text-neutral-400">{s.desc}</p>
|
||
</motion.div>
|
||
</ParallaxContainer>
|
||
))}
|
||
</div>
|
||
<div className="mt-10 grid items-center gap-6 rounded-2xl border border-secondary-300/30 bg-secondary-50/50 p-6 sm:grid-cols-2 dark:border-secondary-700/30 dark:bg-secondary-900/20">
|
||
<div className="text-sm text-primary-900 dark:text-primary-100">
|
||
<div className="font-medium">Your gift restores hope</div>
|
||
<p className="mt-3 leading-relaxed">
|
||
Donations support outreach, emergency assistance, wellness support, and resource navigation for women, families, survivors, and communities in crisis throughout Los Angeles County.
|
||
</p>
|
||
</div>
|
||
<div className="flex flex-wrap items-center justify-end gap-3">
|
||
<a href="#/donate" className="btn-primary">
|
||
Donate <ArrowRight className="ml-2 h-4 w-4" />
|
||
</a>
|
||
<a href="#/stories" className="btn-secondary">
|
||
Stories of hope
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function HowItWorks() {
|
||
const steps = HOW_IT_WORKS.steps.map((s) => ({ title: s.title, body: s.body }))
|
||
|
||
return (
|
||
<section id="how" className="relative py-24 overflow-hidden">
|
||
{/* Animated Background Elements */}
|
||
<div className="absolute inset-0 pointer-events-none">
|
||
<motion.div
|
||
className="absolute top-16 right-1/4 w-3 h-3 bg-primary-300/40 dark:bg-primary-600/40 rounded-full"
|
||
animate={{
|
||
y: [-30, 30, -30],
|
||
x: [-15, 15, -15],
|
||
scale: [0.5, 1.5, 0.5],
|
||
opacity: [0.3, 0.8, 0.3]
|
||
}}
|
||
transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }}
|
||
/>
|
||
<motion.div
|
||
className="absolute bottom-20 left-1/3 w-2 h-2 bg-secondary-300/40 dark:bg-secondary-600/40 rounded-full"
|
||
animate={{
|
||
y: [20, -20, 20],
|
||
x: [10, -10, 10],
|
||
scale: [0.8, 1.2, 0.8],
|
||
opacity: [0.4, 0.9, 0.4]
|
||
}}
|
||
transition={{ duration: 6, repeat: Infinity, ease: "easeInOut", delay: 2 }}
|
||
/>
|
||
<motion.div
|
||
className="absolute top-1/3 left-1/5 w-4 h-4 bg-gradient-to-br from-primary-200/30 to-secondary-200/30 rounded-full blur-sm"
|
||
animate={{
|
||
rotate: [0, 360],
|
||
scale: [0.6, 1.1, 0.6],
|
||
y: [-10, 10, -10]
|
||
}}
|
||
transition={{ duration: 12, repeat: Infinity, ease: "easeInOut", delay: 4 }}
|
||
/>
|
||
</div>
|
||
|
||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 relative z-10">
|
||
<ParallaxContainer depth={0.2}>
|
||
<SectionHeader eyebrow={HOW_IT_WORKS.eyebrow} title={HOW_IT_WORKS.title} subtitle={HOW_IT_WORKS.subtitle} />
|
||
</ParallaxContainer>
|
||
<div className="mt-10 grid gap-6 md:grid-cols-2">
|
||
<ParallaxContainer depth={0.3}>
|
||
<motion.div
|
||
className="card"
|
||
initial={{ opacity: 0, rotateY: -15 }}
|
||
whileInView={{ opacity: 1, rotateY: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.8 }}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<ol className="relative space-y-6">
|
||
{steps.map((s, i) => (
|
||
<motion.li
|
||
key={i}
|
||
className="relative pl-8"
|
||
initial={{ opacity: 0, x: -20 }}
|
||
whileInView={{ opacity: 1, x: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.6, delay: i * 0.15 }}
|
||
whileHover={{
|
||
x: 5,
|
||
transition: { duration: 0.2 }
|
||
}}
|
||
>
|
||
<motion.div
|
||
className="absolute left-0 top-1 grid h-6 w-6 place-items-center rounded-full bg-gradient-to-br from-primary-500 to-secondary-600 text-white shadow text-xs"
|
||
whileHover={{
|
||
scale: 1.1,
|
||
rotateY: 180,
|
||
boxShadow: "0 4px 8px rgba(0,0,0,0.2)"
|
||
}}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
{i + 1}
|
||
</motion.div>
|
||
<div className="font-medium">{s.title}</div>
|
||
<p className="text-sm text-neutral-700 dark:text-neutral-300">{s.body}</p>
|
||
</motion.li>
|
||
))}
|
||
</ol>
|
||
</motion.div>
|
||
</ParallaxContainer>
|
||
<div className="grid gap-6">
|
||
<ParallaxContainer depth={0.4}>
|
||
<motion.div
|
||
className="rounded-2xl border border-primary-500/20 bg-primary-50/80 p-6 dark:border-primary-700/30 dark:bg-primary-900/20"
|
||
initial={{ opacity: 0, rotateX: 15, y: 20 }}
|
||
whileInView={{ opacity: 1, rotateX: 0, y: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.7, delay: 0.2 }}
|
||
whileHover={{
|
||
scale: 1.02,
|
||
rotateY: 3,
|
||
z: 20,
|
||
boxShadow: "0 8px 25px rgba(16, 185, 129, 0.15)"
|
||
}}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<motion.div
|
||
className="grid h-10 w-10 place-items-center rounded-xl bg-primary-600 text-white"
|
||
whileHover={{ rotateY: 180, scale: 1.1 }}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<Heart className="h-5 w-5"/>
|
||
</motion.div>
|
||
<div className="font-semibold tracking-tight">Donor promise</div>
|
||
</div>
|
||
<p className="mt-2 text-sm text-primary-900 dark:text-primary-100">{HOW_IT_WORKS.donorPromise}</p>
|
||
</motion.div>
|
||
</ParallaxContainer>
|
||
<ParallaxContainer depth={0.5}>
|
||
<motion.div
|
||
className="rounded-2xl border border-secondary-500/20 bg-secondary-500/10 p-6"
|
||
initial={{ opacity: 0, rotateX: -15, y: 20 }}
|
||
whileInView={{ opacity: 1, rotateX: 0, y: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.7, delay: 0.4 }}
|
||
whileHover={{
|
||
scale: 1.02,
|
||
rotateY: -3,
|
||
z: 20,
|
||
boxShadow: "0 8px 25px rgba(147, 51, 234, 0.15)"
|
||
}}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<motion.div
|
||
className="grid h-10 w-10 place-items-center rounded-xl bg-secondary-500 text-white"
|
||
whileHover={{ rotateY: 180, scale: 1.1 }}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<Users className="h-5 w-5"/>
|
||
</motion.div>
|
||
<div className="font-semibold tracking-tight">For partners & volunteers</div>
|
||
</div>
|
||
<p className="mt-2 text-sm text-secondary-950 dark:text-secondary-100">{HOW_IT_WORKS.partnerNote}</p>
|
||
</motion.div>
|
||
</ParallaxContainer>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function Testimonial() {
|
||
return (
|
||
<section className="relative py-20">
|
||
<div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||
<div className="card p-8">
|
||
<div className="absolute -left-10 -top-10 h-60 w-60 rounded-full bg-gradient-to-tr from-primary-500/30 to-secondary-600/30 blur-3xl" />
|
||
<blockquote className="text-balance text-xl leading-8 text-neutral-800 dark:text-neutral-100">
|
||
“{HOME_TESTIMONIAL.quote}”
|
||
</blockquote>
|
||
<div className="mt-4 flex items-center gap-3 text-sm text-neutral-600 dark:text-neutral-300">
|
||
<Star className="h-4 w-4 text-secondary-500" />
|
||
{HOME_TESTIMONIAL.attribution}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function GetInvolved() {
|
||
const options = [
|
||
{ title: "Donate", body: "Restore hope through outreach, emergency aid, and compassionate care.", href: "#/donate", accent: "from-secondary-500 to-primary-500", icon: Heart },
|
||
{ title: "Volunteer", body: "Serve at outreach events and support families in your community.", href: "#/volunteers", accent: "from-sky-500 to-secondary-500", icon: Users },
|
||
{ title: "Partner with us", body: "Churches, businesses, and organizations — let's serve together.", href: "#/contact", accent: "from-primary-500 to-secondary-500", icon: Globe },
|
||
]
|
||
|
||
return (
|
||
<section id="get-involved" className="relative py-24 overflow-hidden">
|
||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||
<ParallaxContainer depth={0.3}>
|
||
<SectionHeader eyebrow="Get involved" title="There is a role for everyone" subtitle="Give, volunteer, or partner — every act of compassion builds hope." />
|
||
</ParallaxContainer>
|
||
|
||
<div className="mt-10 grid gap-6 md:grid-cols-3">
|
||
{options.map((o, i) => (
|
||
<ParallaxContainer key={i} depth={0.5 + i * 0.1}>
|
||
<Callout {...o} index={i} />
|
||
</ParallaxContainer>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function Callout({ title, body, href, accent, icon: Icon, index = 0 }: CalloutProps) {
|
||
return (
|
||
<motion.div
|
||
className="group card card-hover transform-gpu"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
whileInView={{ opacity: 1, y: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||
whileHover={{
|
||
scale: 1.05,
|
||
rotateY: index % 2 === 0 ? 5 : -5,
|
||
z: 50
|
||
}}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<div className={`absolute -right-10 -top-10 h-36 w-36 rounded-full bg-gradient-to-br ${accent} opacity-30 blur-2xl group-hover:opacity-50 transition-opacity duration-300`} />
|
||
|
||
<motion.div
|
||
className={`mb-3 grid h-12 w-12 place-items-center rounded-xl bg-gradient-to-br ${accent} text-white shadow`}
|
||
whileHover={{
|
||
rotateY: 180,
|
||
scale: 1.1
|
||
}}
|
||
transition={{ duration: 0.3 }}
|
||
>
|
||
<Icon className="h-6 w-6" />
|
||
</motion.div>
|
||
|
||
<div className="font-semibold tracking-tight">{title}</div>
|
||
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{body}</p>
|
||
|
||
<motion.a
|
||
href={href}
|
||
className="mt-4 inline-flex items-center text-sm text-neutral-800 underline-offset-4 hover:underline dark:text-neutral-200"
|
||
whileHover={{ x: 5 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
Learn more <ArrowRight className="ml-1 h-4 w-4" />
|
||
</motion.a>
|
||
</motion.div>
|
||
)
|
||
}
|
||
|
||
function CTA() {
|
||
return (
|
||
<section className="relative py-24">
|
||
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||
<div className="relative overflow-hidden rounded-3xl border border-white/30 bg-gradient-to-br from-primary-600 via-secondary-600 to-secondary-700 p-8 text-white shadow-2xl dark:border-white/10">
|
||
<div className="absolute inset-0 opacity-30 [mask-image:radial-gradient(60%_80%_at_70%_50%,white,transparent)]" style={{ backgroundImage: "url('data:image/svg+xml;utf8,<svg xmlns=\\'http://www.w3.org/2000/svg\\' width=\\'400\\' height=\\'400\\'><defs><pattern id=\\'g\\' width=\\'40\\' height=\\'40\\' patternUnits=\\'userSpaceOnUse\\'><path d=\\'M0 20 H40 M20 0 V40\\' stroke=\\'white\\' stroke-opacity=\\'0.25\\' stroke-width=\\'1\\'/></pattern></defs><rect width=\\'100%\\' height=\\'100%\\' fill=\\'url(%23g)\\'/></svg>')" }} />
|
||
<div className="grid items-center gap-8 md:grid-cols-2">
|
||
<div>
|
||
<h2 className="font-serif text-balance text-3xl font-semibold tracking-tight sm:text-4xl">Help restore hope in someone's hardest season</h2>
|
||
<p className="mt-3 max-w-md text-white/90">Your generosity supports outreach, emergency assistance, and resource navigation for families facing crisis throughout Los Angeles County.</p>
|
||
<div className="mt-6 flex flex-wrap items-center gap-3">
|
||
<a href="#/donate" className="btn-white">Donate now</a>
|
||
<a href="#/request-assistance" className="inline-flex items-center gap-2 rounded-full border-2 border-white/40 px-6 py-3 text-sm font-medium text-white backdrop-blur transition hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2">
|
||
Request assistance <ArrowRight className="ml-1 h-4 w-4" />
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div className="relative">
|
||
<div className="rounded-2xl border border-white/40 bg-white/10 p-6 backdrop-blur">
|
||
<h3 className="font-medium tracking-tight">Contact us</h3>
|
||
<ul className="mt-3 space-y-2 text-sm text-white/90">
|
||
<li className="flex items-center gap-2"><Mail className="h-4 w-4"/> <a href={SITE.emailHref} className="underline">{SITE.email}</a></li>
|
||
<li className="flex items-start gap-2"><MapPin className="mt-0.5 h-4 w-4 shrink-0"/> {SITE.serviceArea}</li>
|
||
</ul>
|
||
<p className="mt-3 text-xs text-white/80">{SITE.legalStatus}. EIN {SITE.ein}.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
/* ===================== Pages ===================== */
|
||
function PageShell({ title, icon: Icon, eyebrow, children, cta }: PageShellProps) {
|
||
return (
|
||
<section className="relative py-16 sm:py-24">
|
||
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||
<div className="mb-10 flex items-center gap-3">
|
||
<div className="grid h-12 w-12 place-items-center rounded-xl bg-gradient-to-br from-primary-500 to-secondary-600 text-white shadow">
|
||
<Icon className="h-6 w-6"/>
|
||
</div>
|
||
<div>
|
||
{eyebrow && <div className="section-eyebrow">{eyebrow}</div>}
|
||
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">{title}</h1>
|
||
</div>
|
||
<div className="ml-auto">{cta}</div>
|
||
</div>
|
||
<div className="grid gap-8">{children}</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function DonatePage() {
|
||
const [selectedAmount, setSelectedAmount] = useState(50)
|
||
const [customAmount, setCustomAmount] = useState('')
|
||
const [isRecurring, setIsRecurring] = useState(false)
|
||
const [donorInfo, setDonorInfo] = useState({ email: '', name: '', anonymous: false })
|
||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod>(paymentMethods[0])
|
||
const [isProcessing, setIsProcessing] = useState(false)
|
||
const { addNotification } = useNotifications()
|
||
const { t } = useLanguage()
|
||
|
||
const suggestedAmounts = [
|
||
{ amount: 25, impact: "Essentials for one individual", popular: false },
|
||
{ amount: 50, impact: "Support for one family in crisis", popular: true },
|
||
{ amount: 100, impact: "Outreach supplies for two families", popular: false },
|
||
{ amount: 250, impact: "Emergency relief for multiple families", popular: false },
|
||
]
|
||
|
||
const finalAmount = customAmount ? parseInt(customAmount) || 0 : selectedAmount
|
||
const impactData = calculateDonationImpact(finalAmount)
|
||
|
||
// Enhanced impact text with real calculations
|
||
const getDetailedImpactText = (amount: number) => {
|
||
const impact = calculateDonationImpact(amount)
|
||
const impactItems = []
|
||
|
||
if (impact.families > 0) impactItems.push(`${impact.families} famil${impact.families > 1 ? 'ies' : 'y'} with compassionate support`)
|
||
if (impact.backpacks > 0) impactItems.push(`${impact.backpacks} backpack kit${impact.backpacks > 1 ? 's' : ''}`)
|
||
if (impact.clothing > 0) impactItems.push(`${impact.clothing} clothing item${impact.clothing > 1 ? 's' : ''}`)
|
||
if (impact.emergency > 0) impactItems.push(`${impact.emergency} emergency response${impact.emergency > 1 ? 's' : ''}`)
|
||
|
||
return impactItems.length > 0 ? impactItems.join(', ') : "Every dollar helps restore hope for someone in need"
|
||
}
|
||
|
||
// Track donation interactions
|
||
useEffect(() => {
|
||
trackEvent('donation_page_view', { amount: finalAmount, recurring: isRecurring })
|
||
}, [])
|
||
|
||
const handleDonationSubmit = async () => {
|
||
if (finalAmount <= 0) return
|
||
|
||
setIsProcessing(true)
|
||
trackEvent('donation_initiated', {
|
||
amount: finalAmount,
|
||
recurring: isRecurring,
|
||
anonymous: donorInfo.anonymous,
|
||
method: selectedPaymentMethod.id
|
||
})
|
||
|
||
addNotification({
|
||
type: 'info',
|
||
title: 'Processing Payment',
|
||
message: `Processing your ${isRecurring ? 'monthly ' : ''}donation of $${finalAmount}...`
|
||
})
|
||
|
||
try {
|
||
const result = await PaymentProcessor.processPayment(finalAmount, selectedPaymentMethod, donorInfo)
|
||
|
||
if (result.success) {
|
||
addNotification({
|
||
type: 'success',
|
||
title: 'Donation Successful!',
|
||
message: `Thank you for your ${isRecurring ? 'monthly ' : ''}donation of $${finalAmount}. Transaction ID: ${result.transactionId}`,
|
||
actions: [
|
||
{
|
||
label: 'View Receipt',
|
||
action: () => {
|
||
// In production, would show receipt modal or download PDF
|
||
trackEvent('receipt_viewed', { transactionId: result.transactionId })
|
||
alert('Receipt functionality would open here in production')
|
||
}
|
||
}
|
||
]
|
||
})
|
||
|
||
// Reset form
|
||
setSelectedAmount(50)
|
||
setCustomAmount('')
|
||
setDonorInfo({ email: '', name: '', anonymous: false })
|
||
} else {
|
||
addNotification({
|
||
type: 'error',
|
||
title: 'Payment Failed',
|
||
message: result.error || 'Unable to process payment. Please try again.'
|
||
})
|
||
}
|
||
} catch (error) {
|
||
addNotification({
|
||
type: 'error',
|
||
title: 'Payment Error',
|
||
message: 'An unexpected error occurred. Please try again.'
|
||
})
|
||
} finally {
|
||
setIsProcessing(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<SEOHead
|
||
title="Donate Now — Miracles in Motion Foundation"
|
||
description="Make a secure donation to support outreach, emergency assistance, and resource navigation for families in crisis throughout Los Angeles County."
|
||
/>
|
||
<PageShell title="Donate" icon={Heart} eyebrow="Give with confidence" cta={<a href="#/legal" className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" aria-label="View donation policies">Policies</a>}>
|
||
<div className="grid gap-8 md:grid-cols-3">
|
||
<div className="md:col-span-2 space-y-8">
|
||
{/* Enhanced Impact Calculator */}
|
||
<motion.div
|
||
className="card bg-gradient-to-br from-primary-50 to-secondary-50 dark:from-primary-900/20 dark:to-secondary-900/20 border-primary-200/50 dark:border-primary-800/50"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.6 }}
|
||
whileHover={{ scale: 1.01, y: -2 }}
|
||
>
|
||
<div className="flex items-start gap-4 mb-4">
|
||
<motion.div
|
||
className="p-3 bg-primary-600 text-white rounded-xl"
|
||
whileHover={{ scale: 1.1, rotateY: 180 }}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<Heart className="h-6 w-6" />
|
||
</motion.div>
|
||
<div className="flex-1">
|
||
<h3 className="font-semibold text-lg mb-2">Your Impact: ${finalAmount}</h3>
|
||
<p className="text-primary-700 dark:text-primary-300 font-medium mb-3">
|
||
{getDetailedImpactText(finalAmount)}
|
||
</p>
|
||
|
||
{/* Real-time impact breakdown */}
|
||
{finalAmount > 0 && (
|
||
<div className="grid gap-2 text-sm">
|
||
<div className="flex items-center justify-between p-2 bg-white/50 dark:bg-gray-800/50 rounded">
|
||
<span className="flex items-center gap-2">
|
||
<Users className="h-4 w-4" /> Families supported
|
||
</span>
|
||
<span className="font-semibold">{impactData.families}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between p-2 bg-white/50 dark:bg-gray-800/50 rounded">
|
||
<span className="flex items-center gap-2">
|
||
<Package className="h-4 w-4" /> Outreach kits
|
||
</span>
|
||
<span className="font-semibold">{impactData.backpacks}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between p-2 bg-white/50 dark:bg-gray-800/50 rounded">
|
||
<span className="flex items-center gap-2">
|
||
<Shirt className="h-4 w-4" /> Clothing Items
|
||
</span>
|
||
<span className="font-semibold">{impactData.clothing}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{isRecurring && finalAmount > 0 && (
|
||
<div className="mt-3 p-3 bg-secondary-50 dark:bg-secondary-900/20 rounded-lg">
|
||
<p className="text-sm text-secondary-700 dark:text-secondary-300 font-medium">
|
||
🎉 Annual Impact: {impactData.annual.totalImpact}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Donation Form */}
|
||
<div className="card">
|
||
<div className="mb-6">
|
||
<h3 className="font-semibold text-lg mb-2">Choose Your Donation Amount</h3>
|
||
<p className="text-sm text-neutral-600 dark:text-neutral-400">Every dollar directly supports families and individuals in need</p>
|
||
</div>
|
||
|
||
{/* Suggested Amounts - Mobile Optimized */}
|
||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4 mb-6">
|
||
{suggestedAmounts.map((tier) => (
|
||
<motion.button
|
||
key={tier.amount}
|
||
onClick={() => {
|
||
setSelectedAmount(tier.amount)
|
||
setCustomAmount('')
|
||
trackEvent('donation_amount_selected', { amount: tier.amount, method: 'suggested' })
|
||
}}
|
||
className={`relative p-4 rounded-xl border-2 transition-all focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
|
||
selectedAmount === tier.amount && !customAmount
|
||
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
|
||
: 'border-neutral-200 dark:border-neutral-700 hover:border-primary-300'
|
||
}`}
|
||
style={{ minHeight: '88px', minWidth: '120px' }} // Enhanced touch targets for mobile
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
aria-label={`Donate $${tier.amount} - ${tier.impact}`}
|
||
>
|
||
{tier.popular && (
|
||
<div className="absolute -top-2 left-1/2 transform -translate-x-1/2">
|
||
<span className="bg-secondary-500 text-white text-xs px-2 py-1 rounded-full font-medium">
|
||
Most Popular
|
||
</span>
|
||
</div>
|
||
)}
|
||
<div className="text-2xl font-bold text-primary-600 dark:text-primary-400">
|
||
${tier.amount}
|
||
</div>
|
||
<div className="text-xs text-neutral-600 dark:text-neutral-400 mt-1">
|
||
{tier.impact}
|
||
</div>
|
||
</motion.button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Custom Amount */}
|
||
<div className="mb-6">
|
||
<label htmlFor="custom-amount" className="block text-sm font-medium mb-2">
|
||
Or enter a custom amount
|
||
</label>
|
||
<div className="relative">
|
||
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-500">$</span>
|
||
<input
|
||
id="custom-amount"
|
||
type="number"
|
||
min="1"
|
||
value={customAmount}
|
||
onChange={(e) => {
|
||
setCustomAmount(e.target.value)
|
||
setSelectedAmount(0)
|
||
}}
|
||
placeholder="Enter amount"
|
||
className="input pl-8 w-full focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
aria-label="Custom donation amount in dollars"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Recurring Option & Donor Info */}
|
||
<div className="space-y-4 mb-6">
|
||
<div className="p-4 bg-secondary-50 dark:bg-secondary-900/20 rounded-xl">
|
||
<label className="flex items-start gap-3 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={isRecurring}
|
||
onChange={(e) => {
|
||
setIsRecurring(e.target.checked)
|
||
trackEvent('donation_recurring_toggle', { recurring: e.target.checked, amount: finalAmount })
|
||
}}
|
||
className="mt-1 h-5 w-5 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" // Enhanced for mobile
|
||
style={{ minHeight: '20px', minWidth: '20px' }}
|
||
/>
|
||
<div className="flex-1">
|
||
<div className="font-medium">Make this a monthly donation</div>
|
||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||
Recurring donations help us plan better and have more impact
|
||
</div>
|
||
{isRecurring && finalAmount > 0 && (
|
||
<div className="text-sm text-secondary-600 dark:text-secondary-400 mt-2 p-2 bg-white/50 dark:bg-gray-800/50 rounded font-medium">
|
||
🎯 Annual Impact: {impactData.annual.totalImpact}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</label>
|
||
</div>
|
||
|
||
{/* Donor Information */}
|
||
<div className="grid gap-4 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-xl">
|
||
<h4 className="font-medium text-gray-900 dark:text-gray-100">Donor Information (Optional)</h4>
|
||
<div className="grid gap-3 sm:grid-cols-2">
|
||
<input
|
||
type="text"
|
||
placeholder="Your Name"
|
||
value={donorInfo.name}
|
||
onChange={(e) => setDonorInfo({ ...donorInfo, name: e.target.value })}
|
||
className="input focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
style={{ minHeight: '44px' }}
|
||
/>
|
||
<input
|
||
type="email"
|
||
placeholder="Email (for receipt)"
|
||
value={donorInfo.email}
|
||
onChange={(e) => setDonorInfo({ ...donorInfo, email: e.target.value })}
|
||
className="input focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
style={{ minHeight: '44px' }}
|
||
/>
|
||
</div>
|
||
<label className="flex items-center gap-3 text-sm">
|
||
<input
|
||
type="checkbox"
|
||
checked={donorInfo.anonymous}
|
||
onChange={(e) => setDonorInfo({ ...donorInfo, anonymous: e.target.checked })}
|
||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||
/>
|
||
<span>Make my donation anonymous</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Payment Method Selection */}
|
||
<div className="mb-6">
|
||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">Payment Method</h4>
|
||
<div className="grid gap-3 sm:grid-cols-2">
|
||
{paymentMethods.map((method) => {
|
||
const fees = PaymentProcessor.calculateFees(finalAmount, method)
|
||
return (
|
||
<motion.button
|
||
key={method.id}
|
||
onClick={() => setSelectedPaymentMethod(method)}
|
||
className={`p-4 rounded-xl border-2 transition-all focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 text-left ${
|
||
selectedPaymentMethod.id === method.id
|
||
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
|
||
: 'border-neutral-200 dark:border-neutral-700 hover:border-primary-300'
|
||
}`}
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
>
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<span className="text-2xl">{method.icon}</span>
|
||
<div className="flex-1">
|
||
<div className="font-medium">{method.name}</div>
|
||
<div className="text-sm text-neutral-500">{method.description}</div>
|
||
</div>
|
||
{selectedPaymentMethod.id === method.id && (
|
||
<Check className="h-5 w-5 text-primary-600" />
|
||
)}
|
||
</div>
|
||
{finalAmount > 0 && (
|
||
<div className="text-xs text-neutral-600 dark:text-neutral-400">
|
||
Fee: ${fees.fee} ({method.processingFee}%) • You give: ${fees.net}
|
||
</div>
|
||
)}
|
||
</motion.button>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Enhanced Donation Buttons */}
|
||
<div className="space-y-3">
|
||
<motion.button
|
||
disabled={finalAmount <= 0 || isProcessing}
|
||
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||
style={{ minHeight: '56px' }} // Enhanced mobile touch target
|
||
whileHover={finalAmount > 0 && !isProcessing ? { scale: 1.02 } : {}}
|
||
whileTap={finalAmount > 0 && !isProcessing ? { scale: 0.98 } : {}}
|
||
onClick={handleDonationSubmit}
|
||
aria-label={`Donate $${finalAmount} ${isRecurring ? 'monthly' : 'one-time'} via ${selectedPaymentMethod.name}`}
|
||
>
|
||
{isProcessing ? (
|
||
<><Clock className="mr-2 h-5 w-5 animate-spin" /> Processing...</>
|
||
) : (
|
||
<><Heart className="mr-2 h-5 w-5" /> {t('donate.title')} ${finalAmount} via {selectedPaymentMethod.icon}</>
|
||
)}
|
||
</motion.button>
|
||
|
||
<div className="grid gap-3 sm:grid-cols-2">
|
||
<motion.button
|
||
className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||
style={{ minHeight: '48px' }}
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={() => trackEvent('donation_method_selected', { method: 'paypal', amount: finalAmount })}
|
||
aria-label="Donate via PayPal"
|
||
>
|
||
💳 PayPal
|
||
</motion.button>
|
||
<motion.button
|
||
className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||
style={{ minHeight: '48px' }}
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={() => trackEvent('donation_method_selected', { method: 'venmo', amount: finalAmount })}
|
||
aria-label="Donate via Venmo"
|
||
>
|
||
📱 Venmo
|
||
</motion.button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4 p-4 bg-neutral-50 dark:bg-neutral-800/50 rounded-lg">
|
||
<div className="flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||
<Shield className="h-4 w-4" />
|
||
<span>Secure 256-bit SSL encryption</span>
|
||
</div>
|
||
<p className="text-xs text-neutral-500 dark:text-neutral-500 mt-2">
|
||
You'll receive an email receipt for your tax records. Donations are tax-deductible to the extent allowed by law. EIN {SITE.ein}.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Matching Gifts */}
|
||
<div className="card">
|
||
<div className="flex items-center gap-3 mb-3">
|
||
<div className="p-2 bg-secondary-100 dark:bg-secondary-900/30 rounded-lg">
|
||
<Building2 className="h-5 w-5 text-secondary-600 dark:text-secondary-400" />
|
||
</div>
|
||
<div>
|
||
<div className="font-medium">Employer Matching Gifts</div>
|
||
<div className="text-sm text-neutral-600 dark:text-neutral-400">Double your impact</div>
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-neutral-700 dark:text-neutral-300 mb-3">
|
||
Many employers will match your charitable donation. Check with your HR department and provide our EIN: {SITE.ein}.
|
||
</p>
|
||
<a href="#" className="inline-flex items-center gap-2 text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded">
|
||
<ExternalLink className="h-4 w-4" />
|
||
Download matching gift letter
|
||
</a>
|
||
</div>
|
||
|
||
{/* Other Ways to Give */}
|
||
<div className="card">
|
||
<div className="font-medium mb-3">Other Ways to Give</div>
|
||
<div className="space-y-3">
|
||
<div className="flex items-start gap-3 p-3 bg-neutral-50 dark:bg-neutral-800/50 rounded-lg">
|
||
<Mail className="h-5 w-5 text-neutral-500 mt-0.5" />
|
||
<div>
|
||
<div className="font-medium text-sm">Mail a Check</div>
|
||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||
Miracles In Motion<br />
|
||
20274 Via Medici<br />
|
||
Porter Ranch, CA 91326
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-start gap-3 p-3 bg-neutral-50 dark:bg-neutral-800/50 rounded-lg">
|
||
<DollarSign className="h-5 w-5 text-neutral-500 mt-0.5" />
|
||
<div>
|
||
<div className="font-medium text-sm">Stock or Securities</div>
|
||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||
Contact us for account transfer information
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Sidebar */}
|
||
<div className="space-y-6">
|
||
{/* Trust Indicators */}
|
||
<div className="card bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<div className="p-2 bg-green-600 text-white rounded-lg">
|
||
<Award className="h-5 w-5" />
|
||
</div>
|
||
<div>
|
||
<div className="font-medium text-green-900 dark:text-green-100">501(c)(3) Verified</div>
|
||
<div className="text-sm text-green-700 dark:text-green-300">Tax-deductible donations</div>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex justify-between text-green-800 dark:text-green-200">
|
||
<span>Program expenses:</span>
|
||
<span className="font-medium">85%</span>
|
||
</div>
|
||
<div className="flex justify-between text-green-800 dark:text-green-200">
|
||
<span>Administrative:</span>
|
||
<span className="font-medium">10%</span>
|
||
</div>
|
||
<div className="flex justify-between text-green-800 dark:text-green-200">
|
||
<span>Fundraising:</span>
|
||
<span className="font-medium">5%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Recent Impact */}
|
||
<div className="card">
|
||
<div className="font-medium mb-3">Recent Impact</div>
|
||
<div className="space-y-3">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-2 h-2 bg-primary-500 rounded-full"></div>
|
||
<div className="text-sm text-neutral-700 dark:text-neutral-300">
|
||
<span className="font-medium">Families across LA County</span> received outreach and emergency support this month
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-2 h-2 bg-secondary-500 rounded-full"></div>
|
||
<div className="text-sm text-neutral-700 dark:text-neutral-300">
|
||
<span className="font-medium">43 families</span> got emergency clothing support
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||
<div className="text-sm text-neutral-700 dark:text-neutral-300">
|
||
<span className="font-medium">$12,450</span> raised this week by donors like you
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Contact Info */}
|
||
<div className="card bg-primary-50 dark:bg-primary-900/20">
|
||
<div className="font-medium mb-3">Questions about donating?</div>
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex items-center gap-2">
|
||
<Phone className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||
<a href="tel:+18184916884" className="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300">
|
||
(818) 491-6884
|
||
</a>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Mail className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||
<a href={SITE.emailHref} className="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300">
|
||
{SITE.email}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</PageShell>
|
||
</>
|
||
)
|
||
}
|
||
|
||
function VolunteerPage() {
|
||
return (
|
||
<PageShell title="Volunteer" icon={Users} eyebrow="Serve locally">
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
<div className="card">
|
||
<div className="font-medium">Sign-up form</div>
|
||
<form className="mt-4 grid grid-cols-1 gap-4" onSubmit={(e)=>{e.preventDefault(); alert("Thanks! We'll be in touch via email.");}}>
|
||
<div className="grid gap-2 sm:grid-cols-2">
|
||
<input required aria-label="First name" placeholder="First name" className="input" />
|
||
<input required aria-label="Last name" placeholder="Last name" className="input" />
|
||
</div>
|
||
<div className="grid gap-2 sm:grid-cols-2">
|
||
<input required type="email" aria-label="Email" placeholder="Email" className="input" />
|
||
<input aria-label="Phone" placeholder="Phone" className="input" />
|
||
</div>
|
||
<div className="grid gap-2 sm:grid-cols-2">
|
||
<select aria-label="Interests" className="input">
|
||
<option>Assemble kits</option>
|
||
<option>Delivery driver</option>
|
||
<option>Community partner</option>
|
||
<option>Admin support</option>
|
||
</select>
|
||
<select aria-label="Availability" className="input">
|
||
<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>
|
||
<button className="btn-primary w-full justify-center" type="submit">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 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>
|
||
<form className="mt-4 grid gap-4" onSubmit={(e)=>{e.preventDefault(); alert("Thanks! We'll reach out soon.");}}>
|
||
<input className="input" placeholder="Company name" aria-label="Company name" required />
|
||
<div className="grid gap-2 sm:grid-cols-2">
|
||
<input className="input" placeholder="Contact name" aria-label="Contact name" required />
|
||
<input type="email" className="input" placeholder="Email" aria-label="Email" required />
|
||
</div>
|
||
<select className="input" aria-label="Tier interest">
|
||
<option>Bronze</option>
|
||
<option>Silver</option>
|
||
<option>Gold</option>
|
||
<option>Platinum</option>
|
||
<option>Custom</option>
|
||
</select>
|
||
<textarea className="input min-h-28 resize-none" placeholder="Tell us about your goals" aria-label="Message"></textarea>
|
||
<label className="flex items-start gap-3 text-xs opacity-80">
|
||
<input type="checkbox" className="mt-1 h-4 w-4"/>
|
||
I agree to logo-use guidelines and brand approvals per our <a className="underline" href="#/legal#sponsorship-terms">Sponsorship Terms</a>.
|
||
</label>
|
||
<button className="btn-primary w-full justify-center" type="submit">Send</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</PageShell>
|
||
)
|
||
}
|
||
|
||
function StoriesPage() {
|
||
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>
|
||
<form className="mt-3 grid gap-3" onSubmit={(e)=>{e.preventDefault(); alert("Thanks for sharing! We'll review and publish with consent.");}}>
|
||
<input className="input" placeholder="Your name (or Anonymous)" aria-label="Name" />
|
||
<textarea className="input min-h-28 resize-none" placeholder="Your story (please omit private identifying details)" aria-label="Story" required />
|
||
<label className="flex items-start gap-3 text-xs opacity-80">
|
||
<input type="checkbox" className="mt-1 h-4 w-4" required />
|
||
I grant permission to publish this story (edited for length/clarity). I have removed personally identifying information.
|
||
</label>
|
||
<button className="btn-primary w-full justify-center">Submit</button>
|
||
</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={SITE.emailHref}>{SITE.email}</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={SITE.emailHref}>{SITE.email}</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:access@miraclesinmotion.org">[email protected]</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 SectionHeader({ eyebrow, title, subtitle }: SectionHeaderProps) {
|
||
return (
|
||
<div className="section-header">
|
||
{eyebrow && <div className="section-eyebrow">{eyebrow}</div>}
|
||
<h2 className="section-title">{title}</h2>
|
||
{subtitle && <p className="section-subtitle">{subtitle}</p>}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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>
|
||
)
|
||
}
|
||
|
||
const MIM_ASSISTANCE_DRAFT_KEY = 'mim_assistance_draft'
|
||
|
||
const initialAssistanceFormState = () => ({
|
||
requestType: '',
|
||
urgency: 'medium',
|
||
studentInfo: {
|
||
firstName: '',
|
||
lastName: '',
|
||
grade: '',
|
||
school: '',
|
||
studentId: ''
|
||
},
|
||
contactInfo: {
|
||
parentName: '',
|
||
phone: '',
|
||
email: '',
|
||
address: '',
|
||
relationship: ''
|
||
},
|
||
needs: {
|
||
schoolSupplies: false,
|
||
clothing: false,
|
||
shoes: false,
|
||
backpack: false,
|
||
emergency: false,
|
||
other: false,
|
||
otherDescription: ''
|
||
},
|
||
details: '',
|
||
verificationMethod: 'school'
|
||
})
|
||
|
||
// Assistance Request Page — inline validation, auto-save draft, specific error messages
|
||
function AssistanceRequestPage() {
|
||
const [formData, setFormData] = useState(() => {
|
||
try {
|
||
const raw = typeof window !== 'undefined' && window.localStorage.getItem(MIM_ASSISTANCE_DRAFT_KEY)
|
||
if (raw) {
|
||
const parsed = JSON.parse(raw) as ReturnType<typeof initialAssistanceFormState>
|
||
return { ...initialAssistanceFormState(), ...parsed }
|
||
}
|
||
} catch (_) { /* ignore */ }
|
||
return initialAssistanceFormState()
|
||
})
|
||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||
const [honeypot, setHoneypot] = useState('')
|
||
const [draftSaved, setDraftSaved] = useState(false)
|
||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||
|
||
// Auto-save draft (debounced); show "Draft saved" briefly
|
||
useEffect(() => {
|
||
const t = setTimeout(() => {
|
||
try {
|
||
window.localStorage.setItem(MIM_ASSISTANCE_DRAFT_KEY, JSON.stringify(formData))
|
||
setDraftSaved(true)
|
||
setTimeout(() => setDraftSaved(false), 2000)
|
||
} catch (_) { /* ignore */ }
|
||
}, 500)
|
||
return () => clearTimeout(t)
|
||
}, [formData])
|
||
|
||
const validateField = (name: string, value: string | undefined): string => {
|
||
const v = value ?? ''
|
||
switch (name) {
|
||
case 'requestType':
|
||
return !v.trim() ? 'Please select a request type.' : ''
|
||
case 'studentInfo.firstName':
|
||
return !v.trim() ? 'First name is required.' : ''
|
||
case 'studentInfo.lastName':
|
||
return !v.trim() ? 'Last name is required.' : ''
|
||
case 'studentInfo.grade':
|
||
return ''
|
||
case 'studentInfo.school':
|
||
return !v.trim() ? 'City or neighborhood in LA County is required.' : ''
|
||
case 'contactInfo.parentName':
|
||
return !v.trim() ? 'Your name is required.' : ''
|
||
case 'contactInfo.phone':
|
||
return !v.trim() ? 'Phone number is required.' : /^[\d\s\-\(\)\+\.]{10,}$/.test(v.replace(/\s/g, '')) ? '' : 'Please enter a valid phone number (at least 10 digits).'
|
||
case 'contactInfo.email':
|
||
return !v.trim() ? 'Email is required.' : /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? '' : 'Please enter a valid email address.'
|
||
case 'contactInfo.relationship':
|
||
return !v.trim() ? 'Please select your relationship.' : ''
|
||
case 'details':
|
||
return !v.trim() ? 'Please describe the situation and specific needs.' : ''
|
||
default:
|
||
return ''
|
||
}
|
||
}
|
||
|
||
const setFieldError = (name: string, value: string | undefined) => {
|
||
const msg = validateField(name, value)
|
||
setErrors(prev => (msg ? { ...prev, [name]: msg } : (() => { const n = { ...prev }; delete n[name]; return n })()))
|
||
}
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
setSubmitError(null)
|
||
if (honeypot) return
|
||
const allErrors: Record<string, string> = {}
|
||
const fields: [string, string | undefined][] = [
|
||
['requestType', formData.requestType],
|
||
['studentInfo.firstName', formData.studentInfo.firstName],
|
||
['studentInfo.lastName', formData.studentInfo.lastName],
|
||
['studentInfo.school', formData.studentInfo.school],
|
||
['contactInfo.parentName', formData.contactInfo.parentName],
|
||
['contactInfo.phone', formData.contactInfo.phone],
|
||
['contactInfo.email', formData.contactInfo.email],
|
||
['contactInfo.relationship', formData.contactInfo.relationship],
|
||
['details', formData.details]
|
||
]
|
||
fields.forEach(([name, value]) => {
|
||
const msg = validateField(name, value)
|
||
if (msg) allErrors[name] = msg
|
||
})
|
||
setErrors(allErrors)
|
||
if (Object.keys(allErrors).length > 0) return
|
||
setIsSubmitting(true)
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
setTimeout(() => {
|
||
try {
|
||
window.localStorage.removeItem(MIM_ASSISTANCE_DRAFT_KEY)
|
||
resolve(undefined)
|
||
} catch (e) {
|
||
reject(e)
|
||
}
|
||
}, 300)
|
||
})
|
||
alert('Request submitted successfully! We will contact you within 24-48 hours.')
|
||
} catch (_) {
|
||
setSubmitError('Submission could not be completed. Your draft has been saved. Please try again or call (818) 491-6884 for assistance.')
|
||
} finally {
|
||
setIsSubmitting(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<PageShell title="Request Assistance" icon={ClipboardList} eyebrow="Compassionate support for individuals and families in crisis">
|
||
<div className="max-w-4xl mx-auto">
|
||
{/* Emergency Notice */}
|
||
<motion.div
|
||
className="mb-8 p-6 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
>
|
||
<div className="flex items-start gap-4">
|
||
<AlertCircle className="h-6 w-6 text-red-600 dark:text-red-400 mt-1" />
|
||
<div>
|
||
<h3 className="font-semibold text-red-900 dark:text-red-100 mb-2">Emergency Assistance</h3>
|
||
<p className="text-red-800 dark:text-red-200 text-sm mb-3">
|
||
For urgent needs (no food, no warm clothing in winter, etc.), please call us directly at
|
||
<strong className="ml-1">(818) 491-6884</strong> for immediate assistance.
|
||
</p>
|
||
<p className="text-red-700 dark:text-red-300 text-xs">
|
||
This form is for non-emergency assistance requests and will be processed within 24-48 hours.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
<form onSubmit={handleSubmit} className="space-y-8">
|
||
{/* Honeypot: leave empty; bots that fill it are ignored */}
|
||
<div className="absolute -left-[9999px] w-1 h-1 overflow-hidden" aria-hidden="true">
|
||
<label htmlFor="mim-website">Website</label>
|
||
<input id="mim-website" type="text" name="website" tabIndex={-1} autoComplete="off" value={honeypot} onChange={(e) => setHoneypot(e.target.value)} />
|
||
</div>
|
||
{/* Request Type */}
|
||
<div className="card">
|
||
<h3 className="font-semibold text-lg mb-4">Type of Request <span className="text-red-600 dark:text-red-400">*</span></h3>
|
||
{errors.requestType && <p className="mb-2 text-sm text-red-600 dark:text-red-400" role="alert">{errors.requestType}</p>}
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||
{[
|
||
{ id: 'individual', label: 'Individual', icon: Users, desc: 'Support for one person' },
|
||
{ id: 'family', label: 'Family', icon: HomeIcon, desc: 'Support for a household' },
|
||
{ id: 'partner', label: 'Agency / partner', icon: Building2, desc: 'Referral from a community partner' },
|
||
{ id: 'emergency', label: 'Emergency', icon: AlertCircle, desc: 'Urgent assistance — call if immediate' }
|
||
].map(type => (
|
||
<motion.label
|
||
key={type.id}
|
||
className={`p-4 border-2 rounded-lg cursor-pointer transition-all hover:border-primary-300 ${
|
||
formData.requestType === type.id
|
||
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
|
||
: errors.requestType ? 'border-red-500 dark:border-red-400' : 'border-neutral-200 dark:border-neutral-700'
|
||
}`}
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
>
|
||
<input
|
||
type="radio"
|
||
name="requestType"
|
||
value={type.id}
|
||
checked={formData.requestType === type.id}
|
||
onChange={(e) => {
|
||
setFormData({ ...formData, requestType: e.target.value })
|
||
setFieldError('requestType', e.target.value)
|
||
}}
|
||
onBlur={() => setFieldError('requestType', formData.requestType)}
|
||
className="sr-only"
|
||
aria-invalid={!!errors.requestType}
|
||
/>
|
||
<type.icon className="h-6 w-6 text-primary-600 dark:text-primary-400 mb-2" />
|
||
<div className="font-medium text-sm">{type.label}</div>
|
||
<div className="text-xs text-neutral-600 dark:text-neutral-400">{type.desc}</div>
|
||
</motion.label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Person needing assistance */}
|
||
<div className="card">
|
||
<h3 className="font-semibold text-lg mb-4">Who needs assistance?</h3>
|
||
<div className="grid gap-4 sm:grid-cols-2">
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" htmlFor="student-first-name">First name *</label>
|
||
<input
|
||
id="student-first-name"
|
||
required
|
||
type="text"
|
||
className={`input ${errors['studentInfo.firstName'] ? 'border-red-500 dark:border-red-400' : ''}`}
|
||
value={formData.studentInfo.firstName}
|
||
onChange={(e) => {
|
||
setFormData({ ...formData, studentInfo: { ...formData.studentInfo, firstName: e.target.value } })
|
||
setFieldError('studentInfo.firstName', e.target.value)
|
||
}}
|
||
onBlur={() => setFieldError('studentInfo.firstName', formData.studentInfo.firstName)}
|
||
aria-invalid={!!errors['studentInfo.firstName']}
|
||
aria-describedby={errors['studentInfo.firstName'] ? 'err-student-first-name' : undefined}
|
||
/>
|
||
{errors['studentInfo.firstName'] && <p id="err-student-first-name" className="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{errors['studentInfo.firstName']}</p>}
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" htmlFor="student-last-name">Last name *</label>
|
||
<input
|
||
id="student-last-name"
|
||
required
|
||
type="text"
|
||
className={`input ${errors['studentInfo.lastName'] ? 'border-red-500 dark:border-red-400' : ''}`}
|
||
value={formData.studentInfo.lastName}
|
||
onChange={(e) => {
|
||
setFormData({ ...formData, studentInfo: { ...formData.studentInfo, lastName: e.target.value } })
|
||
setFieldError('studentInfo.lastName', e.target.value)
|
||
}}
|
||
onBlur={() => setFieldError('studentInfo.lastName', formData.studentInfo.lastName)}
|
||
aria-invalid={!!errors['studentInfo.lastName']}
|
||
aria-describedby={errors['studentInfo.lastName'] ? 'err-student-last-name' : undefined}
|
||
/>
|
||
{errors['studentInfo.lastName'] && <p id="err-student-last-name" className="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{errors['studentInfo.lastName']}</p>}
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" htmlFor="student-grade">Age range (optional)</label>
|
||
<select
|
||
id="student-grade"
|
||
className="input"
|
||
value={formData.studentInfo.grade}
|
||
onChange={(e) => {
|
||
setFormData({ ...formData, studentInfo: { ...formData.studentInfo, grade: e.target.value } })
|
||
}}
|
||
>
|
||
<option value="">Prefer not to say</option>
|
||
<option value="child">Child (under 12)</option>
|
||
<option value="teen">Teen (12–17)</option>
|
||
<option value="adult">Adult (18–64)</option>
|
||
<option value="senior">Senior (65+)</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" htmlFor="student-school">City / neighborhood in LA County *</label>
|
||
<input
|
||
id="student-school"
|
||
required
|
||
type="text"
|
||
className={`input ${errors['studentInfo.school'] ? 'border-red-500 dark:border-red-400' : ''}`}
|
||
placeholder="e.g. San Fernando Valley, South LA"
|
||
value={formData.studentInfo.school}
|
||
onChange={(e) => {
|
||
setFormData({ ...formData, studentInfo: { ...formData.studentInfo, school: e.target.value } })
|
||
setFieldError('studentInfo.school', e.target.value)
|
||
}}
|
||
onBlur={() => setFieldError('studentInfo.school', formData.studentInfo.school)}
|
||
aria-invalid={!!errors['studentInfo.school']}
|
||
aria-describedby={errors['studentInfo.school'] ? 'err-student-school' : undefined}
|
||
/>
|
||
{errors['studentInfo.school'] && <p id="err-student-school" className="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{errors['studentInfo.school']}</p>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Contact Information */}
|
||
<div className="card">
|
||
<h3 className="font-semibold text-lg mb-4">Contact Information</h3>
|
||
<div className="grid gap-4 sm:grid-cols-2">
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" htmlFor="contact-parent">Your name *</label>
|
||
<input
|
||
id="contact-parent"
|
||
required
|
||
type="text"
|
||
className={`input ${errors['contactInfo.parentName'] ? 'border-red-500 dark:border-red-400' : ''}`}
|
||
value={formData.contactInfo.parentName}
|
||
onChange={(e) => {
|
||
setFormData({ ...formData, contactInfo: { ...formData.contactInfo, parentName: e.target.value } })
|
||
setFieldError('contactInfo.parentName', e.target.value)
|
||
}}
|
||
onBlur={() => setFieldError('contactInfo.parentName', formData.contactInfo.parentName)}
|
||
aria-invalid={!!errors['contactInfo.parentName']}
|
||
aria-describedby={errors['contactInfo.parentName'] ? 'err-contact-parent' : undefined}
|
||
/>
|
||
{errors['contactInfo.parentName'] && <p id="err-contact-parent" className="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{errors['contactInfo.parentName']}</p>}
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" htmlFor="contact-phone">Phone Number *</label>
|
||
<input
|
||
id="contact-phone"
|
||
required
|
||
type="tel"
|
||
className={`input ${errors['contactInfo.phone'] ? 'border-red-500 dark:border-red-400' : ''}`}
|
||
placeholder="(555) 123-4567"
|
||
value={formData.contactInfo.phone}
|
||
onChange={(e) => {
|
||
setFormData({ ...formData, contactInfo: { ...formData.contactInfo, phone: e.target.value } })
|
||
setFieldError('contactInfo.phone', e.target.value)
|
||
}}
|
||
onBlur={() => setFieldError('contactInfo.phone', formData.contactInfo.phone)}
|
||
aria-invalid={!!errors['contactInfo.phone']}
|
||
aria-describedby={errors['contactInfo.phone'] ? 'err-contact-phone' : undefined}
|
||
/>
|
||
{errors['contactInfo.phone'] && <p id="err-contact-phone" className="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{errors['contactInfo.phone']}</p>}
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" htmlFor="contact-email">Email Address *</label>
|
||
<input
|
||
id="contact-email"
|
||
required
|
||
type="email"
|
||
className={`input ${errors['contactInfo.email'] ? 'border-red-500 dark:border-red-400' : ''}`}
|
||
value={formData.contactInfo.email}
|
||
onChange={(e) => {
|
||
setFormData({ ...formData, contactInfo: { ...formData.contactInfo, email: e.target.value } })
|
||
setFieldError('contactInfo.email', e.target.value)
|
||
}}
|
||
onBlur={() => setFieldError('contactInfo.email', formData.contactInfo.email)}
|
||
aria-invalid={!!errors['contactInfo.email']}
|
||
aria-describedby={errors['contactInfo.email'] ? 'err-contact-email' : undefined}
|
||
/>
|
||
{errors['contactInfo.email'] && <p id="err-contact-email" className="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{errors['contactInfo.email']}</p>}
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" htmlFor="contact-relationship">Your relationship *</label>
|
||
<select
|
||
id="contact-relationship"
|
||
required
|
||
className={`input ${errors['contactInfo.relationship'] ? 'border-red-500 dark:border-red-400' : ''}`}
|
||
value={formData.contactInfo.relationship}
|
||
onChange={(e) => {
|
||
setFormData({ ...formData, contactInfo: { ...formData.contactInfo, relationship: e.target.value } })
|
||
setFieldError('contactInfo.relationship', e.target.value)
|
||
}}
|
||
onBlur={() => setFieldError('contactInfo.relationship', formData.contactInfo.relationship)}
|
||
aria-invalid={!!errors['contactInfo.relationship']}
|
||
aria-describedby={errors['contactInfo.relationship'] ? 'err-contact-relationship' : undefined}
|
||
>
|
||
<option value="">Select relationship</option>
|
||
<option value="parent">Parent</option>
|
||
<option value="guardian">Legal Guardian</option>
|
||
<option value="relative">Relative</option>
|
||
<option value="self">Self (requesting for myself)</option>
|
||
<option value="social-worker">Social worker / case manager</option>
|
||
<option value="partner">Community partner / agency</option>
|
||
<option value="faith-leader">Faith leader / pastor</option>
|
||
</select>
|
||
{errors['contactInfo.relationship'] && <p id="err-contact-relationship" className="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{errors['contactInfo.relationship']}</p>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Needs Assessment */}
|
||
<div className="card">
|
||
<h3 className="font-semibold text-lg mb-4">What kind of support is needed? (Select all that apply)</h3>
|
||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||
{[
|
||
{ key: 'schoolSupplies', label: 'Essentials & supplies', icon: Package, desc: 'Food, hygiene, household basics' },
|
||
{ key: 'clothing', label: 'Clothing', icon: Shirt, desc: 'Seasonal clothing and dignity items' },
|
||
{ key: 'shoes', label: 'Wellness support', icon: Heart, desc: 'Grief, trauma, mental health navigation' },
|
||
{ key: 'backpack', label: 'Resource navigation', icon: BookOpenText, desc: 'Help connecting to services' },
|
||
{ key: 'emergency', label: 'Emergency assistance', icon: CreditCard, desc: 'Urgent financial or crisis needs' },
|
||
{ key: 'other', label: 'Other', icon: Plus, desc: 'Describe below' }
|
||
].map(need => (
|
||
<label key={need.key} className="flex items-start gap-3 p-3 border border-neutral-200 dark:border-neutral-700 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-800/50 cursor-pointer transition-colors">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.needs[need.key as keyof typeof formData.needs] as boolean}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
needs: {...formData.needs, [need.key]: e.target.checked}
|
||
})}
|
||
className="mt-1 h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||
/>
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<need.icon className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||
<span className="font-medium text-sm">{need.label}</span>
|
||
</div>
|
||
<div className="text-xs text-neutral-600 dark:text-neutral-400">{need.desc}</div>
|
||
</div>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Additional Details */}
|
||
<div className="card">
|
||
<h3 className="font-semibold text-lg mb-4">Additional Information</h3>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" htmlFor="details">
|
||
Describe the situation and specific needs (required)
|
||
</label>
|
||
<textarea
|
||
id="details"
|
||
required
|
||
rows={4}
|
||
className={`input resize-none ${errors.details ? 'border-red-500 dark:border-red-400' : ''}`}
|
||
placeholder="Please describe the situation, what support is needed, urgency, and any relevant details (omit sensitive identifiers you do not wish to share)..."
|
||
value={formData.details}
|
||
onChange={(e) => {
|
||
setFormData({ ...formData, details: e.target.value })
|
||
setFieldError('details', e.target.value)
|
||
}}
|
||
onBlur={() => setFieldError('details', formData.details)}
|
||
aria-invalid={!!errors.details}
|
||
aria-describedby={errors.details ? 'err-details' : undefined}
|
||
/>
|
||
{errors.details && <p id="err-details" className="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{errors.details}</p>}
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2">Urgency Level</label>
|
||
<div className="flex gap-3">
|
||
{[
|
||
{ value: 'low', label: 'Low', color: 'text-green-600', desc: 'Within 2 weeks' },
|
||
{ value: 'medium', label: 'Medium', color: 'text-yellow-600', desc: 'Within 1 week' },
|
||
{ value: 'high', label: 'High', color: 'text-red-600', desc: 'Within 2-3 days' }
|
||
].map(urgency => (
|
||
<label key={urgency.value} className="flex items-center gap-2 cursor-pointer">
|
||
<input
|
||
type="radio"
|
||
name="urgency"
|
||
value={urgency.value}
|
||
checked={formData.urgency === urgency.value}
|
||
onChange={(e) => setFormData({...formData, urgency: e.target.value})}
|
||
className="h-4 w-4 text-primary-600 focus:ring-primary-500"
|
||
/>
|
||
<span className={`font-medium ${urgency.color}`}>{urgency.label}</span>
|
||
<span className="text-xs text-neutral-500">({urgency.desc})</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Submit */}
|
||
<div className="card bg-primary-50 dark:bg-primary-900/20">
|
||
{draftSaved && <p className="text-sm text-primary-700 dark:text-primary-300 mb-2" role="status">Draft saved. You can leave and return later.</p>}
|
||
{submitError && (
|
||
<div className="mb-4 p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800" role="alert">
|
||
<p className="text-sm text-red-800 dark:text-red-200">{submitError}</p>
|
||
</div>
|
||
)}
|
||
<div className="flex items-start gap-4 mb-6">
|
||
<Shield className="h-6 w-6 text-primary-600 dark:text-primary-400 mt-1" />
|
||
<div>
|
||
<h3 className="font-semibold text-primary-900 dark:text-primary-100 mb-2">Privacy & Next Steps</h3>
|
||
<ul className="text-sm text-primary-800 dark:text-primary-200 space-y-1">
|
||
<li>• All information is kept confidential and secure</li>
|
||
<li>• We will contact you within 24-48 hours to verify details</li>
|
||
<li>• Assistance is provided at no cost to families</li>
|
||
<li>• We may coordinate with trusted community partners when appropriate</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col sm:flex-row gap-4">
|
||
<motion.button
|
||
type="submit"
|
||
disabled={isSubmitting}
|
||
className="flex-1 btn-primary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:opacity-70"
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
>
|
||
<ClipboardList className="mr-2 h-5 w-5" />
|
||
{isSubmitting ? 'Submitting…' : 'Submit Request'}
|
||
</motion.button>
|
||
|
||
<a
|
||
href="tel:+18184916884"
|
||
className="btn-secondary text-center focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||
>
|
||
<Phone className="mr-2 h-4 w-4" />
|
||
Call for Urgent Needs
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</PageShell>
|
||
)
|
||
}
|
||
|
||
// 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"
|
||
>
|
||
<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="your.email@example.com"
|
||
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 right-4 z-50 md: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-4 left-4 right-4 z-50 card p-4 md:max-w-md md:right-auto" role="dialog" aria-label="Cookie consent">
|
||
<p className="text-sm">
|
||
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>
|
||
)
|
||
}
|
||
|
||
function Magnetic({ children }: { children: React.ReactNode }) {
|
||
return (
|
||
<div className="relative">
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/* ===================== Main App Component ===================== */
|
||
export default function App() {
|
||
return (
|
||
<LanguageProvider>
|
||
<NotificationProvider>
|
||
<AuthProvider>
|
||
<AppContent />
|
||
<PWAInstallPrompt />
|
||
</AuthProvider>
|
||
</NotificationProvider>
|
||
</LanguageProvider>
|
||
)
|
||
}
|
||
|
||
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(() => {
|
||
const handleHashChange = () => setCurrentPath(window.location.hash.slice(1) || '/')
|
||
window.addEventListener('hashchange', handleHashChange)
|
||
return () => window.removeEventListener('hashchange', handleHashChange)
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
if (darkMode) {
|
||
document.documentElement.classList.add('dark')
|
||
} else {
|
||
document.documentElement.classList.remove('dark')
|
||
}
|
||
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 <HomePage />
|
||
case '/donate':
|
||
return <DonatePage />
|
||
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 <AssistanceRequestPage />
|
||
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 <AIPortalPage />
|
||
case '/advanced-analytics':
|
||
return <AdvancedAnalyticsPage />
|
||
case '/mobile-volunteer':
|
||
return <MobileVolunteerPage />
|
||
case '/staff-training':
|
||
return <StaffTrainingPage />
|
||
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>
|
||
)}
|
||
|
||
<header className="sticky top-0 z-50 backdrop-blur supports-[backdrop-filter]:bg-white/50 dark:supports-[backdrop-filter]:bg-black/40 border-b border-white/30 dark:border-white/10">
|
||
<Navigation
|
||
darkMode={darkMode}
|
||
setDarkMode={setDarkMode}
|
||
mobileMenuOpen={isMobileMenuOpen}
|
||
setMobileMenuOpen={setIsMobileMenuOpen}
|
||
/>
|
||
</header>
|
||
|
||
<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) && <MimChatAgent currentPath={currentPath} />}
|
||
<StickyDonate />
|
||
<CookieBanner />
|
||
</div>
|
||
)
|
||
}
|