- Added SalesforceConnector for handling Salesforce authentication, case creation, and updates. - Integrated real-time processing capabilities with StudentAssistanceAI and Salesforce. - Implemented WebSocket support for real-time updates and request handling. - Enhanced metrics tracking for processing performance and sync status. - Added error handling and retry logic for processing requests. - Created factory function for easy initialization of RealTimeProcessor with default configurations.
4505 lines
187 KiB
TypeScript
4505 lines
187 KiB
TypeScript
import React, { useState, useEffect, useRef, useContext, createContext } from 'react'
|
||
import { motion, useMotionValue, useSpring, useTransform, useScroll, AnimatePresence } from 'framer-motion'
|
||
import {
|
||
ArrowRight,
|
||
Backpack,
|
||
CheckCircle2,
|
||
Facebook,
|
||
Globe,
|
||
Heart,
|
||
Instagram,
|
||
Mail,
|
||
MapPin,
|
||
Menu,
|
||
Moon,
|
||
Phone,
|
||
Shirt,
|
||
Sparkles,
|
||
Star,
|
||
SunMedium,
|
||
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'
|
||
|
||
/**
|
||
* 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: 'Equipping kids for success—school supplies, clothing, & more',
|
||
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.students': { en: 'Students Supported', es: 'Estudiantes Apoyados', fr: 'Étudiants Soutenus' }
|
||
}
|
||
|
||
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 students = Math.floor(amount / 25) // $25 per student for basic supplies
|
||
const families = Math.floor(amount / 50) // $50 per family for comprehensive support
|
||
const backpacks = Math.floor(amount / 30) // $30 for complete backpack kit
|
||
const clothing = Math.floor(amount / 45) // $45 for clothing items
|
||
const emergency = Math.floor(amount / 75) // $75 for emergency assistance
|
||
|
||
return {
|
||
students,
|
||
families,
|
||
backpacks,
|
||
clothing,
|
||
emergency,
|
||
annual: {
|
||
students: Math.floor((amount * 12) / 25),
|
||
families: Math.floor((amount * 12) / 50),
|
||
totalImpact: `${Math.floor((amount * 12) / 25)} students 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 || 'Miracles in Motion - Equipping Students for Success')
|
||
updateOGTag('og:description', description || 'Nonprofit providing students with school supplies, clothing, and emergency assistance to thrive in their education.')
|
||
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 TiltCardProps {
|
||
icon: React.ComponentType<IconProps>
|
||
title: string
|
||
desc: string
|
||
}
|
||
|
||
interface FeatureCardProps {
|
||
icon: React.ComponentType<IconProps>
|
||
title: string
|
||
body: string
|
||
}
|
||
|
||
interface StatProps {
|
||
label: string
|
||
value: number
|
||
}
|
||
|
||
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>
|
||
)
|
||
}
|
||
|
||
interface NavProps {
|
||
darkMode: boolean
|
||
setDarkMode: (value: boolean) => void
|
||
mobileMenuOpen: boolean
|
||
setMobileMenuOpen: (value: boolean) => void
|
||
}
|
||
|
||
function Nav({ darkMode, setDarkMode, mobileMenuOpen, setMobileMenuOpen }: NavProps) {
|
||
// Close mobile menu when route changes
|
||
useEffect(() => {
|
||
setMobileMenuOpen(false)
|
||
}, [window.location.hash])
|
||
|
||
// Handle keyboard navigation
|
||
const handleKeyDown = (e: React.KeyboardEvent, action: () => void) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault()
|
||
action()
|
||
}
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<nav className="mx-auto flex w-full max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8" role="navigation" aria-label="Main navigation">
|
||
<div className="flex items-center gap-3">
|
||
<a
|
||
href="#/"
|
||
className="flex items-center gap-3 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded-lg p-1"
|
||
aria-label="Miracles in Motion - Home"
|
||
>
|
||
<LogoMark />
|
||
<div className="-space-y-1">
|
||
<div className="font-semibold tracking-tight">Miracles in Motion</div>
|
||
<div className="text-xs text-neutral-600 dark:text-neutral-400">Essentials for every student</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
|
||
{/* Desktop Navigation */}
|
||
<div className="hidden items-center gap-6 md:flex">
|
||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/stories" aria-label="Read success stories">Stories</a>
|
||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/testimonies" aria-label="View testimonies">Testimonies</a>
|
||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/volunteers" aria-label="Volunteer opportunities">Volunteers</a>
|
||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/sponsors" aria-label="Corporate partnerships">Corporate</a>
|
||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/request-assistance" aria-label="Request assistance">Get Help</a>
|
||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/portals" aria-label="Portal login">Portals</a>
|
||
</div>
|
||
|
||
{/* Desktop Actions */}
|
||
<div className="hidden md:flex items-center gap-3">
|
||
<Magnetic>
|
||
<a
|
||
href="#/donate"
|
||
className="btn-primary focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||
aria-label="Make a donation"
|
||
>
|
||
<Heart className="mr-2 h-4 w-4" aria-hidden="true" /> Donate
|
||
</a>
|
||
</Magnetic>
|
||
<button
|
||
aria-label={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
||
onClick={() => setDarkMode(!darkMode)}
|
||
onKeyDown={(e) => handleKeyDown(e, () => setDarkMode(!darkMode))}
|
||
className="group rounded-full border border-neutral-200/70 bg-white/70 p-2 shadow-sm transition hover:scale-105 hover:bg-white dark:border-white/10 dark:bg-white/10 dark:hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||
>
|
||
{darkMode ? (
|
||
<SunMedium className="h-5 w-5 transition group-hover:rotate-12" aria-hidden="true" />
|
||
) : (
|
||
<Moon className="h-5 w-5 transition group-hover:-rotate-12" aria-hidden="true" />
|
||
)}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Mobile Actions */}
|
||
<div className="flex md:hidden items-center gap-2">
|
||
<Magnetic>
|
||
<a
|
||
href="#/donate"
|
||
className="btn-primary text-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||
aria-label="Make a donation"
|
||
>
|
||
<Heart className="h-4 w-4" aria-hidden="true" />
|
||
</a>
|
||
</Magnetic>
|
||
<button
|
||
aria-label={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
||
onClick={() => setDarkMode(!darkMode)}
|
||
className="rounded-full border border-neutral-200/70 bg-white/70 p-2 shadow-sm transition hover:scale-105 hover:bg-white dark:border-white/10 dark:bg-white/10 dark:hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||
>
|
||
{darkMode ? (
|
||
<SunMedium className="h-4 w-4" aria-hidden="true" />
|
||
) : (
|
||
<Moon className="h-4 w-4" aria-hidden="true" />
|
||
)}
|
||
</button>
|
||
<button
|
||
aria-label={mobileMenuOpen ? 'Close navigation menu' : 'Open navigation menu'}
|
||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||
onKeyDown={(e) => handleKeyDown(e, () => setMobileMenuOpen(!mobileMenuOpen))}
|
||
className="rounded-full border border-neutral-200/70 bg-white/70 p-2 shadow-sm transition hover:scale-105 hover:bg-white dark:border-white/10 dark:bg-white/10 dark:hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||
aria-expanded={mobileMenuOpen}
|
||
aria-controls="mobile-menu"
|
||
>
|
||
{mobileMenuOpen ? (
|
||
<X className="h-5 w-5" aria-hidden="true" />
|
||
) : (
|
||
<Menu className="h-5 w-5" aria-hidden="true" />
|
||
)}
|
||
</button>
|
||
</div>
|
||
</nav>
|
||
|
||
{/* Mobile Menu */}
|
||
<motion.div
|
||
id="mobile-menu"
|
||
className="md:hidden"
|
||
initial={false}
|
||
animate={{
|
||
height: mobileMenuOpen ? 'auto' : 0,
|
||
opacity: mobileMenuOpen ? 1 : 0
|
||
}}
|
||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||
style={{ overflow: 'hidden' }}
|
||
role="region"
|
||
aria-label="Mobile navigation menu"
|
||
>
|
||
<div className="border-t border-neutral-200/50 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur">
|
||
<div className="max-w-7xl mx-auto px-4 py-4 space-y-3">
|
||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/stories">Success Stories</a>
|
||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/testimonies">Testimonies</a>
|
||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/volunteers">Volunteer</a>
|
||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/sponsors">Corporate Partners</a>
|
||
<a className="block py-3 px-4 text-lg font-medium text-emerald-900 dark:text-emerald-100 bg-emerald-50 dark:bg-emerald-900/20 hover:bg-emerald-100 dark:hover:bg-emerald-900/30 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/request-assistance">
|
||
<ClipboardList className="inline mr-2 h-5 w-5" aria-hidden="true" />
|
||
Request Assistance
|
||
</a>
|
||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/portals">Staff & Partner Portals</a>
|
||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/legal">Legal & Privacy</a>
|
||
<div className="pt-2 border-t border-neutral-200/50 dark:border-white/10">
|
||
<a className="block py-3 px-4 bg-primary-600 text-white font-semibold rounded-lg hover:bg-primary-700 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/donate">
|
||
<Heart className="inline mr-2 h-5 w-5" aria-hidden="true" />
|
||
Donate Now
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</>
|
||
)
|
||
}
|
||
|
||
function LogoMark() {
|
||
return (
|
||
<div className="relative grid h-10 w-10 place-items-center overflow-hidden rounded-2xl bg-gradient-to-br from-primary-500 via-secondary-500 to-secondary-600 shadow-lg shadow-primary-500/20">
|
||
<motion.div
|
||
className="absolute inset-0 opacity-60"
|
||
animate={{
|
||
background: [
|
||
"radial-gradient(120px 80px at 20% 20%, rgba(255,255,255,0.4), transparent)",
|
||
"radial-gradient(120px 80px at 80% 30%, rgba(255,255,255,0.4), transparent)",
|
||
"radial-gradient(120px 80px at 50% 80%, rgba(255,255,255,0.4), transparent)",
|
||
]
|
||
}}
|
||
transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }}
|
||
/>
|
||
<Sparkles className="relative h-6 w-6 text-white drop-shadow" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/* ===================== Home Page ===================== */
|
||
function HomePage() {
|
||
return (
|
||
<>
|
||
<Hero />
|
||
<PartnerMarquee />
|
||
<Programs />
|
||
<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%', '50%'])
|
||
const contentY = useTransform(scrollYProgress, [0, 1], ['0%', '100%'])
|
||
|
||
return (
|
||
<section ref={containerRef} className="relative isolate overflow-hidden min-h-screen flex items-center">
|
||
{/* Enhanced 3D Background Layers */}
|
||
<motion.div
|
||
className="absolute inset-0 bg-gradient-to-br from-primary-50/30 via-white to-secondary-50/30 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>
|
||
|
||
{/* Content Layer with Parallax */}
|
||
<motion.div
|
||
className="mx-auto grid max-w-7xl items-center gap-16 px-4 py-20 sm:px-6 lg:grid-cols-12 lg:gap-8 lg:py-28 lg:px-8 relative z-10"
|
||
style={{ y: contentY }}
|
||
>
|
||
<div className="lg:col-span-7">
|
||
<ParallaxContainer depth={0.3}>
|
||
<motion.h1
|
||
initial={{ opacity: 0, y: 12 }}
|
||
whileInView={{ opacity: 1, y: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.6 }}
|
||
className="text-balance text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl transform-gpu"
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
Equipping kids for success—
|
||
<span className="relative whitespace-pre gradient-text"> school supplies, clothing, & more</span>
|
||
</motion.h1>
|
||
<p className="mt-5 max-w-xl text-lg leading-7 text-neutral-700 dark:text-neutral-300">
|
||
Miracles in Motion is a nonprofit providing students with the essentials they need to thrive: backpacks and notebooks, uniforms and shoes, and the everything-else fund for urgent needs.
|
||
</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="#/stories" className="rounded-full border border-transparent px-4 py-2 text-sm text-neutral-700 underline-offset-4 hover:underline dark:text-neutral-300">
|
||
Read stories
|
||
</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-emerald-500"/> 501(c)(3) public charity</li>
|
||
<li className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-emerald-500"/> Donations tax-deductible</li>
|
||
<li className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-emerald-500"/> Community-driven impact</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() {
|
||
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">
|
||
<TiltCard icon={Backpack} title="School Supplies" desc="Backpacks, notebooks, calculators" />
|
||
<TiltCard icon={Shirt} title="School Clothing" desc="Uniforms, shoes, coats" />
|
||
<TiltCard icon={Sparkles} title="Everything Else" desc="Glasses, fees, emergencies" />
|
||
<TiltCard icon={Users} title="Family Support" desc="Referrals & wraparound care" />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function TiltCard({ icon: Icon, title, desc }: TiltCardProps) {
|
||
const x = useMotionValue(0)
|
||
const y = useMotionValue(0)
|
||
const rx = useTransform(y, [-50, 50], [12, -12])
|
||
const ry = useTransform(x, [-50, 50], [-12, 12])
|
||
const springX = useSpring(rx, { stiffness: 150, damping: 15 })
|
||
const springY = useSpring(ry, { stiffness: 150, damping: 15 })
|
||
const scale = useSpring(1, { stiffness: 300, damping: 25 })
|
||
|
||
return (
|
||
<motion.div
|
||
onMouseMove={(e) => {
|
||
const rect = e.currentTarget.getBoundingClientRect()
|
||
x.set(e.clientX - rect.left - rect.width / 2)
|
||
y.set(e.clientY - rect.top - rect.height / 2)
|
||
scale.set(1.05)
|
||
}}
|
||
onMouseLeave={() => {
|
||
x.set(0)
|
||
y.set(0)
|
||
scale.set(1)
|
||
}}
|
||
style={{
|
||
rotateX: springX,
|
||
rotateY: springY,
|
||
scale: scale,
|
||
transformStyle: "preserve-3d",
|
||
boxShadow: useTransform(scale, [1, 1.05], [
|
||
"0 4px 6px -1px rgba(0, 0, 0, 0.1)",
|
||
"0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.04)"
|
||
])
|
||
}}
|
||
className="group card card-hover will-change-transform cursor-pointer"
|
||
whileHover={{
|
||
z: 20
|
||
}}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(150px_80px_at_var(--x)_var(--y),_rgba(255,255,255,0.35),_transparent)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||
<div className="flex items-start gap-4" style={{ transform: "translateZ(40px)" }}>
|
||
<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-lg">
|
||
<Icon className="h-6 w-6" />
|
||
</div>
|
||
<div>
|
||
<div className="font-semibold tracking-tight">{title}</div>
|
||
<div className="text-sm text-neutral-600 dark:text-neutral-300">{desc}</div>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 flex items-center gap-2 text-sm text-primary-600 transition group-hover:translate-x-1 dark:text-primary-300">
|
||
Learn more <ArrowRight className="h-4 w-4" />
|
||
</div>
|
||
</motion.div>
|
||
)
|
||
}
|
||
|
||
function PartnerMarquee() {
|
||
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" /> Trusted by schools & community partners
|
||
</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" />
|
||
Lincoln Unified • Sunrise Elementary • Northview High • Rotary Club • City Youth Fund • BookBank • Caring Co.
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function Programs() {
|
||
const items = [
|
||
{ icon: Backpack, title: "School Supplies", body: "Backpacks, notebooks, art kits, calculators—ready for day one." },
|
||
{ icon: Shirt, title: "School Clothing", body: "Uniforms, shoes, coats, and seasonal essentials in all sizes." },
|
||
{ icon: Sparkles, title: "Everything Else", body: "Glasses, test fees, activity passes, hygiene kits—fast relief when needed." },
|
||
]
|
||
|
||
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="Practical, student-first programs" subtitle="Flexible help that meets families where they are." />
|
||
</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>
|
||
</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>
|
||
<motion.div
|
||
className="mt-5 text-sm text-primary-700 transition dark:text-primary-300 flex items-center"
|
||
whileHover={{ x: 8 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
Explore
|
||
<motion.div
|
||
whileHover={{ x: 4 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<ArrowRight className="ml-1 h-4 w-4" />
|
||
</motion.div>
|
||
</motion.div>
|
||
</motion.div>
|
||
)
|
||
}
|
||
|
||
function Impact() {
|
||
const stats = [
|
||
{ label: "Students equipped", value: 4200 },
|
||
{ label: "Schools partnered", value: 38 },
|
||
{ label: "Avg. response time (hrs)", value: 24 },
|
||
{ label: "Counties served", value: 6 },
|
||
]
|
||
|
||
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="Impact" title="Every gift moves a student forward" subtitle="Transparent, measurable outcomes powered by local partnerships." />
|
||
</ParallaxContainer>
|
||
|
||
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||
{stats.map((s, i) => (
|
||
<ParallaxContainer key={i} depth={0.3 + i * 0.05}>
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 30, rotateX: -15 }}
|
||
whileInView={{ opacity: 1, y: 0, rotateX: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.8, delay: i * 0.15 }}
|
||
whileHover={{
|
||
scale: 1.05,
|
||
rotateY: 8,
|
||
z: 60,
|
||
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)"
|
||
}}
|
||
style={{ transformStyle: 'preserve-3d' }}
|
||
>
|
||
<Stat label={s.label} value={s.value} />
|
||
</motion.div>
|
||
</ParallaxContainer>
|
||
))}
|
||
</div>
|
||
<div className="mt-10 grid items-center gap-6 rounded-2xl border border-emerald-500/20 bg-emerald-500/10 p-6 sm:grid-cols-2">
|
||
<div className="text-sm text-emerald-900 dark:text-emerald-100">
|
||
<div className="font-medium">Where your donation goes</div>
|
||
<ul className="mt-3 space-y-2">
|
||
<li className="flex items-center gap-2"><span className="h-1.5 w-1.5 rounded-full bg-emerald-500"/> 85% programs • 10% ops • 5% fundraising</li>
|
||
<li className="flex items-center gap-2"><span className="h-1.5 w-1.5 rounded-full bg-emerald-500"/> Average grant: $48 for supplies; $72 for clothing</li>
|
||
</ul>
|
||
</div>
|
||
<div className="flex items-center justify-end">
|
||
<a href="#/donate" className="btn-primary">
|
||
See the impact report <ArrowRight className="ml-2 h-4 w-4" />
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function Stat({ label, value }: StatProps) {
|
||
return (
|
||
<div className="card text-center">
|
||
<div className="absolute -right-10 -top-10 h-28 w-28 rounded-full bg-gradient-to-br from-primary-500/20 to-secondary-600/20 blur-2xl" />
|
||
<div className="text-4xl font-semibold tracking-tight">
|
||
<AnimatedNumber value={value} />
|
||
</div>
|
||
<div className="mt-2 text-sm text-neutral-600 dark:text-neutral-300">{label}</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function AnimatedNumber({ value }: { value: number }) {
|
||
const mv = useMotionValue(0)
|
||
const spring = useSpring(mv, { stiffness: 90, damping: 15 })
|
||
const rounded = useTransform(spring, (latest) => Math.floor(latest).toLocaleString())
|
||
|
||
useEffect(() => {
|
||
mv.set(0)
|
||
const timeout = setTimeout(() => mv.set(value), 150)
|
||
return () => clearTimeout(timeout)
|
||
}, [value, mv])
|
||
|
||
return <motion.span>{rounded}</motion.span>
|
||
}
|
||
|
||
function HowItWorks() {
|
||
const steps = [
|
||
{ title: "Referral", body: "A school counselor, social worker, or parent submits a simple request." },
|
||
{ title: "Fast response", body: "Needs are verified same-day and approved within 24 hours." },
|
||
{ title: "Fulfillment", body: "We purchase items locally or provide vouchers for pickup." },
|
||
{ title: "Follow-up", body: "We confirm support reached the student and log outcomes." },
|
||
]
|
||
|
||
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" title="Simple process, real results" subtitle="Designed with counselors, optimized for speed and dignity." />
|
||
</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-emerald-500/20 bg-emerald-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.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-emerald-500 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-emerald-900 dark:text-emerald-100">Your gift funds direct student needs first. We publish anonymized impact and receipts for transparency.</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 counselors</div>
|
||
</div>
|
||
<p className="mt-2 text-sm text-secondary-950 dark:text-secondary-100">One-page referral. No uploads required. We do the running so students don't miss class.</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">
|
||
"A student arrived with torn shoes before winter break. Within 24 hours, Miracles in Motion had a new coat and shoes ready. He came back from break beaming."
|
||
</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-amber-500" />
|
||
Counselor, Northview High
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function GetInvolved() {
|
||
const options = [
|
||
{ title: "Donate", body: "Fuel fast responses for urgent student needs.", href: "#/donate", accent: "from-rose-500 to-primary-500", icon: Heart },
|
||
{ title: "Volunteer", body: "Assemble kits, deliver items, host a drive.", href: "#/volunteers", accent: "from-sky-500 to-secondary-500", icon: Users },
|
||
{ title: "Corporate sponsor", body: "Schools and community orgs: let's team up.", href: "#/sponsors", accent: "from-emerald-500 to-lime-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's a role for everyone" subtitle="Give monthly, share skills, or introduce us to your school." />
|
||
</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="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">Help a student start school with pride</h2>
|
||
<p className="mt-3 max-w-md text-white/90">Your gift today equips a child with the essentials they need—fast. $48 fills a backpack; $72 outfits a student with shoes and a coat.</p>
|
||
<div className="mt-6 flex flex-wrap items-center gap-3">
|
||
<a href="#/donate" className="btn-white">Donate $48</a>
|
||
<a href="#/donate" 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">
|
||
Give monthly <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">Prefer offline?</h3>
|
||
<ul className="mt-3 space-y-2 text-sm text-white/90">
|
||
<li className="flex items-center gap-2"><MapPin className="h-4 w-4"/> 20274 Via Medici, Porter Ranch, CA 91326</li>
|
||
<li className="flex items-center gap-2"><Mail className="h-4 w-4"/> contact@mim4u.org</li>
|
||
<li className="flex items-center gap-2"><Phone className="h-4 w-4"/> (818) 491-6884</li>
|
||
</ul>
|
||
<p className="mt-3 text-xs text-white/80">Miracles in Motion is a 501(c)(3). EIN 12-3456789.</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: "School supplies for 1 student", popular: false },
|
||
{ amount: 50, impact: "Complete backpack kit for 1 student", popular: true },
|
||
{ amount: 100, impact: "School clothing for 2 students", popular: false },
|
||
{ amount: 250, impact: "Emergency fund for 5 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.students > 0) impactItems.push(`${impact.students} student${impact.students > 1 ? 's' : ''} with supplies`)
|
||
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 a student 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 - Help Students in Need"
|
||
description="Make a secure donation to Miracles in Motion. Your gift provides school supplies, clothing, and emergency assistance directly to students who need it most."
|
||
/>
|
||
<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">
|
||
<Backpack className="h-4 w-4" /> Students Supported
|
||
</span>
|
||
<span className="font-semibold">{impactData.students}</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" /> Backpack 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 students 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 12-3456789.
|
||
</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: 12-3456789.
|
||
</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">127 students</span> received school supplies 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="mailto:contact@mim4u.org" className="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300">
|
||
contact@mim4u.org
|
||
</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>School liaison</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="#">Download prospectus</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: "Ready on Day One", tag: "Supplies", body: "A 3rd grader received a backpack and art kit, returning to class with pride.", by: "Ms. Lee, Teacher" },
|
||
{ title: "Warm for Winter", tag: "Clothing", body: "A high schooler got a coat and shoes before a cold snap—attendance rebounded.", by: "School Social Worker" },
|
||
{ title: "Glasses for Growth", tag: "Everything Else", body: "A student's new glasses improved reading in two weeks.", by: "Counselor" },
|
||
]
|
||
|
||
return (
|
||
<PageShell title="Stories" icon={BookOpenText} eyebrow="Impact in motion" cta={<a href="#/testimonies" className="btn-secondary">Read testimonies</a>}>
|
||
<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 (no private student info)" 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 student information.
|
||
</label>
|
||
<button className="btn-primary w-full justify-center">Submit</button>
|
||
</form>
|
||
</div>
|
||
</PageShell>
|
||
)
|
||
}
|
||
|
||
function TestimoniesPage() {
|
||
const items = [
|
||
{ who: "Parent", quote: "The uniform voucher saved us right before school started." },
|
||
{ who: "Counselor", quote: "The 24-hr turnaround is unmatched for our families." },
|
||
{ 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 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. You may request access, correction, or deletion at <a className="underline" href="mailto:privacy@mim4u.org">privacy@mim4u.org</a>.</p>
|
||
</PolicySection>
|
||
<PolicySection id="donor-privacy" title="Donor Privacy Policy">
|
||
<p>We never sell, share, or trade donor names or personal information with any other entity, nor send donor mailings on behalf of other organizations. This policy applies to all information received online and offline.</p>
|
||
</PolicySection>
|
||
<PolicySection id="terms" title="Terms of Use">
|
||
<ul className="list-disc pl-5 space-y-2">
|
||
<li>Content is provided "as is" for informational purposes; no warranties.</li>
|
||
<li>By submitting content (stories/testimonies), you grant us a non-exclusive license to edit and publish.</li>
|
||
<li>Unauthorized scraping or misuse of site content is prohibited.</li>
|
||
</ul>
|
||
</PolicySection>
|
||
<PolicySection id="cookies" title="Cookie Policy">
|
||
<p>We use essential cookies for site functionality and, with your consent, privacy-friendly analytics. You can change your choices via the cookie banner or by clearing site data.</p>
|
||
</PolicySection>
|
||
<PolicySection id="refunds" title="Refund Policy">
|
||
<p>Donations are generally non-refundable. If you believe a donation was made in error, contact <a className="underline" href="mailto:donate@mim4u.org">donate@mim4u.org</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, new school supplies/clothing, DAF grants, and publicly traded securities.</li>
|
||
<li>In-kind items must be new and appropriate for school settings. 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 students 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">access@miraclesinmotion.org</a>.</p>
|
||
</PolicySection>
|
||
<PolicySection id="financials" title="Financials & 990">
|
||
<p>Miracles in Motion is a 501(c)(3) nonprofit. EIN 12-3456789. 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 brand guidelines.</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>
|
||
)
|
||
}
|
||
|
||
// Assistance Request Page
|
||
function AssistanceRequestPage() {
|
||
const [formData, setFormData] = useState({
|
||
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'
|
||
})
|
||
|
||
const handleSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
alert('Request submitted successfully! We will contact you within 24-48 hours.')
|
||
}
|
||
|
||
return (
|
||
<PageShell title="Request Assistance" icon={ClipboardList} eyebrow="Get help for students in need">
|
||
<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">
|
||
{/* Request Type */}
|
||
<div className="card">
|
||
<h3 className="font-semibold text-lg mb-4">Type of Request</h3>
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||
{[
|
||
{ id: 'individual', label: 'Individual Student', icon: Users, desc: 'Help for one student' },
|
||
{ id: 'family', label: 'Family (Multiple)', icon: HomeIcon, desc: 'Multiple children' },
|
||
{ id: 'classroom', label: 'Classroom Need', icon: School, desc: 'Entire class project' },
|
||
{ id: 'emergency', label: 'Emergency', icon: AlertCircle, desc: 'Urgent assistance' }
|
||
].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'
|
||
: '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})}
|
||
className="sr-only"
|
||
/>
|
||
<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>
|
||
|
||
{/* Student Information */}
|
||
<div className="card">
|
||
<h3 className="font-semibold text-lg mb-4">Student Information</h3>
|
||
<div className="grid gap-4 sm:grid-cols-2">
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2">Student First Name *</label>
|
||
<input
|
||
required
|
||
type="text"
|
||
className="input"
|
||
value={formData.studentInfo.firstName}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
studentInfo: {...formData.studentInfo, firstName: e.target.value}
|
||
})}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2">Student Last Name *</label>
|
||
<input
|
||
required
|
||
type="text"
|
||
className="input"
|
||
value={formData.studentInfo.lastName}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
studentInfo: {...formData.studentInfo, lastName: e.target.value}
|
||
})}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2">Grade Level *</label>
|
||
<select
|
||
required
|
||
className="input"
|
||
value={formData.studentInfo.grade}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
studentInfo: {...formData.studentInfo, grade: e.target.value}
|
||
})}
|
||
>
|
||
<option value="">Select grade</option>
|
||
<option value="PreK">Pre-K</option>
|
||
<option value="K">Kindergarten</option>
|
||
{Array.from({length: 12}, (_, i) => (
|
||
<option key={i+1} value={`${i+1}`}>Grade {i+1}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2">School Name *</label>
|
||
<input
|
||
required
|
||
type="text"
|
||
className="input"
|
||
placeholder="Full school name"
|
||
value={formData.studentInfo.school}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
studentInfo: {...formData.studentInfo, school: e.target.value}
|
||
})}
|
||
/>
|
||
</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">Parent/Guardian Name *</label>
|
||
<input
|
||
required
|
||
type="text"
|
||
className="input"
|
||
value={formData.contactInfo.parentName}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
contactInfo: {...formData.contactInfo, parentName: e.target.value}
|
||
})}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2">Phone Number *</label>
|
||
<input
|
||
required
|
||
type="tel"
|
||
className="input"
|
||
placeholder="(555) 123-4567"
|
||
value={formData.contactInfo.phone}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
contactInfo: {...formData.contactInfo, phone: e.target.value}
|
||
})}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2">Email Address *</label>
|
||
<input
|
||
required
|
||
type="email"
|
||
className="input"
|
||
value={formData.contactInfo.email}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
contactInfo: {...formData.contactInfo, email: e.target.value}
|
||
})}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2">Relationship to Student *</label>
|
||
<select
|
||
required
|
||
className="input"
|
||
value={formData.contactInfo.relationship}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
contactInfo: {...formData.contactInfo, relationship: e.target.value}
|
||
})}
|
||
>
|
||
<option value="">Select relationship</option>
|
||
<option value="parent">Parent</option>
|
||
<option value="guardian">Legal Guardian</option>
|
||
<option value="relative">Relative</option>
|
||
<option value="counselor">School Counselor</option>
|
||
<option value="social-worker">Social Worker</option>
|
||
<option value="teacher">Teacher</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Needs Assessment */}
|
||
<div className="card">
|
||
<h3 className="font-semibold text-lg mb-4">What does the student need? (Select all that apply)</h3>
|
||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||
{[
|
||
{ key: 'schoolSupplies', label: 'School Supplies', icon: BookOpenText, desc: 'Notebooks, pencils, folders, etc.' },
|
||
{ key: 'clothing', label: 'School Clothing', icon: Shirt, desc: 'Uniforms, appropriate attire' },
|
||
{ key: 'shoes', label: 'Shoes', icon: Package, desc: 'School-appropriate footwear' },
|
||
{ key: 'backpack', label: 'Backpack', icon: Backpack, desc: 'New backpack or bag' },
|
||
{ key: 'emergency', label: 'Emergency Fund', icon: CreditCard, desc: 'Fees, glasses, urgent needs' },
|
||
{ key: 'other', label: 'Other', icon: Plus, desc: 'Specify in description 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">
|
||
Describe the situation and specific needs (required)
|
||
</label>
|
||
<textarea
|
||
required
|
||
rows={4}
|
||
className="input resize-none"
|
||
placeholder="Please provide details about the student's situation, specific items needed, sizes if applicable, and any special circumstances..."
|
||
value={formData.details}
|
||
onChange={(e) => setFormData({...formData, details: e.target.value})}
|
||
/>
|
||
</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">
|
||
<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 work directly with schools when possible</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col sm:flex-row gap-4">
|
||
<motion.button
|
||
type="submit"
|
||
className="flex-1 btn-primary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
>
|
||
<ClipboardList className="mr-2 h-5 w-5" />
|
||
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-rose-50 dark:from-red-900/20 dark:to-rose-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-blue-50 to-sky-50 dark:from-blue-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 schools, social workers, and partner 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 student 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-purple-50 to-violet-50 dark:from-purple-900/20 dark:to-violet-900/20 border-purple-200 dark:border-purple-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-purple-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-purple-900 dark:text-purple-100">AI Assistance Portal</h3>
|
||
<p className="text-sm text-purple-700 dark:text-purple-300 mt-2">
|
||
AI-powered request matching and insights
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-sm text-purple-800 dark:text-purple-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-purple-600 text-white py-3 px-4 rounded-lg hover:bg-purple-700 transition-colors font-medium focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
|
||
>
|
||
Access AI Portal
|
||
</a>
|
||
</motion.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: support@mim4u.org</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: 'Porter Elementary', need: 'School supplies', priority: 'High', time: '2 hours ago' },
|
||
{ student: 'James R.', school: 'Valley Middle School', need: 'Winter clothing', priority: 'Medium', time: '4 hours ago' },
|
||
{ student: 'Ana L.', school: 'Northridge High', need: 'Backpack & supplies', 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: 'Kit Assembly - Elementary Supplies', location: 'Main Warehouse', students: 12 },
|
||
{ time: '1:00 PM', task: 'Delivery Route - Porter Ranch Area', location: 'Various Schools', students: 5 },
|
||
{ time: '3:30 PM', task: 'Inventory Count - Winter 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} students 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: 'Backpack, Notebooks', school: 'Valley Elementary', deadline: 'Tomorrow' },
|
||
{ student: 'Carlos R.', items: 'Winter jacket, Gloves', school: 'Oak Middle School', deadline: 'Friday' },
|
||
{ student: 'Emma K.', items: 'Art supplies kit', school: 'Northridge High', 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>Students 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="Resource center portal for submitting and tracking student 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 student 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: 'School supplies', submitted: '2 days ago', eta: 'Tomorrow' },
|
||
{ id: 'REQ-2024-0341', student: 'James Rodriguez', status: 'Approved', need: 'Winter clothing', submitted: '3 days ago', eta: 'Today' },
|
||
{ id: 'REQ-2024-0340', student: 'Ana Lopez', status: 'Delivered', need: 'Backpack & supplies', 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-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-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-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 border-purple-200 dark:border-purple-800"
|
||
whileHover={{ scale: 1.02, y: -2 }}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-purple-600 dark:text-purple-400">Students Helped</p>
|
||
<p className="text-2xl font-bold text-purple-900 dark:text-purple-100">1,847</p>
|
||
</div>
|
||
<Target className="h-8 w-8 text-purple-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-purple-600 dark:text-purple-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: 'Roosevelt Elementary', time: '15 minutes ago', icon: School },
|
||
{ 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-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||
<div className="flex items-center gap-3">
|
||
<AlertCircle className="h-6 w-6 text-purple-600" />
|
||
<span className="font-medium">Emergency Responses</span>
|
||
</div>
|
||
<span className="text-2xl font-bold text-purple-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 student assistance matching and insights for Miracles in Motion." />
|
||
<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>
|
||
)
|
||
}
|
||
|
||
function Footer() {
|
||
return (
|
||
<footer className="relative mt-24 border-t border-white/30 bg-white/50 backdrop-blur dark:border-white/10 dark:bg-white/5">
|
||
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||
<div className="grid gap-8 lg:grid-cols-4">
|
||
<div className="lg:col-span-2">
|
||
<div className="flex items-center gap-3">
|
||
<LogoMark />
|
||
<div>
|
||
<div className="font-semibold">Miracles in Motion</div>
|
||
<div className="text-sm text-neutral-600 dark:text-neutral-400">Essentials for every student</div>
|
||
</div>
|
||
</div>
|
||
<p className="mt-4 max-w-md text-sm text-neutral-600 dark:text-neutral-400">
|
||
A 501(c)(3) nonprofit providing students with school supplies, clothing, and emergency support to help them succeed.
|
||
</p>
|
||
<div className="mt-4 flex gap-4">
|
||
<a href="#" className="text-neutral-600 hover:text-primary-600 dark:text-neutral-400">
|
||
<Facebook className="h-5 w-5" />
|
||
</a>
|
||
<a href="#" className="text-neutral-600 hover:text-primary-600 dark:text-neutral-400">
|
||
<Instagram className="h-5 w-5" />
|
||
</a>
|
||
<a href="#" className="text-neutral-600 hover:text-primary-600 dark:text-neutral-400">
|
||
<Globe className="h-5 w-5" />
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h3 className="font-semibold">Get Involved</h3>
|
||
<ul className="mt-4 space-y-2 text-sm">
|
||
<li><a href="#/donate" className="navlink">Donate</a></li>
|
||
<li><a href="#/volunteers" className="navlink">Volunteer</a></li>
|
||
<li><a href="#/sponsors" className="navlink">Corporate Partnerships</a></li>
|
||
<li><a href="#/stories" className="navlink">Success Stories</a></li>
|
||
</ul>
|
||
</div>
|
||
<div>
|
||
<h3 className="font-semibold">Organization</h3>
|
||
<ul className="mt-4 space-y-2 text-sm">
|
||
<li><a href="#/testimonies" className="navlink">Testimonials</a></li>
|
||
<li><a href="#/legal" className="navlink">Legal & Policies</a></li>
|
||
<li><a href="mailto:contact@miraclesinmotion.org" className="navlink">Contact Us</a></li>
|
||
<li><a href="tel:+15551234567" className="navlink">(555) 123-4567</a></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div className="mt-8 border-t border-white/30 pt-8 text-center text-xs text-neutral-500 dark:border-white/10 dark:text-neutral-400">
|
||
<p>© 2024 Miracles in Motion. All rights reserved. EIN: 12-3456789</p>
|
||
<p className="mt-1">501(c)(3) nonprofit organization. Donations are tax-deductible to the extent allowed by law.</p>
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
)
|
||
}
|
||
|
||
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>
|
||
)
|
||
}
|
||
|
||
function CookieBanner() {
|
||
const [show, setShow] = useState(true)
|
||
|
||
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">
|
||
<p className="text-sm">
|
||
We use cookies to improve your experience. By continuing, you agree to our cookie policy.
|
||
</p>
|
||
<div className="mt-3 flex gap-2">
|
||
<button
|
||
onClick={() => setShow(false)}
|
||
className="btn-primary text-xs px-4 py-2"
|
||
>
|
||
Accept
|
||
</button>
|
||
<button
|
||
onClick={() => setShow(false)}
|
||
className="btn-secondary text-xs px-4 py-2"
|
||
>
|
||
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])
|
||
|
||
const renderPage = () => {
|
||
// Update document title based on current route
|
||
useEffect(() => {
|
||
document.title = currentPath === "/"
|
||
? "Miracles in Motion — Essentials for Every Student"
|
||
: `Miracles in Motion — ${currentPath.replace("/", "").replace(/-/g, " ").replace(/\b\w/g, (m) => m.toUpperCase())}`
|
||
}, [currentPath])
|
||
|
||
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 '/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">
|
||
<Nav
|
||
darkMode={darkMode}
|
||
setDarkMode={setDarkMode}
|
||
mobileMenuOpen={isMobileMenuOpen}
|
||
setMobileMenuOpen={setIsMobileMenuOpen}
|
||
/>
|
||
</header>
|
||
|
||
<main id="content" className="relative flex-1">
|
||
{renderPage()}
|
||
</main>
|
||
|
||
<Footer />
|
||
<StickyDonate />
|
||
<CookieBanner />
|
||
</div>
|
||
)
|
||
} |