Deploy to production - ensure all endpoints operational

This commit is contained in:
defiQUG
2025-11-12 08:17:28 -08:00
parent b421d2964c
commit f1c61c8339
171 changed files with 50830 additions and 42363 deletions
+4413 -4413
View File
File diff suppressed because it is too large Load Diff
+266 -266
View File
@@ -1,267 +1,267 @@
import React, { useState, useEffect } from 'react'
import { AnimatePresence } from 'framer-motion'
import { Sun, Moon, Menu, X } from 'lucide-react'
// Contexts
import { AuthProvider } from './contexts/AuthContext'
import { NotificationProvider } from './contexts/NotificationContext'
// Pages
import { HomePage } from './pages/HomePage'
import { DonatePage } from './pages/DonatePage'
// Components
import AIAssistancePortal from './components/AIAssistancePortal'
import AdvancedAnalyticsDashboard from './components/AdvancedAnalyticsDashboard'
import MobileVolunteerApp from './components/MobileVolunteerApp'
import StaffTrainingDashboard from './components/StaffTrainingDashboard'
// Hooks
import { useHashRoute } from './hooks/useCommon'
// Analytics
import { analytics } from './utils/analytics'
// Main App Component
const App: React.FC = () => {
const [darkMode, setDarkMode] = useState<boolean>(false)
const [mobileMenuOpen, setMobileMenuOpen] = useState<boolean>(false)
const { route } = useHashRoute()
// Initialize analytics
useEffect(() => {
analytics.init()
}, [])
// Dark mode toggle
const toggleDarkMode = (): void => {
setDarkMode(prev => {
const newMode = !prev
document.documentElement.classList.toggle('dark', newMode)
localStorage.setItem('darkMode', newMode.toString())
return newMode
})
}
// Initialize dark mode from localStorage
useEffect(() => {
const savedMode = localStorage.getItem('darkMode') === 'true'
setDarkMode(savedMode)
document.documentElement.classList.toggle('dark', savedMode)
}, [])
// Close mobile menu on route change
useEffect(() => {
setMobileMenuOpen(false)
}, [route])
const renderPage = (): React.ReactNode => {
switch (route) {
case '/':
return <HomePage />
case '/donate':
return <DonatePage />
case '/ai-assistance':
return <AIAssistancePortal userRole="admin" />
case '/analytics':
return <AdvancedAnalyticsDashboard />
case '/mobile-volunteer':
return <MobileVolunteerApp />
case '/staff-training':
return <StaffTrainingDashboard />
default:
return <HomePage />
}
}
return (
<AuthProvider>
<NotificationProvider>
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 dark:from-gray-900 dark:via-gray-800 dark:to-purple-900">
{/* Navigation */}
<Navigation
darkMode={darkMode}
toggleDarkMode={toggleDarkMode}
mobileMenuOpen={mobileMenuOpen}
setMobileMenuOpen={setMobileMenuOpen}
/>
{/* Main Content */}
<main>
<AnimatePresence mode="wait">
{renderPage()}
</AnimatePresence>
</main>
{/* Footer */}
<Footer />
</div>
</NotificationProvider>
</AuthProvider>
)
}
// Navigation Component
interface NavigationProps {
darkMode: boolean
toggleDarkMode: () => void
mobileMenuOpen: boolean
setMobileMenuOpen: (open: boolean) => void
}
const Navigation: React.FC<NavigationProps> = ({
darkMode,
toggleDarkMode,
mobileMenuOpen,
setMobileMenuOpen
}) => {
const navItems = [
{ label: 'Home', href: '/' },
{ label: 'Donate', href: '/donate' },
{ label: 'Volunteer', href: '/volunteer' },
{ label: 'About', href: '/about' },
{ label: 'Contact', href: '/contact' }
]
return (
<nav className="sticky top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-white/20 dark:border-gray-700/50">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex items-center">
<a href="#/" className="text-2xl font-bold text-primary-600 hover:text-primary-700 transition-colors">
Miracles in Motion
</a>
</div>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-8">
{navItems.map((item) => (
<a
key={item.href}
href={`#${item.href}`}
className="text-gray-600 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors font-medium"
>
{item.label}
</a>
))}
<button
onClick={toggleDarkMode}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle dark mode"
>
{darkMode ? (
<Sun className="w-5 h-5 text-yellow-500" />
) : (
<Moon className="w-5 h-5 text-gray-600" />
)}
</button>
</div>
{/* Mobile Menu Button */}
<div className="md:hidden flex items-center space-x-2">
<button
onClick={toggleDarkMode}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle dark mode"
>
{darkMode ? (
<Sun className="w-5 h-5 text-yellow-500" />
) : (
<Moon className="w-5 h-5 text-gray-600" />
)}
</button>
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle menu"
>
{mobileMenuOpen ? (
<X className="w-5 h-5 text-gray-600 dark:text-gray-300" />
) : (
<Menu className="w-5 h-5 text-gray-600 dark:text-gray-300" />
)}
</button>
</div>
</div>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="md:hidden py-4 border-t border-gray-200 dark:border-gray-700">
<div className="space-y-2">
{navItems.map((item) => (
<a
key={item.href}
href={`#${item.href}`}
className="block px-4 py-2 text-gray-600 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors font-medium"
>
{item.label}
</a>
))}
</div>
</div>
)}
</div>
</nav>
)
}
// Footer Component
const Footer: React.FC = () => {
return (
<footer className="bg-gray-900 text-white py-12">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Organization Info */}
<div className="col-span-1 md:col-span-2">
<h3 className="text-2xl font-bold mb-4">Miracles in Motion</h3>
<p className="text-gray-300 mb-4 max-w-md">
A 501(c)3 non-profit organization dedicated to providing essential support
to students and families in need. Every contribution makes a lasting impact.
</p>
<p className="text-sm text-gray-400">
EIN: 88-1234567 • All donations are tax-deductible
</p>
</div>
{/* Quick Links */}
<div>
<h4 className="font-semibold mb-4">Quick Links</h4>
<div className="space-y-2">
<a href="#/donate" className="block text-gray-300 hover:text-white transition-colors">
Donate Now
</a>
<a href="#/volunteer" className="block text-gray-300 hover:text-white transition-colors">
Volunteer
</a>
<a href="#/about" className="block text-gray-300 hover:text-white transition-colors">
About Us
</a>
<a href="#/impact" className="block text-gray-300 hover:text-white transition-colors">
Our Impact
</a>
</div>
</div>
{/* Contact */}
<div>
<h4 className="font-semibold mb-4">Contact</h4>
<div className="space-y-2 text-gray-300">
<p>contact@mim4u.org</p>
<p>(818) 491-6884</p>
<p>20274 Via Medici<br />Porter Ranch, CA 91326</p>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-8 pt-8 text-center text-gray-400">
<p>&copy; 2025 Miracles in Motion. All rights reserved.</p>
</div>
</div>
</footer>
)
}
import React, { useState, useEffect } from 'react'
import { AnimatePresence } from 'framer-motion'
import { Sun, Moon, Menu, X } from 'lucide-react'
// Contexts
import { AuthProvider } from './contexts/AuthContext'
import { NotificationProvider } from './contexts/NotificationContext'
// Pages
import { HomePage } from './pages/HomePage'
import { DonatePage } from './pages/DonatePage'
// Components
import AIAssistancePortal from './components/AIAssistancePortal'
import AdvancedAnalyticsDashboard from './components/AdvancedAnalyticsDashboard'
import MobileVolunteerApp from './components/MobileVolunteerApp'
import StaffTrainingDashboard from './components/StaffTrainingDashboard'
// Hooks
import { useHashRoute } from './hooks/useCommon'
// Analytics
import { analytics } from './utils/analytics'
// Main App Component
const App: React.FC = () => {
const [darkMode, setDarkMode] = useState<boolean>(false)
const [mobileMenuOpen, setMobileMenuOpen] = useState<boolean>(false)
const { route } = useHashRoute()
// Initialize analytics
useEffect(() => {
analytics.init()
}, [])
// Dark mode toggle
const toggleDarkMode = (): void => {
setDarkMode(prev => {
const newMode = !prev
document.documentElement.classList.toggle('dark', newMode)
localStorage.setItem('darkMode', newMode.toString())
return newMode
})
}
// Initialize dark mode from localStorage
useEffect(() => {
const savedMode = localStorage.getItem('darkMode') === 'true'
setDarkMode(savedMode)
document.documentElement.classList.toggle('dark', savedMode)
}, [])
// Close mobile menu on route change
useEffect(() => {
setMobileMenuOpen(false)
}, [route])
const renderPage = (): React.ReactNode => {
switch (route) {
case '/':
return <HomePage />
case '/donate':
return <DonatePage />
case '/ai-assistance':
return <AIAssistancePortal userRole="admin" />
case '/analytics':
return <AdvancedAnalyticsDashboard />
case '/mobile-volunteer':
return <MobileVolunteerApp />
case '/staff-training':
return <StaffTrainingDashboard />
default:
return <HomePage />
}
}
return (
<AuthProvider>
<NotificationProvider>
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 dark:from-gray-900 dark:via-gray-800 dark:to-purple-900">
{/* Navigation */}
<Navigation
darkMode={darkMode}
toggleDarkMode={toggleDarkMode}
mobileMenuOpen={mobileMenuOpen}
setMobileMenuOpen={setMobileMenuOpen}
/>
{/* Main Content */}
<main>
<AnimatePresence mode="wait">
{renderPage()}
</AnimatePresence>
</main>
{/* Footer */}
<Footer />
</div>
</NotificationProvider>
</AuthProvider>
)
}
// Navigation Component
interface NavigationProps {
darkMode: boolean
toggleDarkMode: () => void
mobileMenuOpen: boolean
setMobileMenuOpen: (open: boolean) => void
}
const Navigation: React.FC<NavigationProps> = ({
darkMode,
toggleDarkMode,
mobileMenuOpen,
setMobileMenuOpen
}) => {
const navItems = [
{ label: 'Home', href: '/' },
{ label: 'Donate', href: '/donate' },
{ label: 'Volunteer', href: '/volunteer' },
{ label: 'About', href: '/about' },
{ label: 'Contact', href: '/contact' }
]
return (
<nav className="sticky top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-white/20 dark:border-gray-700/50">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex items-center">
<a href="#/" className="text-2xl font-bold text-primary-600 hover:text-primary-700 transition-colors">
Miracles in Motion
</a>
</div>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-8">
{navItems.map((item) => (
<a
key={item.href}
href={`#${item.href}`}
className="text-gray-600 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors font-medium"
>
{item.label}
</a>
))}
<button
onClick={toggleDarkMode}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle dark mode"
>
{darkMode ? (
<Sun className="w-5 h-5 text-yellow-500" />
) : (
<Moon className="w-5 h-5 text-gray-600" />
)}
</button>
</div>
{/* Mobile Menu Button */}
<div className="md:hidden flex items-center space-x-2">
<button
onClick={toggleDarkMode}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle dark mode"
>
{darkMode ? (
<Sun className="w-5 h-5 text-yellow-500" />
) : (
<Moon className="w-5 h-5 text-gray-600" />
)}
</button>
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle menu"
>
{mobileMenuOpen ? (
<X className="w-5 h-5 text-gray-600 dark:text-gray-300" />
) : (
<Menu className="w-5 h-5 text-gray-600 dark:text-gray-300" />
)}
</button>
</div>
</div>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="md:hidden py-4 border-t border-gray-200 dark:border-gray-700">
<div className="space-y-2">
{navItems.map((item) => (
<a
key={item.href}
href={`#${item.href}`}
className="block px-4 py-2 text-gray-600 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors font-medium"
>
{item.label}
</a>
))}
</div>
</div>
)}
</div>
</nav>
)
}
// Footer Component
const Footer: React.FC = () => {
return (
<footer className="bg-gray-900 text-white py-12">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Organization Info */}
<div className="col-span-1 md:col-span-2">
<h3 className="text-2xl font-bold mb-4">Miracles in Motion</h3>
<p className="text-gray-300 mb-4 max-w-md">
A 501(c)3 non-profit organization dedicated to providing essential support
to students and families in need. Every contribution makes a lasting impact.
</p>
<p className="text-sm text-gray-400">
EIN: 88-1234567 • All donations are tax-deductible
</p>
</div>
{/* Quick Links */}
<div>
<h4 className="font-semibold mb-4">Quick Links</h4>
<div className="space-y-2">
<a href="#/donate" className="block text-gray-300 hover:text-white transition-colors">
Donate Now
</a>
<a href="#/volunteer" className="block text-gray-300 hover:text-white transition-colors">
Volunteer
</a>
<a href="#/about" className="block text-gray-300 hover:text-white transition-colors">
About Us
</a>
<a href="#/impact" className="block text-gray-300 hover:text-white transition-colors">
Our Impact
</a>
</div>
</div>
{/* Contact */}
<div>
<h4 className="font-semibold mb-4">Contact</h4>
<div className="space-y-2 text-gray-300">
<p>contact@mim4u.org</p>
<p>(818) 491-6884</p>
<p>20274 Via Medici<br />Porter Ranch, CA 91326</p>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-8 pt-8 text-center text-gray-400">
<p>&copy; 2025 Miracles in Motion. All rights reserved.</p>
</div>
</div>
</footer>
)
}
export default App
+230 -230
View File
@@ -1,231 +1,231 @@
// Simplified Browser-Compatible Processing Pipeline
import type {
StudentRequest,
MatchResult,
AIUpdate,
AIInsight
} from './types'
// Simple browser-compatible processing
export class SimpleBrowserProcessor {
private processingQueue: StudentRequest[] = []
private isProcessing = false
private subscribers: Set<(update: AIUpdate) => void> = new Set()
// Add request to processing queue
async addRequest(request: StudentRequest): Promise<void> {
this.processingQueue.push(request)
console.log(`📋 Added request ${request.id} to processing queue`)
// Start processing if not already running
if (!this.isProcessing) {
this.processQueue()
}
}
// Subscribe to processing updates
subscribe(callback: (update: AIUpdate) => void): () => void {
this.subscribers.add(callback)
return () => this.subscribers.delete(callback)
}
// Notify subscribers of updates
private notify(update: AIUpdate): void {
this.subscribers.forEach(callback => {
try {
callback(update)
} catch (error) {
console.error('Error in processing callback:', error)
}
})
}
// Process the queue
private async processQueue(): Promise<void> {
if (this.isProcessing || this.processingQueue.length === 0) {
return
}
this.isProcessing = true
console.log('🔄 Starting processing queue...')
while (this.processingQueue.length > 0) {
const request = this.processingQueue.shift()!
try {
this.notify({
type: 'request-processed',
requestId: request.id,
message: `Processing request for ${request.studentName}`,
timestamp: new Date()
})
// Mock AI processing with realistic delay
await new Promise(resolve => setTimeout(resolve, 2000))
const matches = await this.mockAIMatching(request)
this.notify({
type: 'request-processed',
requestId: request.id,
message: `Found ${matches.length} potential matches`,
timestamp: new Date()
})
} catch (error) {
console.error('Error processing request:', error)
this.notify({
type: 'alert',
requestId: request.id,
message: 'Processing failed',
timestamp: new Date()
})
}
}
this.isProcessing = false
console.log('✅ Processing queue completed')
}
// Mock AI matching for browser demo
private async mockAIMatching(request: StudentRequest): Promise<MatchResult[]> {
console.log(`🤖 AI processing: ${request.description}`)
// Simulate AI analysis
const categoryMatch = this.getCategoryMatch(request.category)
const urgencyMultiplier = this.getUrgencyMultiplier(request.urgency)
return [{
resourceId: `resource-${Date.now()}`,
resourceName: categoryMatch.name,
resourceType: this.mapCategoryToResourceType(request.category),
confidenceScore: 0.75 + (Math.random() * 0.2), // 75-95%
estimatedImpact: categoryMatch.impact * urgencyMultiplier,
logisticalComplexity: categoryMatch.complexity,
estimatedCost: categoryMatch.cost,
fulfillmentTimeline: categoryMatch.timeline,
reasoningFactors: [
'AI-powered semantic analysis',
`${request.category} category match`,
`${request.urgency} urgency level`,
'Location compatibility verified'
],
riskFactors: categoryMatch.risks
}]
}
private getCategoryMatch(category: string) {
const categoryData: Record<string, any> = {
'clothing': {
name: 'School Clothing Package',
impact: 8.5,
complexity: 2.0,
cost: 60,
timeline: '3-5 days',
risks: ['Size verification needed', 'Seasonal appropriateness']
},
'supplies': {
name: 'Complete Supply Kit',
impact: 9.0,
complexity: 1.5,
cost: 35,
timeline: '1-2 days',
risks: ['Grade-level appropriateness']
},
'food': {
name: 'Emergency Food Support',
impact: 9.5,
complexity: 3.0,
cost: 45,
timeline: '4-6 hours',
risks: ['Dietary restrictions', 'Perishable items']
},
'transportation': {
name: 'Transport Assistance',
impact: 7.5,
complexity: 4.0,
cost: 25,
timeline: '1-3 days',
risks: ['Schedule coordination', 'Safety verification']
},
'emergency': {
name: 'Emergency Response Package',
impact: 10.0,
complexity: 3.5,
cost: 100,
timeline: '2-4 hours',
risks: ['Immediate availability', 'Specialized needs']
}
}
return categoryData[category] || categoryData['supplies']
}
private getUrgencyMultiplier(urgency: string): number {
const multipliers: Record<string, number> = {
'low': 0.8,
'medium': 1.0,
'high': 1.3,
'critical': 1.6
}
return multipliers[urgency] || 1.0
}
private mapCategoryToResourceType(category: string): 'supplies' | 'clothing' | 'food' | 'transport' | 'emergency' | 'other' {
const mapping: Record<string, 'supplies' | 'clothing' | 'food' | 'transport' | 'emergency' | 'other'> = {
'school-supplies': 'supplies',
'clothing': 'clothing',
'food': 'food',
'transportation': 'transport',
'emergency': 'emergency'
}
return mapping[category] || 'other'
}
// Generate insights for dashboard
async generateInsights(requests: StudentRequest[]): Promise<AIInsight[]> {
const insights: AIInsight[] = []
// Category analysis
const categoryCount = requests.reduce((acc, req) => {
acc[req.category] = (acc[req.category] || 0) + 1
return acc
}, {} as Record<string, number>)
const topCategory = Object.entries(categoryCount)
.sort(([,a], [,b]) => b - a)[0]
if (topCategory) {
insights.push({
id: `insight-category-${Date.now()}`,
type: 'trend',
title: `High Demand: ${topCategory[0]}`,
description: `${topCategory[1]} requests for ${topCategory[0]} in recent batch. Consider stocking additional resources.`,
confidence: 0.85,
severity: topCategory[1] > 3 ? 'medium' : 'low',
timestamp: new Date()
})
}
// Urgency analysis
const criticalCount = requests.filter(r => r.urgency.toString() === 'critical').length
if (criticalCount > 0) {
insights.push({
id: `insight-urgency-${Date.now()}`,
type: 'recommendation',
title: 'Critical Requests Detected',
description: `${criticalCount} critical priority requests require immediate attention.`,
confidence: 1.0,
severity: 'high',
timestamp: new Date()
})
}
return insights
}
}
// Export singleton instance
// Simplified Browser-Compatible Processing Pipeline
import type {
StudentRequest,
MatchResult,
AIUpdate,
AIInsight
} from './types'
// Simple browser-compatible processing
export class SimpleBrowserProcessor {
private processingQueue: StudentRequest[] = []
private isProcessing = false
private subscribers: Set<(update: AIUpdate) => void> = new Set()
// Add request to processing queue
async addRequest(request: StudentRequest): Promise<void> {
this.processingQueue.push(request)
console.log(`📋 Added request ${request.id} to processing queue`)
// Start processing if not already running
if (!this.isProcessing) {
this.processQueue()
}
}
// Subscribe to processing updates
subscribe(callback: (update: AIUpdate) => void): () => void {
this.subscribers.add(callback)
return () => this.subscribers.delete(callback)
}
// Notify subscribers of updates
private notify(update: AIUpdate): void {
this.subscribers.forEach(callback => {
try {
callback(update)
} catch (error) {
console.error('Error in processing callback:', error)
}
})
}
// Process the queue
private async processQueue(): Promise<void> {
if (this.isProcessing || this.processingQueue.length === 0) {
return
}
this.isProcessing = true
console.log('🔄 Starting processing queue...')
while (this.processingQueue.length > 0) {
const request = this.processingQueue.shift()!
try {
this.notify({
type: 'request-processed',
requestId: request.id,
message: `Processing request for ${request.studentName}`,
timestamp: new Date()
})
// Mock AI processing with realistic delay
await new Promise(resolve => setTimeout(resolve, 2000))
const matches = await this.mockAIMatching(request)
this.notify({
type: 'request-processed',
requestId: request.id,
message: `Found ${matches.length} potential matches`,
timestamp: new Date()
})
} catch (error) {
console.error('Error processing request:', error)
this.notify({
type: 'alert',
requestId: request.id,
message: 'Processing failed',
timestamp: new Date()
})
}
}
this.isProcessing = false
console.log('✅ Processing queue completed')
}
// Mock AI matching for browser demo
private async mockAIMatching(request: StudentRequest): Promise<MatchResult[]> {
console.log(`🤖 AI processing: ${request.description}`)
// Simulate AI analysis
const categoryMatch = this.getCategoryMatch(request.category)
const urgencyMultiplier = this.getUrgencyMultiplier(request.urgency)
return [{
resourceId: `resource-${Date.now()}`,
resourceName: categoryMatch.name,
resourceType: this.mapCategoryToResourceType(request.category),
confidenceScore: 0.75 + (Math.random() * 0.2), // 75-95%
estimatedImpact: categoryMatch.impact * urgencyMultiplier,
logisticalComplexity: categoryMatch.complexity,
estimatedCost: categoryMatch.cost,
fulfillmentTimeline: categoryMatch.timeline,
reasoningFactors: [
'AI-powered semantic analysis',
`${request.category} category match`,
`${request.urgency} urgency level`,
'Location compatibility verified'
],
riskFactors: categoryMatch.risks
}]
}
private getCategoryMatch(category: string) {
const categoryData: Record<string, any> = {
'clothing': {
name: 'School Clothing Package',
impact: 8.5,
complexity: 2.0,
cost: 60,
timeline: '3-5 days',
risks: ['Size verification needed', 'Seasonal appropriateness']
},
'supplies': {
name: 'Complete Supply Kit',
impact: 9.0,
complexity: 1.5,
cost: 35,
timeline: '1-2 days',
risks: ['Grade-level appropriateness']
},
'food': {
name: 'Emergency Food Support',
impact: 9.5,
complexity: 3.0,
cost: 45,
timeline: '4-6 hours',
risks: ['Dietary restrictions', 'Perishable items']
},
'transportation': {
name: 'Transport Assistance',
impact: 7.5,
complexity: 4.0,
cost: 25,
timeline: '1-3 days',
risks: ['Schedule coordination', 'Safety verification']
},
'emergency': {
name: 'Emergency Response Package',
impact: 10.0,
complexity: 3.5,
cost: 100,
timeline: '2-4 hours',
risks: ['Immediate availability', 'Specialized needs']
}
}
return categoryData[category] || categoryData['supplies']
}
private getUrgencyMultiplier(urgency: string): number {
const multipliers: Record<string, number> = {
'low': 0.8,
'medium': 1.0,
'high': 1.3,
'critical': 1.6
}
return multipliers[urgency] || 1.0
}
private mapCategoryToResourceType(category: string): 'supplies' | 'clothing' | 'food' | 'transport' | 'emergency' | 'other' {
const mapping: Record<string, 'supplies' | 'clothing' | 'food' | 'transport' | 'emergency' | 'other'> = {
'school-supplies': 'supplies',
'clothing': 'clothing',
'food': 'food',
'transportation': 'transport',
'emergency': 'emergency'
}
return mapping[category] || 'other'
}
// Generate insights for dashboard
async generateInsights(requests: StudentRequest[]): Promise<AIInsight[]> {
const insights: AIInsight[] = []
// Category analysis
const categoryCount = requests.reduce((acc, req) => {
acc[req.category] = (acc[req.category] || 0) + 1
return acc
}, {} as Record<string, number>)
const topCategory = Object.entries(categoryCount)
.sort(([,a], [,b]) => b - a)[0]
if (topCategory) {
insights.push({
id: `insight-category-${Date.now()}`,
type: 'trend',
title: `High Demand: ${topCategory[0]}`,
description: `${topCategory[1]} requests for ${topCategory[0]} in recent batch. Consider stocking additional resources.`,
confidence: 0.85,
severity: topCategory[1] > 3 ? 'medium' : 'low',
timestamp: new Date()
})
}
// Urgency analysis
const criticalCount = requests.filter(r => r.urgency.toString() === 'critical').length
if (criticalCount > 0) {
insights.push({
id: `insight-urgency-${Date.now()}`,
type: 'recommendation',
title: 'Critical Requests Detected',
description: `${criticalCount} critical priority requests require immediate attention.`,
confidence: 1.0,
severity: 'high',
timestamp: new Date()
})
}
return insights
}
}
// Export singleton instance
export const browserProcessor = new SimpleBrowserProcessor()
+93 -93
View File
@@ -1,94 +1,94 @@
// AI Model Lazy Loading Implementation
import * as tf from '@tensorflow/tfjs'
import type { StudentRequest, MatchResult } from './types'
export class OptimizedStudentAssistanceAI {
private static models: Map<string, any> = new Map()
private static modelUrls: Record<string, string> = {
'text-vectorization': '/models/text-vectorizer.json',
'matching-engine': '/models/matcher.json',
'priority-classifier': '/models/priority.json'
}
// Lazy load models on demand
private static async loadModel(modelType: string) {
if (this.models.has(modelType)) {
return this.models.get(modelType)
}
try {
console.log(`🤖 Loading AI model: ${modelType}`)
const model = await tf.loadLayersModel(this.modelUrls[modelType])
this.models.set(modelType, model)
console.log(`✅ Model ${modelType} loaded successfully`)
return model
} catch (error) {
console.error(`❌ Failed to load model ${modelType}:`, error)
// Fallback to rule-based system
return null
}
}
// Preload critical models in background
static async preloadCriticalModels() {
try {
// Load text vectorization model first (most commonly used)
await this.loadModel('text-vectorization')
// Load others in background
setTimeout(() => this.loadModel('matching-engine'), 2000)
setTimeout(() => this.loadModel('priority-classifier'), 4000)
} catch (error) {
console.warn('Background model preloading failed:', error)
}
}
async processRequest(request: StudentRequest): Promise<MatchResult[]> {
// Load models as needed
const textModel = await OptimizedStudentAssistanceAI.loadModel('text-vectorization')
const matchingModel = await OptimizedStudentAssistanceAI.loadModel('matching-engine')
// Process with loaded models or fallback to rule-based
if (textModel && matchingModel) {
return this.aiBasedMatching(request, textModel, matchingModel)
} else {
return this.ruleBasedMatching(request)
}
}
private async aiBasedMatching(request: StudentRequest, textModel: any, matchingModel: any): Promise<MatchResult[]> {
// AI-powered matching logic
console.log('🤖 Using AI-powered matching', { request: request.id, textModel: !!textModel, matchingModel: !!matchingModel })
// Mock implementation for now
return [{
resourceId: 'resource-1',
resourceName: 'School Supply Kit',
resourceType: 'supplies',
confidenceScore: 0.85,
estimatedImpact: 8.5,
logisticalComplexity: 2.1,
estimatedCost: 50,
fulfillmentTimeline: '2-3 days',
reasoningFactors: ['AI-based match using TensorFlow models', 'High confidence score'],
riskFactors: ['Low risk - standard supplies']
}]
}
private async ruleBasedMatching(request: StudentRequest): Promise<MatchResult[]> {
// Fallback rule-based matching
console.log('📏 Using rule-based matching (fallback)', { request: request.id })
// Mock implementation for now
return [{
resourceId: 'resource-1',
resourceName: 'Basic Supply Kit',
resourceType: 'supplies',
confidenceScore: 0.65,
estimatedImpact: 6.5,
logisticalComplexity: 3.2,
estimatedCost: 50,
fulfillmentTimeline: '3-5 days',
reasoningFactors: ['Rule-based fallback matching', 'Basic category match'],
riskFactors: ['Medium risk - manual verification needed']
}]
}
// AI Model Lazy Loading Implementation
import * as tf from '@tensorflow/tfjs'
import type { StudentRequest, MatchResult } from './types'
export class OptimizedStudentAssistanceAI {
private static models: Map<string, any> = new Map()
private static modelUrls: Record<string, string> = {
'text-vectorization': '/models/text-vectorizer.json',
'matching-engine': '/models/matcher.json',
'priority-classifier': '/models/priority.json'
}
// Lazy load models on demand
private static async loadModel(modelType: string) {
if (this.models.has(modelType)) {
return this.models.get(modelType)
}
try {
console.log(`🤖 Loading AI model: ${modelType}`)
const model = await tf.loadLayersModel(this.modelUrls[modelType])
this.models.set(modelType, model)
console.log(`✅ Model ${modelType} loaded successfully`)
return model
} catch (error) {
console.error(`❌ Failed to load model ${modelType}:`, error)
// Fallback to rule-based system
return null
}
}
// Preload critical models in background
static async preloadCriticalModels() {
try {
// Load text vectorization model first (most commonly used)
await this.loadModel('text-vectorization')
// Load others in background
setTimeout(() => this.loadModel('matching-engine'), 2000)
setTimeout(() => this.loadModel('priority-classifier'), 4000)
} catch (error) {
console.warn('Background model preloading failed:', error)
}
}
async processRequest(request: StudentRequest): Promise<MatchResult[]> {
// Load models as needed
const textModel = await OptimizedStudentAssistanceAI.loadModel('text-vectorization')
const matchingModel = await OptimizedStudentAssistanceAI.loadModel('matching-engine')
// Process with loaded models or fallback to rule-based
if (textModel && matchingModel) {
return this.aiBasedMatching(request, textModel, matchingModel)
} else {
return this.ruleBasedMatching(request)
}
}
private async aiBasedMatching(request: StudentRequest, textModel: any, matchingModel: any): Promise<MatchResult[]> {
// AI-powered matching logic
console.log('🤖 Using AI-powered matching', { request: request.id, textModel: !!textModel, matchingModel: !!matchingModel })
// Mock implementation for now
return [{
resourceId: 'resource-1',
resourceName: 'School Supply Kit',
resourceType: 'supplies',
confidenceScore: 0.85,
estimatedImpact: 8.5,
logisticalComplexity: 2.1,
estimatedCost: 50,
fulfillmentTimeline: '2-3 days',
reasoningFactors: ['AI-based match using TensorFlow models', 'High confidence score'],
riskFactors: ['Low risk - standard supplies']
}]
}
private async ruleBasedMatching(request: StudentRequest): Promise<MatchResult[]> {
// Fallback rule-based matching
console.log('📏 Using rule-based matching (fallback)', { request: request.id })
// Mock implementation for now
return [{
resourceId: 'resource-1',
resourceName: 'Basic Supply Kit',
resourceType: 'supplies',
confidenceScore: 0.65,
estimatedImpact: 6.5,
logisticalComplexity: 3.2,
estimatedCost: 50,
fulfillmentTimeline: '3-5 days',
reasoningFactors: ['Rule-based fallback matching', 'Basic category match'],
riskFactors: ['Medium risk - manual verification needed']
}]
}
}
+447 -447
View File
@@ -1,448 +1,448 @@
// Phase 3: Browser-Compatible Processing Pipeline for AI Assistance Matching
import {
StudentRequest,
MatchResult,
AIInsight,
AIUpdate,
ProcessingPipelineConfig
} from './types'
// Re-export from the new browser processor
export { browserProcessor, SimpleBrowserProcessor } from './BrowserProcessor'
// Browser-compatible job queue
class BrowserQueue<T> {
private jobs: Array<{ id: string; data: T; priority: number; timestamp: number }> = []
private processors: Map<string, (job: { id: string; data: T }) => Promise<void>> = new Map()
async add(jobType: string, data: T, options: { priority: number; attempts?: number; backoff?: string } = { priority: 0 }): Promise<{ id: string }> {
const job = {
id: `job-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
data,
priority: options.priority,
timestamp: Date.now()
}
this.jobs.push(job)
this.jobs.sort((a, b) => b.priority - a.priority) // Higher priority first
// Process job immediately for demo
setTimeout(() => this.processNextJob(jobType), 100)
return job
}
process(jobType: string, _concurrency: number, processor: (job: { id: string; data: T }) => Promise<void>): void {
this.processors.set(jobType, processor)
}
private async processNextJob(jobType: string): Promise<void> {
const processor = this.processors.get(jobType)
if (!processor || this.jobs.length === 0) return
const job = this.jobs.shift()!
try {
await processor(job)
} catch (error) {
console.error(`Error processing job ${job.id}:`, error)
}
}
}
// Notification Service for real-time updates
class NotificationService {
private subscribers: Set<(update: AIUpdate) => void> = new Set()
subscribe(callback: (update: AIUpdate) => void): () => void {
this.subscribers.add(callback)
return () => this.subscribers.delete(callback)
}
notify(update: AIUpdate): void {
this.subscribers.forEach(callback => {
try {
callback(update)
} catch (error) {
console.error('Error in notification callback:', error)
}
})
}
async notifyStudent(studentId: string, _assignment: any): Promise<void> {
console.log(`📧 Notifying student ${studentId} about assignment`)
// In production: send email, SMS, or push notification
}
async notifyVolunteer(volunteerId: string, _assignment: any): Promise<void> {
console.log(`📧 Notifying volunteer ${volunteerId} about new assignment`)
// In production: send volunteer notification
}
async notifyCoordinators(_assignment: any): Promise<void> {
console.log(`📧 Notifying coordinators about new assignment`)
// In production: alert coordination team
}
async updateDonors(estimatedCost: number): Promise<void> {
console.log(`💰 Updating donors about $${estimatedCost} impact opportunity`)
// In production: trigger donor engagement campaign
}
async notifyReviewer(reviewer: any, _reviewTask: any, aiInsights: any): Promise<void> {
console.log(`👥 Notifying reviewer ${reviewer.id} about review task with AI confidence: ${aiInsights.aiConfidence}`)
// In production: send detailed review notification with AI recommendations
}
}
// Assignment and Review Management
class AssignmentManager {
private static assignments: Map<string, any> = new Map()
static async createAssignment(assignmentData: any): Promise<any> {
const assignment = {
id: `assign-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
...assignmentData,
createdAt: new Date(),
status: 'pending'
}
this.assignments.set(assignment.id, assignment)
console.log(`✅ Created assignment ${assignment.id}`)
return assignment
}
static async getById(id: string): Promise<any | null> {
return this.assignments.get(id) || null
}
}
class ReviewManager {
private static reviewTasks: Map<string, any> = new Map()
static async createReviewTask(taskData: any): Promise<any> {
const task = {
id: `review-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
...taskData,
createdAt: new Date(),
status: 'pending'
}
this.reviewTasks.set(task.id, task)
console.log(`📋 Created review task ${task.id}`)
return task
}
}
// Main Processing Pipeline - Simplified browser-compatible version
export class RealTimeProcessingPipeline {
private queue: BrowserQueue<StudentRequest>
private notificationService: NotificationService
private config: ProcessingPipelineConfig
constructor(config: Partial<ProcessingPipelineConfig> = {}) {
this.queue = new BrowserQueue<StudentRequest>()
this.notificationService = new NotificationService()
this.config = {
autoApprovalThreshold: 0.85,
urgencyWeights: {
'emergency': 1.0,
'high': 0.8,
'medium': 0.5,
'low': 0.2
},
categoryWeights: {
'emergency-housing': 1.0,
'food-assistance': 0.9,
'medical-needs': 0.85,
'clothing': 0.7,
'school-supplies': 0.6,
'transportation': 0.5,
'technology': 0.4,
'extracurricular': 0.3,
'other': 0.4
},
maxProcessingTime: 5000, // 5 seconds
retryAttempts: 3,
notificationEnabled: true,
...config
}
this.setupQueueProcessors()
}
private setupQueueProcessors(): void {
this.queue.process('analyze-request', 5, async (job: { id: string; data: StudentRequest }) => {
const request = job.data
try {
console.log(`🔄 Processing request ${request.id} for ${request.studentName}`)
// Mock AI processing for browser demo
const matches = await this.mockProcessRequest(request)
// Auto-approval for high-confidence matches
if (matches.length > 0 && matches[0].confidenceScore >= this.config.autoApprovalThreshold) {
await this.autoApproveRequest(request, matches[0])
} else {
await this.routeForHumanReview(request, matches)
}
// Update real-time dashboard
await this.updateDashboard(request.id, matches)
// Notify subscribers of processing completion
this.notificationService.notify({
type: 'request-processed',
requestId: request.id,
studentName: request.studentName,
status: matches.length > 0 && matches[0].confidenceScore >= this.config.autoApprovalThreshold ? 'auto-approved' : 'under-review',
recommendations: matches,
timestamp: new Date()
})
} catch (error) {
await this.handleProcessingError(request, error as Error)
}
})
}
async submitRequest(request: StudentRequest): Promise<string> {
console.log(`📥 Submitting request from ${request.studentName}: ${request.category}`)
// Add to processing queue with priority based on urgency
const priority = this.calculatePriority(request.urgency)
const job = await this.queue.add('analyze-request', request, {
priority,
attempts: this.config.retryAttempts,
backoff: 'exponential'
})
// Immediate acknowledgment
await this.sendAcknowledgment(request)
return job.id
}
private calculatePriority(urgency: string): number {
return this.config.urgencyWeights[urgency as keyof typeof this.config.urgencyWeights] || 0.5
}
private async sendAcknowledgment(request: StudentRequest): Promise<void> {
console.log(`✉️ Sending acknowledgment to ${request.studentName}`)
// In production: send immediate confirmation email/SMS
}
private async autoApproveRequest(request: StudentRequest, match: MatchResult): Promise<void> {
console.log(`🤖 Auto-approving request ${request.id} with ${(match.confidenceScore * 100).toFixed(1)}% confidence`)
// Create assistance assignment
const assignment = await AssignmentManager.createAssignment({
requestId: request.id,
studentId: request.studentId,
studentName: request.studentName,
resourceId: match.resourceId,
resourceName: match.resourceName,
volunteerId: match.volunteerMatch?.id,
volunteerName: match.volunteerMatch?.volunteerName,
scheduledDate: new Date(Date.now() + 24 * 60 * 60 * 1000), // Tomorrow
estimatedCost: match.estimatedCost,
approvalStatus: 'auto-approved',
confidence: match.confidenceScore,
aiRecommendation: true,
urgency: request.urgency,
category: request.category
})
// Notify all stakeholders
if (this.config.notificationEnabled) {
await Promise.all([
this.notificationService.notifyStudent(request.studentId, assignment),
match.volunteerMatch ? this.notificationService.notifyVolunteer(assignment.volunteerId, assignment) : Promise.resolve(),
this.notificationService.notifyCoordinators(assignment),
this.notificationService.updateDonors(assignment.estimatedCost)
])
}
// Notify real-time subscribers
this.notificationService.notify({
type: 'auto-approval',
requestId: request.id,
studentName: request.studentName,
message: `Request automatically approved with ${(match.confidenceScore * 100).toFixed(1)}% confidence`,
timestamp: new Date()
})
// Track decision for learning
await this.trackDecision(request, match, 'auto-approved')
}
private async routeForHumanReview(request: StudentRequest, matches: MatchResult[]): Promise<void> {
console.log(`👤 Routing request ${request.id} for human review`)
// Determine best reviewer based on request type and matches
const reviewer = await this.selectOptimalReviewer(request, matches)
// Create review assignment
const reviewTask = await ReviewManager.createReviewTask({
requestId: request.id,
assignedTo: reviewer.id,
assignedToName: reviewer.name,
aiRecommendations: matches,
priority: this.calculateReviewPriority(request, matches),
deadline: this.calculateReviewDeadline(request.urgency),
studentName: request.studentName,
category: request.category,
urgency: request.urgency
})
// Notify reviewer with AI insights
if (this.config.notificationEnabled) {
await this.notificationService.notifyReviewer(reviewer, reviewTask, {
aiConfidence: matches[0]?.confidenceScore || 0,
recommendedAction: this.generateRecommendation(matches),
riskFactors: matches[0]?.riskFactors || []
})
}
}
private async selectOptimalReviewer(request: StudentRequest, _matches: MatchResult[]) {
// Mock reviewer selection - in production, this would use actual staff data
const reviewers = [
{ id: 'rev1', name: 'Sarah Martinez', specialties: ['clothing', 'emergency-housing'], workload: 5 },
{ id: 'rev2', name: 'John Davis', specialties: ['food-assistance', 'transportation'], workload: 3 },
{ id: 'rev3', name: 'Lisa Chen', specialties: ['school-supplies', 'technology'], workload: 7 },
{ id: 'rev4', name: 'Mike Johnson', specialties: ['medical-needs', 'other'], workload: 4 }
]
// Select reviewer based on specialty and workload
const categoryReviewers = reviewers.filter(r =>
r.specialties.includes(request.category) || r.specialties.includes('other')
)
// Return reviewer with lowest workload
return categoryReviewers.sort((a, b) => a.workload - b.workload)[0] || reviewers[0]
}
private calculateReviewPriority(request: StudentRequest, matches: MatchResult[]): number {
let priority = this.config.urgencyWeights[request.urgency as keyof typeof this.config.urgencyWeights] || 0.5
// Boost priority for high AI confidence but below threshold
if (matches.length > 0) {
const topMatch = matches[0]
if (topMatch.confidenceScore > 0.7 && topMatch.confidenceScore < this.config.autoApprovalThreshold) {
priority += 0.2
}
}
// Boost priority for critical categories
priority += this.config.categoryWeights[request.category as keyof typeof this.config.categoryWeights] || 0
return Math.min(priority, 1.0)
}
private calculateReviewDeadline(urgency: string): Date {
const now = new Date()
switch (urgency) {
case 'emergency':
return new Date(now.getTime() + 30 * 60 * 1000) // 30 minutes
case 'high':
return new Date(now.getTime() + 2 * 60 * 60 * 1000) // 2 hours
case 'medium':
return new Date(now.getTime() + 8 * 60 * 60 * 1000) // 8 hours
case 'low':
default:
return new Date(now.getTime() + 24 * 60 * 60 * 1000) // 24 hours
}
}
private generateRecommendation(matches: MatchResult[]): string {
if (matches.length === 0) return 'No suitable matches found - manual resource allocation needed'
const topMatch = matches[0]
if (topMatch.confidenceScore > 0.8) {
return `Strong AI recommendation: ${topMatch.resourceName} (${(topMatch.confidenceScore * 100).toFixed(1)}% confidence)`
} else if (topMatch.confidenceScore > 0.6) {
return `Moderate AI recommendation: ${topMatch.resourceName} - review for accuracy`
} else {
return `Low confidence match: manual evaluation recommended`
}
}
private async updateDashboard(requestId: string, _matches: MatchResult[]): Promise<void> {
console.log(`📊 Updating dashboard for request ${requestId}`)
// In production: update real-time analytics dashboard
}
private async handleProcessingError(request: StudentRequest, error: Error): Promise<void> {
console.error(`❌ Error processing request ${request.id}:`, error.message)
// Notify administrators of processing error
this.notificationService.notify({
type: 'alert',
requestId: request.id,
studentName: request.studentName,
message: `Processing error: ${error.message}`,
timestamp: new Date()
})
// Route to manual processing
await this.routeForHumanReview(request, [])
}
private async trackDecision(request: StudentRequest, _match: MatchResult, decision: string): Promise<void> {
console.log(`📈 Tracking decision: ${decision} for request ${request.id}`)
// In production: log decision for ML model training
}
// Public methods for integration
subscribe(callback: (update: AIUpdate) => void): () => void {
return this.notificationService.subscribe(callback)
}
private async mockProcessRequest(request: StudentRequest): Promise<MatchResult[]> {
// Mock implementation for browser demo
console.log('Mock processing request:', request.id)
await new Promise(resolve => setTimeout(resolve, 1000))
return [{
resourceId: 'mock-resource-1',
resourceName: 'Mock School Supplies',
resourceType: 'supplies',
confidenceScore: 0.8,
estimatedImpact: 8.0,
logisticalComplexity: 2.5,
estimatedCost: 50,
fulfillmentTimeline: '2-3 days',
reasoningFactors: ['Mock processing', 'Demo mode'],
riskFactors: ['Demo data only']
}]
}
async generateInsights(requests: StudentRequest[]): Promise<AIInsight[]> {
// Mock insights for browser demo
return requests.map((req, index) => ({
id: `insight-${index}`,
type: 'recommendation' as const,
title: `Insight for ${req.studentName}`,
description: `Mock insight for request ${req.id}`,
confidence: 0.75,
timestamp: new Date()
}))
}
getConfig(): ProcessingPipelineConfig {
return { ...this.config }
}
updateConfig(newConfig: Partial<ProcessingPipelineConfig>): void {
this.config = { ...this.config, ...newConfig }
console.log('🔧 Pipeline configuration updated')
}
}
// Export singleton instance for backward compatibility
export const pipeline = new RealTimeProcessingPipeline()
// Export classes for testing and advanced usage
// Phase 3: Browser-Compatible Processing Pipeline for AI Assistance Matching
import {
StudentRequest,
MatchResult,
AIInsight,
AIUpdate,
ProcessingPipelineConfig
} from './types'
// Re-export from the new browser processor
export { browserProcessor, SimpleBrowserProcessor } from './BrowserProcessor'
// Browser-compatible job queue
class BrowserQueue<T> {
private jobs: Array<{ id: string; data: T; priority: number; timestamp: number }> = []
private processors: Map<string, (job: { id: string; data: T }) => Promise<void>> = new Map()
async add(jobType: string, data: T, options: { priority: number; attempts?: number; backoff?: string } = { priority: 0 }): Promise<{ id: string }> {
const job = {
id: `job-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
data,
priority: options.priority,
timestamp: Date.now()
}
this.jobs.push(job)
this.jobs.sort((a, b) => b.priority - a.priority) // Higher priority first
// Process job immediately for demo
setTimeout(() => this.processNextJob(jobType), 100)
return job
}
process(jobType: string, _concurrency: number, processor: (job: { id: string; data: T }) => Promise<void>): void {
this.processors.set(jobType, processor)
}
private async processNextJob(jobType: string): Promise<void> {
const processor = this.processors.get(jobType)
if (!processor || this.jobs.length === 0) return
const job = this.jobs.shift()!
try {
await processor(job)
} catch (error) {
console.error(`Error processing job ${job.id}:`, error)
}
}
}
// Notification Service for real-time updates
class NotificationService {
private subscribers: Set<(update: AIUpdate) => void> = new Set()
subscribe(callback: (update: AIUpdate) => void): () => void {
this.subscribers.add(callback)
return () => this.subscribers.delete(callback)
}
notify(update: AIUpdate): void {
this.subscribers.forEach(callback => {
try {
callback(update)
} catch (error) {
console.error('Error in notification callback:', error)
}
})
}
async notifyStudent(studentId: string, _assignment: any): Promise<void> {
console.log(`📧 Notifying student ${studentId} about assignment`)
// In production: send email, SMS, or push notification
}
async notifyVolunteer(volunteerId: string, _assignment: any): Promise<void> {
console.log(`📧 Notifying volunteer ${volunteerId} about new assignment`)
// In production: send volunteer notification
}
async notifyCoordinators(_assignment: any): Promise<void> {
console.log(`📧 Notifying coordinators about new assignment`)
// In production: alert coordination team
}
async updateDonors(estimatedCost: number): Promise<void> {
console.log(`💰 Updating donors about $${estimatedCost} impact opportunity`)
// In production: trigger donor engagement campaign
}
async notifyReviewer(reviewer: any, _reviewTask: any, aiInsights: any): Promise<void> {
console.log(`👥 Notifying reviewer ${reviewer.id} about review task with AI confidence: ${aiInsights.aiConfidence}`)
// In production: send detailed review notification with AI recommendations
}
}
// Assignment and Review Management
class AssignmentManager {
private static assignments: Map<string, any> = new Map()
static async createAssignment(assignmentData: any): Promise<any> {
const assignment = {
id: `assign-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
...assignmentData,
createdAt: new Date(),
status: 'pending'
}
this.assignments.set(assignment.id, assignment)
console.log(`✅ Created assignment ${assignment.id}`)
return assignment
}
static async getById(id: string): Promise<any | null> {
return this.assignments.get(id) || null
}
}
class ReviewManager {
private static reviewTasks: Map<string, any> = new Map()
static async createReviewTask(taskData: any): Promise<any> {
const task = {
id: `review-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
...taskData,
createdAt: new Date(),
status: 'pending'
}
this.reviewTasks.set(task.id, task)
console.log(`📋 Created review task ${task.id}`)
return task
}
}
// Main Processing Pipeline - Simplified browser-compatible version
export class RealTimeProcessingPipeline {
private queue: BrowserQueue<StudentRequest>
private notificationService: NotificationService
private config: ProcessingPipelineConfig
constructor(config: Partial<ProcessingPipelineConfig> = {}) {
this.queue = new BrowserQueue<StudentRequest>()
this.notificationService = new NotificationService()
this.config = {
autoApprovalThreshold: 0.85,
urgencyWeights: {
'emergency': 1.0,
'high': 0.8,
'medium': 0.5,
'low': 0.2
},
categoryWeights: {
'emergency-housing': 1.0,
'food-assistance': 0.9,
'medical-needs': 0.85,
'clothing': 0.7,
'school-supplies': 0.6,
'transportation': 0.5,
'technology': 0.4,
'extracurricular': 0.3,
'other': 0.4
},
maxProcessingTime: 5000, // 5 seconds
retryAttempts: 3,
notificationEnabled: true,
...config
}
this.setupQueueProcessors()
}
private setupQueueProcessors(): void {
this.queue.process('analyze-request', 5, async (job: { id: string; data: StudentRequest }) => {
const request = job.data
try {
console.log(`🔄 Processing request ${request.id} for ${request.studentName}`)
// Mock AI processing for browser demo
const matches = await this.mockProcessRequest(request)
// Auto-approval for high-confidence matches
if (matches.length > 0 && matches[0].confidenceScore >= this.config.autoApprovalThreshold) {
await this.autoApproveRequest(request, matches[0])
} else {
await this.routeForHumanReview(request, matches)
}
// Update real-time dashboard
await this.updateDashboard(request.id, matches)
// Notify subscribers of processing completion
this.notificationService.notify({
type: 'request-processed',
requestId: request.id,
studentName: request.studentName,
status: matches.length > 0 && matches[0].confidenceScore >= this.config.autoApprovalThreshold ? 'auto-approved' : 'under-review',
recommendations: matches,
timestamp: new Date()
})
} catch (error) {
await this.handleProcessingError(request, error as Error)
}
})
}
async submitRequest(request: StudentRequest): Promise<string> {
console.log(`📥 Submitting request from ${request.studentName}: ${request.category}`)
// Add to processing queue with priority based on urgency
const priority = this.calculatePriority(request.urgency)
const job = await this.queue.add('analyze-request', request, {
priority,
attempts: this.config.retryAttempts,
backoff: 'exponential'
})
// Immediate acknowledgment
await this.sendAcknowledgment(request)
return job.id
}
private calculatePriority(urgency: string): number {
return this.config.urgencyWeights[urgency as keyof typeof this.config.urgencyWeights] || 0.5
}
private async sendAcknowledgment(request: StudentRequest): Promise<void> {
console.log(`✉️ Sending acknowledgment to ${request.studentName}`)
// In production: send immediate confirmation email/SMS
}
private async autoApproveRequest(request: StudentRequest, match: MatchResult): Promise<void> {
console.log(`🤖 Auto-approving request ${request.id} with ${(match.confidenceScore * 100).toFixed(1)}% confidence`)
// Create assistance assignment
const assignment = await AssignmentManager.createAssignment({
requestId: request.id,
studentId: request.studentId,
studentName: request.studentName,
resourceId: match.resourceId,
resourceName: match.resourceName,
volunteerId: match.volunteerMatch?.id,
volunteerName: match.volunteerMatch?.volunteerName,
scheduledDate: new Date(Date.now() + 24 * 60 * 60 * 1000), // Tomorrow
estimatedCost: match.estimatedCost,
approvalStatus: 'auto-approved',
confidence: match.confidenceScore,
aiRecommendation: true,
urgency: request.urgency,
category: request.category
})
// Notify all stakeholders
if (this.config.notificationEnabled) {
await Promise.all([
this.notificationService.notifyStudent(request.studentId, assignment),
match.volunteerMatch ? this.notificationService.notifyVolunteer(assignment.volunteerId, assignment) : Promise.resolve(),
this.notificationService.notifyCoordinators(assignment),
this.notificationService.updateDonors(assignment.estimatedCost)
])
}
// Notify real-time subscribers
this.notificationService.notify({
type: 'auto-approval',
requestId: request.id,
studentName: request.studentName,
message: `Request automatically approved with ${(match.confidenceScore * 100).toFixed(1)}% confidence`,
timestamp: new Date()
})
// Track decision for learning
await this.trackDecision(request, match, 'auto-approved')
}
private async routeForHumanReview(request: StudentRequest, matches: MatchResult[]): Promise<void> {
console.log(`👤 Routing request ${request.id} for human review`)
// Determine best reviewer based on request type and matches
const reviewer = await this.selectOptimalReviewer(request, matches)
// Create review assignment
const reviewTask = await ReviewManager.createReviewTask({
requestId: request.id,
assignedTo: reviewer.id,
assignedToName: reviewer.name,
aiRecommendations: matches,
priority: this.calculateReviewPriority(request, matches),
deadline: this.calculateReviewDeadline(request.urgency),
studentName: request.studentName,
category: request.category,
urgency: request.urgency
})
// Notify reviewer with AI insights
if (this.config.notificationEnabled) {
await this.notificationService.notifyReviewer(reviewer, reviewTask, {
aiConfidence: matches[0]?.confidenceScore || 0,
recommendedAction: this.generateRecommendation(matches),
riskFactors: matches[0]?.riskFactors || []
})
}
}
private async selectOptimalReviewer(request: StudentRequest, _matches: MatchResult[]) {
// Mock reviewer selection - in production, this would use actual staff data
const reviewers = [
{ id: 'rev1', name: 'Sarah Martinez', specialties: ['clothing', 'emergency-housing'], workload: 5 },
{ id: 'rev2', name: 'John Davis', specialties: ['food-assistance', 'transportation'], workload: 3 },
{ id: 'rev3', name: 'Lisa Chen', specialties: ['school-supplies', 'technology'], workload: 7 },
{ id: 'rev4', name: 'Mike Johnson', specialties: ['medical-needs', 'other'], workload: 4 }
]
// Select reviewer based on specialty and workload
const categoryReviewers = reviewers.filter(r =>
r.specialties.includes(request.category) || r.specialties.includes('other')
)
// Return reviewer with lowest workload
return categoryReviewers.sort((a, b) => a.workload - b.workload)[0] || reviewers[0]
}
private calculateReviewPriority(request: StudentRequest, matches: MatchResult[]): number {
let priority = this.config.urgencyWeights[request.urgency as keyof typeof this.config.urgencyWeights] || 0.5
// Boost priority for high AI confidence but below threshold
if (matches.length > 0) {
const topMatch = matches[0]
if (topMatch.confidenceScore > 0.7 && topMatch.confidenceScore < this.config.autoApprovalThreshold) {
priority += 0.2
}
}
// Boost priority for critical categories
priority += this.config.categoryWeights[request.category as keyof typeof this.config.categoryWeights] || 0
return Math.min(priority, 1.0)
}
private calculateReviewDeadline(urgency: string): Date {
const now = new Date()
switch (urgency) {
case 'emergency':
return new Date(now.getTime() + 30 * 60 * 1000) // 30 minutes
case 'high':
return new Date(now.getTime() + 2 * 60 * 60 * 1000) // 2 hours
case 'medium':
return new Date(now.getTime() + 8 * 60 * 60 * 1000) // 8 hours
case 'low':
default:
return new Date(now.getTime() + 24 * 60 * 60 * 1000) // 24 hours
}
}
private generateRecommendation(matches: MatchResult[]): string {
if (matches.length === 0) return 'No suitable matches found - manual resource allocation needed'
const topMatch = matches[0]
if (topMatch.confidenceScore > 0.8) {
return `Strong AI recommendation: ${topMatch.resourceName} (${(topMatch.confidenceScore * 100).toFixed(1)}% confidence)`
} else if (topMatch.confidenceScore > 0.6) {
return `Moderate AI recommendation: ${topMatch.resourceName} - review for accuracy`
} else {
return `Low confidence match: manual evaluation recommended`
}
}
private async updateDashboard(requestId: string, _matches: MatchResult[]): Promise<void> {
console.log(`📊 Updating dashboard for request ${requestId}`)
// In production: update real-time analytics dashboard
}
private async handleProcessingError(request: StudentRequest, error: Error): Promise<void> {
console.error(`❌ Error processing request ${request.id}:`, error.message)
// Notify administrators of processing error
this.notificationService.notify({
type: 'alert',
requestId: request.id,
studentName: request.studentName,
message: `Processing error: ${error.message}`,
timestamp: new Date()
})
// Route to manual processing
await this.routeForHumanReview(request, [])
}
private async trackDecision(request: StudentRequest, _match: MatchResult, decision: string): Promise<void> {
console.log(`📈 Tracking decision: ${decision} for request ${request.id}`)
// In production: log decision for ML model training
}
// Public methods for integration
subscribe(callback: (update: AIUpdate) => void): () => void {
return this.notificationService.subscribe(callback)
}
private async mockProcessRequest(request: StudentRequest): Promise<MatchResult[]> {
// Mock implementation for browser demo
console.log('Mock processing request:', request.id)
await new Promise(resolve => setTimeout(resolve, 1000))
return [{
resourceId: 'mock-resource-1',
resourceName: 'Mock School Supplies',
resourceType: 'supplies',
confidenceScore: 0.8,
estimatedImpact: 8.0,
logisticalComplexity: 2.5,
estimatedCost: 50,
fulfillmentTimeline: '2-3 days',
reasoningFactors: ['Mock processing', 'Demo mode'],
riskFactors: ['Demo data only']
}]
}
async generateInsights(requests: StudentRequest[]): Promise<AIInsight[]> {
// Mock insights for browser demo
return requests.map((req, index) => ({
id: `insight-${index}`,
type: 'recommendation' as const,
title: `Insight for ${req.studentName}`,
description: `Mock insight for request ${req.id}`,
confidence: 0.75,
timestamp: new Date()
}))
}
getConfig(): ProcessingPipelineConfig {
return { ...this.config }
}
updateConfig(newConfig: Partial<ProcessingPipelineConfig>): void {
this.config = { ...this.config, ...newConfig }
console.log('🔧 Pipeline configuration updated')
}
}
// Export singleton instance for backward compatibility
export const pipeline = new RealTimeProcessingPipeline()
// Export classes for testing and advanced usage
export { NotificationService, AssignmentManager, ReviewManager }
File diff suppressed because it is too large Load Diff
+190 -190
View File
@@ -1,191 +1,191 @@
// Phase 3: AI Types and Interfaces
export interface StudentRequest {
id: string
studentId: string
studentName: string
description: string
category: AssistanceCategory
urgency: UrgencyLevel
location: GeographicLocation
constraints: RequestConstraints
deadline?: Date
submittedAt: Date
estimatedCost?: number
requiredSkills?: string[]
}
export interface MatchResult {
resourceId: string
resourceName: string
resourceType: 'clothing' | 'supplies' | 'food' | 'transport' | 'emergency' | 'other'
confidenceScore: number
estimatedImpact: number
logisticalComplexity: number
volunteerMatch?: VolunteerAssignment
estimatedCost: number
fulfillmentTimeline: string
reasoningFactors: string[]
riskFactors: string[]
}
export interface RequestAnalysis {
primaryNeeds: NeedCategory[]
urgencyScore: number
complexityEstimate: number
resourceRequirements: ResourceRequirement[]
locationConstraints?: GeographicConstraint[]
timeConstraints?: TemporalConstraint[]
requiredSkills?: string[]
estimatedBudget: number
}
export interface AIInsight {
id: string
type: 'anomaly' | 'optimization' | 'trend' | 'prediction' | 'recommendation'
title: string
description: string
confidence: number
severity?: 'low' | 'medium' | 'high' | 'critical'
timestamp: Date
actionItems?: string[]
estimatedImpact?: string
data?: Record<string, any>
}
export interface AIMetrics {
accuracyRate: number
accuracyTrend: number
avgProcessingTime: number
speedTrend: number
autoApprovalRate: number
automationTrend: number
impactPredictionAccuracy: number
impactTrend: number
totalRequestsProcessed: number
successfulMatches: number
}
export interface AIUpdate {
type: 'request-processed' | 'new-insight' | 'auto-approval' | 'model-updated' | 'alert'
requestId?: string
studentName?: string
status?: string
recommendations?: MatchResult[]
insight?: AIInsight
message?: string
timestamp: Date
}
export type AssistanceCategory =
| 'clothing'
| 'school-supplies'
| 'food-assistance'
| 'transportation'
| 'emergency-housing'
| 'medical-needs'
| 'technology'
| 'extracurricular'
| 'other'
export type UrgencyLevel = 'low' | 'medium' | 'high' | 'emergency'
export interface GeographicLocation {
latitude?: number
longitude?: number
address?: string
city: string
state: string
zipCode: string
schoolDistrict?: string
}
export interface RequestConstraints {
maxBudget?: number
timeframe: 'immediate' | 'within-week' | 'within-month' | 'flexible'
deliveryMethod: 'pickup' | 'delivery' | 'mail' | 'school-delivery' | 'any'
privacyLevel: 'anonymous' | 'semi-anonymous' | 'open'
specialRequirements?: string[]
}
export interface VolunteerAssignment {
id: string
volunteerId: string
volunteerName: string
skills: string[]
availability: Date[]
location: GeographicLocation
rating: number
completedAssignments: number
}
export interface NeedCategory {
category: AssistanceCategory
subcategory?: string
priority: number
quantity?: number
specifications?: Record<string, any>
}
export interface ResourceRequirement {
type: string
quantity: number
specifications: Record<string, any>
alternatives?: string[]
estimatedCost: number
}
export interface GeographicConstraint {
maxDistance: number
preferredAreas?: string[]
excludedAreas?: string[]
}
export interface TemporalConstraint {
earliestStart: Date
latestCompletion: Date
preferredTimes?: string[]
blackoutPeriods?: DateRange[]
}
export interface DateRange {
start: Date
end: Date
}
export interface ImpactPrediction {
estimatedBeneficiaries: number
successProbability: number
timeToImpact: number
sustainabilityScore: number
rippleEffects: RippleEffect[]
measurableOutcomes: string[]
}
export interface RippleEffect {
type: 'family' | 'community' | 'academic' | 'social' | 'economic'
description: string
estimatedBeneficiaries: number
confidenceLevel: number
}
export interface LearningFeedback {
requestId: string
matchId: string
outcome: 'successful' | 'partial' | 'failed'
actualCost: number
actualTimeToComplete: number
satisfactionScore: number
issues?: string[]
improvements?: string[]
measuredImpact?: Record<string, number>
}
export interface ProcessingPipelineConfig {
autoApprovalThreshold: number
urgencyWeights: Record<UrgencyLevel, number>
categoryWeights: Record<AssistanceCategory, number>
maxProcessingTime: number
retryAttempts: number
notificationEnabled: boolean
// Phase 3: AI Types and Interfaces
export interface StudentRequest {
id: string
studentId: string
studentName: string
description: string
category: AssistanceCategory
urgency: UrgencyLevel
location: GeographicLocation
constraints: RequestConstraints
deadline?: Date
submittedAt: Date
estimatedCost?: number
requiredSkills?: string[]
}
export interface MatchResult {
resourceId: string
resourceName: string
resourceType: 'clothing' | 'supplies' | 'food' | 'transport' | 'emergency' | 'other'
confidenceScore: number
estimatedImpact: number
logisticalComplexity: number
volunteerMatch?: VolunteerAssignment
estimatedCost: number
fulfillmentTimeline: string
reasoningFactors: string[]
riskFactors: string[]
}
export interface RequestAnalysis {
primaryNeeds: NeedCategory[]
urgencyScore: number
complexityEstimate: number
resourceRequirements: ResourceRequirement[]
locationConstraints?: GeographicConstraint[]
timeConstraints?: TemporalConstraint[]
requiredSkills?: string[]
estimatedBudget: number
}
export interface AIInsight {
id: string
type: 'anomaly' | 'optimization' | 'trend' | 'prediction' | 'recommendation'
title: string
description: string
confidence: number
severity?: 'low' | 'medium' | 'high' | 'critical'
timestamp: Date
actionItems?: string[]
estimatedImpact?: string
data?: Record<string, any>
}
export interface AIMetrics {
accuracyRate: number
accuracyTrend: number
avgProcessingTime: number
speedTrend: number
autoApprovalRate: number
automationTrend: number
impactPredictionAccuracy: number
impactTrend: number
totalRequestsProcessed: number
successfulMatches: number
}
export interface AIUpdate {
type: 'request-processed' | 'new-insight' | 'auto-approval' | 'model-updated' | 'alert'
requestId?: string
studentName?: string
status?: string
recommendations?: MatchResult[]
insight?: AIInsight
message?: string
timestamp: Date
}
export type AssistanceCategory =
| 'clothing'
| 'school-supplies'
| 'food-assistance'
| 'transportation'
| 'emergency-housing'
| 'medical-needs'
| 'technology'
| 'extracurricular'
| 'other'
export type UrgencyLevel = 'low' | 'medium' | 'high' | 'emergency'
export interface GeographicLocation {
latitude?: number
longitude?: number
address?: string
city: string
state: string
zipCode: string
schoolDistrict?: string
}
export interface RequestConstraints {
maxBudget?: number
timeframe: 'immediate' | 'within-week' | 'within-month' | 'flexible'
deliveryMethod: 'pickup' | 'delivery' | 'mail' | 'school-delivery' | 'any'
privacyLevel: 'anonymous' | 'semi-anonymous' | 'open'
specialRequirements?: string[]
}
export interface VolunteerAssignment {
id: string
volunteerId: string
volunteerName: string
skills: string[]
availability: Date[]
location: GeographicLocation
rating: number
completedAssignments: number
}
export interface NeedCategory {
category: AssistanceCategory
subcategory?: string
priority: number
quantity?: number
specifications?: Record<string, any>
}
export interface ResourceRequirement {
type: string
quantity: number
specifications: Record<string, any>
alternatives?: string[]
estimatedCost: number
}
export interface GeographicConstraint {
maxDistance: number
preferredAreas?: string[]
excludedAreas?: string[]
}
export interface TemporalConstraint {
earliestStart: Date
latestCompletion: Date
preferredTimes?: string[]
blackoutPeriods?: DateRange[]
}
export interface DateRange {
start: Date
end: Date
}
export interface ImpactPrediction {
estimatedBeneficiaries: number
successProbability: number
timeToImpact: number
sustainabilityScore: number
rippleEffects: RippleEffect[]
measurableOutcomes: string[]
}
export interface RippleEffect {
type: 'family' | 'community' | 'academic' | 'social' | 'economic'
description: string
estimatedBeneficiaries: number
confidenceLevel: number
}
export interface LearningFeedback {
requestId: string
matchId: string
outcome: 'successful' | 'partial' | 'failed'
actualCost: number
actualTimeToComplete: number
satisfactionScore: number
issues?: string[]
improvements?: string[]
measuredImpact?: Record<string, number>
}
export interface ProcessingPipelineConfig {
autoApprovalThreshold: number
urgencyWeights: Record<UrgencyLevel, number>
categoryWeights: Record<AssistanceCategory, number>
maxProcessingTime: number
retryAttempts: number
notificationEnabled: boolean
}
File diff suppressed because it is too large Load Diff
+369 -369
View File
@@ -1,370 +1,370 @@
// Phase 3B: Advanced Analytics Dashboard for Nonprofit Impact Tracking
import React, { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
interface ImpactMetrics {
totalStudentsServed: number
totalResourcesAllocated: number
totalDonationsProcessed: number
averageResponseTime: number
costEfficiencyRatio: number
volunteerEngagement: number
schoolPartnershipGrowth: number
monthlyTrends: MonthlyTrend[]
}
interface MonthlyTrend {
month: string
studentsServed: number
resourcesAllocated: number
donations: number
efficiency: number
}
interface PredictiveAnalysis {
nextMonthDemand: number
resourceNeeds: ResourceForecast[]
budgetProjection: number
volunteerRequirement: number
riskFactors: string[]
opportunities: string[]
}
interface ResourceForecast {
category: string
predictedDemand: number
currentInventory: number
recommendedPurchase: number
urgencyLevel: 'low' | 'medium' | 'high' | 'critical'
}
interface GeographicData {
region: string
studentsServed: number
averageNeed: number
responseTime: number
efficiency: number
coordinates: [number, number]
}
const AdvancedAnalyticsDashboard: React.FC = () => {
const [metrics, setMetrics] = useState<ImpactMetrics | null>(null)
const [predictions, setPredictions] = useState<PredictiveAnalysis | null>(null)
const [geoData, setGeoData] = useState<GeographicData[]>([])
const [selectedTimeframe, setSelectedTimeframe] = useState<'week' | 'month' | 'quarter' | 'year'>('month')
const [loading, setLoading] = useState(true)
useEffect(() => {
loadAnalyticsData()
}, [selectedTimeframe])
const loadAnalyticsData = async () => {
setLoading(true)
try {
// Simulate loading comprehensive analytics
await new Promise(resolve => setTimeout(resolve, 2000))
setMetrics({
totalStudentsServed: 2847,
totalResourcesAllocated: 15690,
totalDonationsProcessed: 89234,
averageResponseTime: 4.2,
costEfficiencyRatio: 0.87,
volunteerEngagement: 0.93,
schoolPartnershipGrowth: 0.24,
monthlyTrends: [
{ month: 'Jan', studentsServed: 234, resourcesAllocated: 1250, donations: 7800, efficiency: 0.85 },
{ month: 'Feb', studentsServed: 289, resourcesAllocated: 1420, donations: 8900, efficiency: 0.87 },
{ month: 'Mar', studentsServed: 312, resourcesAllocated: 1580, donations: 9200, efficiency: 0.89 },
{ month: 'Apr', studentsServed: 298, resourcesAllocated: 1490, donations: 8700, efficiency: 0.88 },
{ month: 'May', studentsServed: 356, resourcesAllocated: 1780, donations: 10500, efficiency: 0.91 },
{ month: 'Jun', studentsServed: 378, resourcesAllocated: 1890, donations: 11200, efficiency: 0.93 }
]
})
setPredictions({
nextMonthDemand: 425,
budgetProjection: 12800,
volunteerRequirement: 67,
resourceNeeds: [
{ category: 'School Supplies', predictedDemand: 156, currentInventory: 89, recommendedPurchase: 75, urgencyLevel: 'medium' },
{ category: 'Clothing', predictedDemand: 134, currentInventory: 45, recommendedPurchase: 95, urgencyLevel: 'high' },
{ category: 'Food Assistance', predictedDemand: 89, currentInventory: 67, recommendedPurchase: 30, urgencyLevel: 'low' },
{ category: 'Technology', predictedDemand: 46, currentInventory: 12, recommendedPurchase: 40, urgencyLevel: 'critical' }
],
riskFactors: [
'Increased demand in back-to-school season',
'Volunteer availability declining in summer',
'Technology needs growing faster than budget'
],
opportunities: [
'Partnership with local tech companies for device donations',
'Summer clothing drive potential',
'Grant opportunity for educational technology'
]
})
setGeoData([
{ region: 'Downtown Schools', studentsServed: 156, averageNeed: 3.2, responseTime: 3.8, efficiency: 0.91, coordinates: [-122.4194, 37.7749] },
{ region: 'Suburban East', studentsServed: 98, averageNeed: 2.8, responseTime: 4.5, efficiency: 0.85, coordinates: [-122.3894, 37.7849] },
{ region: 'North District', studentsServed: 134, averageNeed: 3.6, responseTime: 4.1, efficiency: 0.88, coordinates: [-122.4094, 37.7949] },
{ region: 'South Valley', studentsServed: 89, averageNeed: 2.9, responseTime: 5.2, efficiency: 0.82, coordinates: [-122.4294, 37.7649] }
])
} catch (error) {
console.error('Error loading analytics:', error)
}
setLoading(false)
}
const getUrgencyColor = (urgency: string) => {
switch (urgency) {
case 'critical': return 'bg-red-500'
case 'high': return 'bg-orange-500'
case 'medium': return 'bg-yellow-500'
case 'low': return 'bg-green-500'
default: return 'bg-gray-500'
}
}
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0
}).format(amount)
}
const formatPercentage = (value: number) => {
return `${(value * 100).toFixed(1)}%`
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full"
/>
<span className="ml-4 text-lg text-blue-700">Loading Advanced Analytics...</span>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<h1 className="text-4xl font-bold text-gray-900 mb-2">Impact Analytics Dashboard</h1>
<p className="text-lg text-gray-600">Comprehensive insights into our nonprofit's reach and effectiveness</p>
<div className="flex gap-4 mt-4">
{(['week', 'month', 'quarter', 'year'] as const).map((timeframe) => (
<button
key={timeframe}
onClick={() => setSelectedTimeframe(timeframe)}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
selectedTimeframe === timeframe
? 'bg-blue-500 text-white shadow-lg'
: 'bg-white text-gray-700 hover:bg-blue-50'
}`}
>
{timeframe.charAt(0).toUpperCase() + timeframe.slice(1)}
</button>
))}
</div>
</motion.div>
{/* Key Metrics Grid */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"
>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Students Served</h3>
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<span className="text-blue-600 font-bold">👥</span>
</div>
</div>
<div className="text-3xl font-bold text-blue-600 mb-2">{metrics?.totalStudentsServed.toLocaleString()}</div>
<div className="text-sm text-gray-500">+12% from last {selectedTimeframe}</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Resources Allocated</h3>
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<span className="text-green-600 font-bold">📦</span>
</div>
</div>
<div className="text-3xl font-bold text-green-600 mb-2">{metrics?.totalResourcesAllocated.toLocaleString()}</div>
<div className="text-sm text-gray-500">+8% from last {selectedTimeframe}</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Donations Processed</h3>
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<span className="text-purple-600 font-bold">💝</span>
</div>
</div>
<div className="text-3xl font-bold text-purple-600 mb-2">{formatCurrency(metrics?.totalDonationsProcessed || 0)}</div>
<div className="text-sm text-gray-500">+15% from last {selectedTimeframe}</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Efficiency Ratio</h3>
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<span className="text-orange-600 font-bold">⚡</span>
</div>
</div>
<div className="text-3xl font-bold text-orange-600 mb-2">{formatPercentage(metrics?.costEfficiencyRatio || 0)}</div>
<div className="text-sm text-gray-500">+3% from last {selectedTimeframe}</div>
</div>
</motion.div>
{/* Trends Chart */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-white rounded-xl p-6 shadow-lg mb-8"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">Monthly Impact Trends</h3>
<div className="h-80 flex items-end justify-between gap-4">
{metrics?.monthlyTrends.map((trend, index) => (
<div key={trend.month} className="flex-1 flex flex-col items-center">
<motion.div
initial={{ height: 0 }}
animate={{ height: `${(trend.studentsServed / 400) * 100}%` }}
transition={{ delay: 0.6 + index * 0.1, duration: 0.8 }}
className="bg-gradient-to-t from-blue-500 to-blue-300 rounded-t-lg w-full mb-2 min-h-[20px]"
/>
<div className="text-sm font-medium text-gray-700">{trend.month}</div>
<div className="text-xs text-gray-500">{trend.studentsServed}</div>
</div>
))}
</div>
</motion.div>
{/* Predictive Analysis and Geographic Data */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Resource Forecasting */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 }}
className="bg-white rounded-xl p-6 shadow-lg"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">Resource Demand Forecast</h3>
<div className="space-y-4">
{predictions?.resourceNeeds.map((resource) => (
<div key={resource.category} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-700">{resource.category}</h4>
<span className={`px-2 py-1 rounded text-xs text-white ${getUrgencyColor(resource.urgencyLevel)}`}>
{resource.urgencyLevel.toUpperCase()}
</span>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500">Predicted Need:</span>
<div className="font-medium">{resource.predictedDemand}</div>
</div>
<div>
<span className="text-gray-500">Current Stock:</span>
<div className="font-medium">{resource.currentInventory}</div>
</div>
<div>
<span className="text-gray-500">Recommended:</span>
<div className="font-medium text-blue-600">+{resource.recommendedPurchase}</div>
</div>
</div>
</div>
))}
</div>
</motion.div>
{/* Geographic Performance */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.8 }}
className="bg-white rounded-xl p-6 shadow-lg"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">Geographic Performance</h3>
<div className="space-y-4">
{geoData.map((region) => (
<div key={region.region} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-700">{region.region}</h4>
<div className="text-sm text-gray-500">{formatPercentage(region.efficiency)} efficiency</div>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500">Students Served:</span>
<div className="font-medium">{region.studentsServed}</div>
</div>
<div>
<span className="text-gray-500">Avg Response:</span>
<div className="font-medium">{region.responseTime}h</div>
</div>
<div>
<span className="text-gray-500">Avg Need Level:</span>
<div className="font-medium">{region.averageNeed}/5</div>
</div>
</div>
</div>
))}
</div>
</motion.div>
</div>
{/* Insights and Recommendations */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.0 }}
className="bg-white rounded-xl p-6 shadow-lg"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">AI-Generated Insights & Recommendations</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-red-600 mb-3">⚠️ Risk Factors</h4>
<ul className="space-y-2">
{predictions?.riskFactors.map((risk, index) => (
<li key={index} className="text-sm text-gray-700 flex items-start">
<span className="text-red-500 mr-2">•</span>
{risk}
</li>
))}
</ul>
</div>
<div>
<h4 className="font-medium text-green-600 mb-3">💡 Opportunities</h4>
<ul className="space-y-2">
{predictions?.opportunities.map((opportunity, index) => (
<li key={index} className="text-sm text-gray-700 flex items-start">
<span className="text-green-500 mr-2">•</span>
{opportunity}
</li>
))}
</ul>
</div>
</div>
</motion.div>
</div>
</div>
)
}
// Phase 3B: Advanced Analytics Dashboard for Nonprofit Impact Tracking
import React, { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
interface ImpactMetrics {
totalStudentsServed: number
totalResourcesAllocated: number
totalDonationsProcessed: number
averageResponseTime: number
costEfficiencyRatio: number
volunteerEngagement: number
schoolPartnershipGrowth: number
monthlyTrends: MonthlyTrend[]
}
interface MonthlyTrend {
month: string
studentsServed: number
resourcesAllocated: number
donations: number
efficiency: number
}
interface PredictiveAnalysis {
nextMonthDemand: number
resourceNeeds: ResourceForecast[]
budgetProjection: number
volunteerRequirement: number
riskFactors: string[]
opportunities: string[]
}
interface ResourceForecast {
category: string
predictedDemand: number
currentInventory: number
recommendedPurchase: number
urgencyLevel: 'low' | 'medium' | 'high' | 'critical'
}
interface GeographicData {
region: string
studentsServed: number
averageNeed: number
responseTime: number
efficiency: number
coordinates: [number, number]
}
const AdvancedAnalyticsDashboard: React.FC = () => {
const [metrics, setMetrics] = useState<ImpactMetrics | null>(null)
const [predictions, setPredictions] = useState<PredictiveAnalysis | null>(null)
const [geoData, setGeoData] = useState<GeographicData[]>([])
const [selectedTimeframe, setSelectedTimeframe] = useState<'week' | 'month' | 'quarter' | 'year'>('month')
const [loading, setLoading] = useState(true)
useEffect(() => {
loadAnalyticsData()
}, [selectedTimeframe])
const loadAnalyticsData = async () => {
setLoading(true)
try {
// Simulate loading comprehensive analytics
await new Promise(resolve => setTimeout(resolve, 2000))
setMetrics({
totalStudentsServed: 2847,
totalResourcesAllocated: 15690,
totalDonationsProcessed: 89234,
averageResponseTime: 4.2,
costEfficiencyRatio: 0.87,
volunteerEngagement: 0.93,
schoolPartnershipGrowth: 0.24,
monthlyTrends: [
{ month: 'Jan', studentsServed: 234, resourcesAllocated: 1250, donations: 7800, efficiency: 0.85 },
{ month: 'Feb', studentsServed: 289, resourcesAllocated: 1420, donations: 8900, efficiency: 0.87 },
{ month: 'Mar', studentsServed: 312, resourcesAllocated: 1580, donations: 9200, efficiency: 0.89 },
{ month: 'Apr', studentsServed: 298, resourcesAllocated: 1490, donations: 8700, efficiency: 0.88 },
{ month: 'May', studentsServed: 356, resourcesAllocated: 1780, donations: 10500, efficiency: 0.91 },
{ month: 'Jun', studentsServed: 378, resourcesAllocated: 1890, donations: 11200, efficiency: 0.93 }
]
})
setPredictions({
nextMonthDemand: 425,
budgetProjection: 12800,
volunteerRequirement: 67,
resourceNeeds: [
{ category: 'School Supplies', predictedDemand: 156, currentInventory: 89, recommendedPurchase: 75, urgencyLevel: 'medium' },
{ category: 'Clothing', predictedDemand: 134, currentInventory: 45, recommendedPurchase: 95, urgencyLevel: 'high' },
{ category: 'Food Assistance', predictedDemand: 89, currentInventory: 67, recommendedPurchase: 30, urgencyLevel: 'low' },
{ category: 'Technology', predictedDemand: 46, currentInventory: 12, recommendedPurchase: 40, urgencyLevel: 'critical' }
],
riskFactors: [
'Increased demand in back-to-school season',
'Volunteer availability declining in summer',
'Technology needs growing faster than budget'
],
opportunities: [
'Partnership with local tech companies for device donations',
'Summer clothing drive potential',
'Grant opportunity for educational technology'
]
})
setGeoData([
{ region: 'Downtown Schools', studentsServed: 156, averageNeed: 3.2, responseTime: 3.8, efficiency: 0.91, coordinates: [-122.4194, 37.7749] },
{ region: 'Suburban East', studentsServed: 98, averageNeed: 2.8, responseTime: 4.5, efficiency: 0.85, coordinates: [-122.3894, 37.7849] },
{ region: 'North District', studentsServed: 134, averageNeed: 3.6, responseTime: 4.1, efficiency: 0.88, coordinates: [-122.4094, 37.7949] },
{ region: 'South Valley', studentsServed: 89, averageNeed: 2.9, responseTime: 5.2, efficiency: 0.82, coordinates: [-122.4294, 37.7649] }
])
} catch (error) {
console.error('Error loading analytics:', error)
}
setLoading(false)
}
const getUrgencyColor = (urgency: string) => {
switch (urgency) {
case 'critical': return 'bg-red-500'
case 'high': return 'bg-orange-500'
case 'medium': return 'bg-yellow-500'
case 'low': return 'bg-green-500'
default: return 'bg-gray-500'
}
}
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0
}).format(amount)
}
const formatPercentage = (value: number) => {
return `${(value * 100).toFixed(1)}%`
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full"
/>
<span className="ml-4 text-lg text-blue-700">Loading Advanced Analytics...</span>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<h1 className="text-4xl font-bold text-gray-900 mb-2">Impact Analytics Dashboard</h1>
<p className="text-lg text-gray-600">Comprehensive insights into our nonprofit's reach and effectiveness</p>
<div className="flex gap-4 mt-4">
{(['week', 'month', 'quarter', 'year'] as const).map((timeframe) => (
<button
key={timeframe}
onClick={() => setSelectedTimeframe(timeframe)}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
selectedTimeframe === timeframe
? 'bg-blue-500 text-white shadow-lg'
: 'bg-white text-gray-700 hover:bg-blue-50'
}`}
>
{timeframe.charAt(0).toUpperCase() + timeframe.slice(1)}
</button>
))}
</div>
</motion.div>
{/* Key Metrics Grid */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"
>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Students Served</h3>
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<span className="text-blue-600 font-bold">👥</span>
</div>
</div>
<div className="text-3xl font-bold text-blue-600 mb-2">{metrics?.totalStudentsServed.toLocaleString()}</div>
<div className="text-sm text-gray-500">+12% from last {selectedTimeframe}</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Resources Allocated</h3>
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<span className="text-green-600 font-bold">📦</span>
</div>
</div>
<div className="text-3xl font-bold text-green-600 mb-2">{metrics?.totalResourcesAllocated.toLocaleString()}</div>
<div className="text-sm text-gray-500">+8% from last {selectedTimeframe}</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Donations Processed</h3>
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<span className="text-purple-600 font-bold">💝</span>
</div>
</div>
<div className="text-3xl font-bold text-purple-600 mb-2">{formatCurrency(metrics?.totalDonationsProcessed || 0)}</div>
<div className="text-sm text-gray-500">+15% from last {selectedTimeframe}</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Efficiency Ratio</h3>
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<span className="text-orange-600 font-bold">⚡</span>
</div>
</div>
<div className="text-3xl font-bold text-orange-600 mb-2">{formatPercentage(metrics?.costEfficiencyRatio || 0)}</div>
<div className="text-sm text-gray-500">+3% from last {selectedTimeframe}</div>
</div>
</motion.div>
{/* Trends Chart */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-white rounded-xl p-6 shadow-lg mb-8"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">Monthly Impact Trends</h3>
<div className="h-80 flex items-end justify-between gap-4">
{metrics?.monthlyTrends.map((trend, index) => (
<div key={trend.month} className="flex-1 flex flex-col items-center">
<motion.div
initial={{ height: 0 }}
animate={{ height: `${(trend.studentsServed / 400) * 100}%` }}
transition={{ delay: 0.6 + index * 0.1, duration: 0.8 }}
className="bg-gradient-to-t from-blue-500 to-blue-300 rounded-t-lg w-full mb-2 min-h-[20px]"
/>
<div className="text-sm font-medium text-gray-700">{trend.month}</div>
<div className="text-xs text-gray-500">{trend.studentsServed}</div>
</div>
))}
</div>
</motion.div>
{/* Predictive Analysis and Geographic Data */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Resource Forecasting */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 }}
className="bg-white rounded-xl p-6 shadow-lg"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">Resource Demand Forecast</h3>
<div className="space-y-4">
{predictions?.resourceNeeds.map((resource) => (
<div key={resource.category} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-700">{resource.category}</h4>
<span className={`px-2 py-1 rounded text-xs text-white ${getUrgencyColor(resource.urgencyLevel)}`}>
{resource.urgencyLevel.toUpperCase()}
</span>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500">Predicted Need:</span>
<div className="font-medium">{resource.predictedDemand}</div>
</div>
<div>
<span className="text-gray-500">Current Stock:</span>
<div className="font-medium">{resource.currentInventory}</div>
</div>
<div>
<span className="text-gray-500">Recommended:</span>
<div className="font-medium text-blue-600">+{resource.recommendedPurchase}</div>
</div>
</div>
</div>
))}
</div>
</motion.div>
{/* Geographic Performance */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.8 }}
className="bg-white rounded-xl p-6 shadow-lg"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">Geographic Performance</h3>
<div className="space-y-4">
{geoData.map((region) => (
<div key={region.region} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-700">{region.region}</h4>
<div className="text-sm text-gray-500">{formatPercentage(region.efficiency)} efficiency</div>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500">Students Served:</span>
<div className="font-medium">{region.studentsServed}</div>
</div>
<div>
<span className="text-gray-500">Avg Response:</span>
<div className="font-medium">{region.responseTime}h</div>
</div>
<div>
<span className="text-gray-500">Avg Need Level:</span>
<div className="font-medium">{region.averageNeed}/5</div>
</div>
</div>
</div>
))}
</div>
</motion.div>
</div>
{/* Insights and Recommendations */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.0 }}
className="bg-white rounded-xl p-6 shadow-lg"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">AI-Generated Insights & Recommendations</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-red-600 mb-3">⚠️ Risk Factors</h4>
<ul className="space-y-2">
{predictions?.riskFactors.map((risk, index) => (
<li key={index} className="text-sm text-gray-700 flex items-start">
<span className="text-red-500 mr-2">•</span>
{risk}
</li>
))}
</ul>
</div>
<div>
<h4 className="font-medium text-green-600 mb-3">💡 Opportunities</h4>
<ul className="space-y-2">
{predictions?.opportunities.map((opportunity, index) => (
<li key={index} className="text-sm text-gray-700 flex items-start">
<span className="text-green-500 mr-2">•</span>
{opportunity}
</li>
))}
</ul>
</div>
</div>
</motion.div>
</div>
</div>
)
}
export default AdvancedAnalyticsDashboard
+63 -63
View File
@@ -1,64 +1,64 @@
import {
Facebook,
Globe,
Instagram,
} from 'lucide-react'
import { LogoMark } from './ui/LogoMark'
export 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:[email protected]" className="navlink">Contact Us</a></li>
<li><a href="tel:+18184916884" className="navlink">(818) 491-6884</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>© 2025 Miracles in Motion. All rights reserved. EIN: 88-1234567</p>
<p className="mt-1">501(c)(3) nonprofit organization. Donations are tax-deductible to the extent allowed by law.</p>
</div>
</div>
</footer>
)
}
import {
Facebook,
Globe,
Instagram,
} from 'lucide-react'
import { LogoMark } from './ui/LogoMark'
export 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:[email protected]" className="navlink">Contact Us</a></li>
<li><a href="tel:+18184916884" className="navlink">(818) 491-6884</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>© 2025 Miracles in Motion. All rights reserved. EIN: 88-1234567</p>
<p className="mt-1">501(c)(3) nonprofit organization. Donations are tax-deductible to the extent allowed by law.</p>
</div>
</div>
</footer>
)
}
export default Footer
File diff suppressed because it is too large Load Diff
+153 -153
View File
@@ -1,154 +1,154 @@
import React, { useEffect } from 'react'
import { motion } from 'framer-motion'
import {
Heart,
Menu,
Moon,
SunMedium,
X,
} from 'lucide-react'
// Import UI components
import { Magnetic, LogoMark } from './ui'
interface NavProps {
darkMode: boolean
setDarkMode: (value: boolean) => void
mobileMenuOpen: boolean
setMobileMenuOpen: (value: boolean) => void
}
export function Navigation({ 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
}}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
style={{ overflow: 'hidden' }}
>
<div className="border-t border-neutral-200/50 bg-white/95 px-4 py-3 backdrop-blur dark:border-white/10 dark:bg-black/90">
<div className="space-y-3">
<a className="block py-2 text-sm font-medium" href="#/stories">Stories</a>
<a className="block py-2 text-sm font-medium" href="#/testimonies">Testimonies</a>
<a className="block py-2 text-sm font-medium" href="#/volunteers">Volunteers</a>
<a className="block py-2 text-sm font-medium" href="#/sponsors">Corporate</a>
<a className="block py-2 text-sm font-medium" href="#/request-assistance">Get Help</a>
<a className="block py-2 text-sm font-medium" href="#/portals">Portals</a>
</div>
</div>
</motion.div>
</>
)
}
import React, { useEffect } from 'react'
import { motion } from 'framer-motion'
import {
Heart,
Menu,
Moon,
SunMedium,
X,
} from 'lucide-react'
// Import UI components
import { Magnetic, LogoMark } from './ui'
interface NavProps {
darkMode: boolean
setDarkMode: (value: boolean) => void
mobileMenuOpen: boolean
setMobileMenuOpen: (value: boolean) => void
}
export function Navigation({ 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
}}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
style={{ overflow: 'hidden' }}
>
<div className="border-t border-neutral-200/50 bg-white/95 px-4 py-3 backdrop-blur dark:border-white/10 dark:bg-black/90">
<div className="space-y-3">
<a className="block py-2 text-sm font-medium" href="#/stories">Stories</a>
<a className="block py-2 text-sm font-medium" href="#/testimonies">Testimonies</a>
<a className="block py-2 text-sm font-medium" href="#/volunteers">Volunteers</a>
<a className="block py-2 text-sm font-medium" href="#/sponsors">Corporate</a>
<a className="block py-2 text-sm font-medium" href="#/request-assistance">Get Help</a>
<a className="block py-2 text-sm font-medium" href="#/portals">Portals</a>
</div>
</div>
</motion.div>
</>
)
}
export default Navigation
+104 -104
View File
@@ -1,105 +1,105 @@
import { Helmet } from 'react-helmet-async'
interface SEOHeadProps {
title?: string
description?: string
image?: string
url?: string
type?: 'website' | 'article'
article?: {
author?: string
publishedTime?: string
modifiedTime?: string
tags?: string[]
}
}
export function SEOHead({
title = 'Miracles in Motion - Empowering Students with Essential Support',
description = 'A 501(c)(3) nonprofit providing students with school supplies, clothing, and emergency support to help them succeed in school and life.',
image = '/og-image.jpg',
url = 'https://miraclesinmotion.org',
type = 'website',
article
}: SEOHeadProps) {
const fullTitle = title.includes('Miracles in Motion') ? title : `${title} | Miracles in Motion`
const fullUrl = url.startsWith('http') ? url : `https://miraclesinmotion.org${url}`
const fullImage = image.startsWith('http') ? image : `https://miraclesinmotion.org${image}`
return (
<Helmet>
{/* Basic Meta Tags */}
<title>{fullTitle}</title>
<meta name="description" content={description} />
<meta name="keywords" content="nonprofit, education, student support, school supplies, clothing assistance, emergency aid, 501c3, charity, community" />
<link rel="canonical" href={fullUrl} />
{/* Open Graph Meta Tags */}
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={fullImage} />
<meta property="og:url" content={fullUrl} />
<meta property="og:type" content={type} />
<meta property="og:site_name" content="Miracles in Motion" />
<meta property="og:locale" content="en_US" />
{/* Twitter Card Meta Tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={fullImage} />
<meta name="twitter:site" content="@MiraclesInMotion" />
<meta name="twitter:creator" content="@MiraclesInMotion" />
{/* Article-specific meta tags */}
{type === 'article' && article && (
<>
{article.author && <meta property="article:author" content={article.author} />}
{article.publishedTime && <meta property="article:published_time" content={article.publishedTime} />}
{article.modifiedTime && <meta property="article:modified_time" content={article.modifiedTime} />}
{article.tags && article.tags.map((tag, index) => (
<meta key={index} property="article:tag" content={tag} />
))}
</>
)}
{/* Additional SEO Meta Tags */}
<meta name="robots" content="index, follow" />
<meta name="author" content="Miracles in Motion" />
<meta name="revisit-after" content="7 days" />
<meta name="rating" content="General" />
{/* Structured Data */}
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "Organization",
"name": "Miracles in Motion",
"url": "https://miraclesinmotion.org",
"logo": "https://miraclesinmotion.org/logo.png",
"description": description,
"foundingDate": "2020",
"areaServed": "United States",
"sameAs": [
"https://facebook.com/miraclesinmotion",
"https://twitter.com/miraclesinmotion",
"https://instagram.com/miraclesinmotion"
],
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+1-818-491-6884",
"contactType": "customer service",
"email": "[email protected]"
},
"address": {
"@type": "PostalAddress",
"addressCountry": "US"
},
"nonprofitStatus": "501(c)(3)"
})}
</script>
</Helmet>
)
}
import { Helmet } from 'react-helmet-async'
interface SEOHeadProps {
title?: string
description?: string
image?: string
url?: string
type?: 'website' | 'article'
article?: {
author?: string
publishedTime?: string
modifiedTime?: string
tags?: string[]
}
}
export function SEOHead({
title = 'Miracles in Motion - Empowering Students with Essential Support',
description = 'A 501(c)(3) nonprofit providing students with school supplies, clothing, and emergency support to help them succeed in school and life.',
image = '/og-image.jpg',
url = 'https://miraclesinmotion.org',
type = 'website',
article
}: SEOHeadProps) {
const fullTitle = title.includes('Miracles in Motion') ? title : `${title} | Miracles in Motion`
const fullUrl = url.startsWith('http') ? url : `https://miraclesinmotion.org${url}`
const fullImage = image.startsWith('http') ? image : `https://miraclesinmotion.org${image}`
return (
<Helmet>
{/* Basic Meta Tags */}
<title>{fullTitle}</title>
<meta name="description" content={description} />
<meta name="keywords" content="nonprofit, education, student support, school supplies, clothing assistance, emergency aid, 501c3, charity, community" />
<link rel="canonical" href={fullUrl} />
{/* Open Graph Meta Tags */}
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={fullImage} />
<meta property="og:url" content={fullUrl} />
<meta property="og:type" content={type} />
<meta property="og:site_name" content="Miracles in Motion" />
<meta property="og:locale" content="en_US" />
{/* Twitter Card Meta Tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={fullImage} />
<meta name="twitter:site" content="@MiraclesInMotion" />
<meta name="twitter:creator" content="@MiraclesInMotion" />
{/* Article-specific meta tags */}
{type === 'article' && article && (
<>
{article.author && <meta property="article:author" content={article.author} />}
{article.publishedTime && <meta property="article:published_time" content={article.publishedTime} />}
{article.modifiedTime && <meta property="article:modified_time" content={article.modifiedTime} />}
{article.tags && article.tags.map((tag, index) => (
<meta key={index} property="article:tag" content={tag} />
))}
</>
)}
{/* Additional SEO Meta Tags */}
<meta name="robots" content="index, follow" />
<meta name="author" content="Miracles in Motion" />
<meta name="revisit-after" content="7 days" />
<meta name="rating" content="General" />
{/* Structured Data */}
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "Organization",
"name": "Miracles in Motion",
"url": "https://miraclesinmotion.org",
"logo": "https://miraclesinmotion.org/logo.png",
"description": description,
"foundingDate": "2020",
"areaServed": "United States",
"sameAs": [
"https://facebook.com/miraclesinmotion",
"https://twitter.com/miraclesinmotion",
"https://instagram.com/miraclesinmotion"
],
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+1-818-491-6884",
"contactType": "customer service",
"email": "[email protected]"
},
"address": {
"@type": "PostalAddress",
"addressCountry": "US"
},
"nonprofitStatus": "501(c)(3)"
})}
</script>
</Helmet>
)
}
export default SEOHead
+68 -68
View File
@@ -1,69 +1,69 @@
import React from 'react'
import { Helmet } from 'react-helmet-async'
interface SEOProps {
title?: string
description?: string
keywords?: string[]
image?: string
url?: string
type?: 'website' | 'article' | 'organization'
}
export const SEO: React.FC<SEOProps> = ({
title = 'Miracles In Motion - Supporting Students in Need',
description = 'A 501(c)3 non-profit providing school supplies, clothing, and emergency assistance to students and families in need.',
keywords = ['nonprofit', 'charity', '501c3', 'student assistance', 'school supplies', 'donations'],
image = 'https://miraclesinmotion.org/og-image.png',
url = 'https://miraclesinmotion.org',
type = 'website'
}) => {
const structuredData = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Miracles In Motion",
"description": description,
"url": url,
"logo": "https://miraclesinmotion.org/logo.png",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+1-555-123-4567",
"contactType": "Customer Service"
},
"sameAs": [
"https://facebook.com/miraclesinmotion",
"https://instagram.com/miraclesinmotion"
]
}
return (
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta name="keywords" content={keywords.join(', ')} />
{/* Open Graph */}
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" content={url} />
<meta property="og:type" content={type} />
{/* Twitter Card */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
{/* Structured Data */}
<script type="application/ld+json">
{JSON.stringify(structuredData)}
</script>
{/* Additional Meta Tags */}
<meta name="robots" content="index, follow" />
<meta name="author" content="Miracles In Motion" />
<link rel="canonical" href={url} />
</Helmet>
)
import React from 'react'
import { Helmet } from 'react-helmet-async'
interface SEOProps {
title?: string
description?: string
keywords?: string[]
image?: string
url?: string
type?: 'website' | 'article' | 'organization'
}
export const SEO: React.FC<SEOProps> = ({
title = 'Miracles In Motion - Supporting Students in Need',
description = 'A 501(c)3 non-profit providing school supplies, clothing, and emergency assistance to students and families in need.',
keywords = ['nonprofit', 'charity', '501c3', 'student assistance', 'school supplies', 'donations'],
image = 'https://miraclesinmotion.org/og-image.png',
url = 'https://miraclesinmotion.org',
type = 'website'
}) => {
const structuredData = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Miracles In Motion",
"description": description,
"url": url,
"logo": "https://miraclesinmotion.org/logo.png",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+1-555-123-4567",
"contactType": "Customer Service"
},
"sameAs": [
"https://facebook.com/miraclesinmotion",
"https://instagram.com/miraclesinmotion"
]
}
return (
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta name="keywords" content={keywords.join(', ')} />
{/* Open Graph */}
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" content={url} />
<meta property="og:type" content={type} />
{/* Twitter Card */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
{/* Structured Data */}
<script type="application/ld+json">
{JSON.stringify(structuredData)}
</script>
{/* Additional Meta Tags */}
<meta name="robots" content="index, follow" />
<meta name="author" content="Miracles In Motion" />
<link rel="canonical" href={url} />
</Helmet>
)
}
File diff suppressed because it is too large Load Diff
+73 -73
View File
@@ -1,74 +1,74 @@
import { describe, it, expect, vi } from 'vitest'
import { render } from '@testing-library/react'
import { screen } from '@testing-library/dom'
import '@testing-library/jest-dom'
import { Footer } from '../Footer'
// Mock the LogoMark component
vi.mock('../ui/LogoMark', () => ({
LogoMark: () => <div data-testid="logo-mark">Logo</div>
}))
describe('Footer Component', () => {
it('renders footer with logo and brand information', () => {
render(<Footer />)
expect(screen.getByTestId('logo-mark')).toBeInTheDocument()
expect(screen.getByText('Miracles in Motion')).toBeInTheDocument()
expect(screen.getByText('Essentials for every student')).toBeInTheDocument()
})
it('renders organization description', () => {
render(<Footer />)
expect(screen.getByText(/A 501\(c\)\(3\) nonprofit providing students/)).toBeInTheDocument()
})
it('renders social media links', () => {
render(<Footer />)
const socialLinks = screen.getAllByRole('link')
const socialIcons = socialLinks.filter((link: HTMLElement) =>
link.getAttribute('href') === '#'
)
expect(socialIcons).toHaveLength(3) // Facebook, Instagram, Globe
})
it('renders Get Involved section with correct links', () => {
render(<Footer />)
expect(screen.getByText('Get Involved')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Donate' })).toHaveAttribute('href', '#/donate')
expect(screen.getByRole('link', { name: 'Volunteer' })).toHaveAttribute('href', '#/volunteers')
expect(screen.getByRole('link', { name: 'Corporate Partnerships' })).toHaveAttribute('href', '#/sponsors')
expect(screen.getByRole('link', { name: 'Success Stories' })).toHaveAttribute('href', '#/stories')
})
it('renders Organization section with correct links', () => {
render(<Footer />)
expect(screen.getByText('Organization')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Testimonials' })).toHaveAttribute('href', '#/testimonies')
expect(screen.getByRole('link', { name: 'Legal & Policies' })).toHaveAttribute('href', '#/legal')
expect(screen.getByRole('link', { name: 'Contact Us' })).toHaveAttribute('href', 'mailto:[email protected]')
expect(screen.getByRole('link', { name: '(818) 491-6884' })).toHaveAttribute('href', 'tel:+18184916884')
})
it('renders copyright information', () => {
render(<Footer />)
expect(screen.getByText(/© 2025 Miracles in Motion. All rights reserved./)).toBeInTheDocument()
expect(screen.getByText(/501\(c\)\(3\) nonprofit organization./)).toBeInTheDocument()
})
it('has proper accessibility structure', () => {
render(<Footer />)
const footer = screen.getByRole('contentinfo')
expect(footer).toBeInTheDocument()
const headings = screen.getAllByRole('heading', { level: 3 })
expect(headings).toHaveLength(2) // "Get Involved" and "Organization"
})
import { describe, it, expect, vi } from 'vitest'
import { render } from '@testing-library/react'
import { screen } from '@testing-library/dom'
import '@testing-library/jest-dom'
import { Footer } from '../Footer'
// Mock the LogoMark component
vi.mock('../ui/LogoMark', () => ({
LogoMark: () => <div data-testid="logo-mark">Logo</div>
}))
describe('Footer Component', () => {
it('renders footer with logo and brand information', () => {
render(<Footer />)
expect(screen.getByTestId('logo-mark')).toBeInTheDocument()
expect(screen.getByText('Miracles in Motion')).toBeInTheDocument()
expect(screen.getByText('Essentials for every student')).toBeInTheDocument()
})
it('renders organization description', () => {
render(<Footer />)
expect(screen.getByText(/A 501\(c\)\(3\) nonprofit providing students/)).toBeInTheDocument()
})
it('renders social media links', () => {
render(<Footer />)
const socialLinks = screen.getAllByRole('link')
const socialIcons = socialLinks.filter((link: HTMLElement) =>
link.getAttribute('href') === '#'
)
expect(socialIcons).toHaveLength(3) // Facebook, Instagram, Globe
})
it('renders Get Involved section with correct links', () => {
render(<Footer />)
expect(screen.getByText('Get Involved')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Donate' })).toHaveAttribute('href', '#/donate')
expect(screen.getByRole('link', { name: 'Volunteer' })).toHaveAttribute('href', '#/volunteers')
expect(screen.getByRole('link', { name: 'Corporate Partnerships' })).toHaveAttribute('href', '#/sponsors')
expect(screen.getByRole('link', { name: 'Success Stories' })).toHaveAttribute('href', '#/stories')
})
it('renders Organization section with correct links', () => {
render(<Footer />)
expect(screen.getByText('Organization')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Testimonials' })).toHaveAttribute('href', '#/testimonies')
expect(screen.getByRole('link', { name: 'Legal & Policies' })).toHaveAttribute('href', '#/legal')
expect(screen.getByRole('link', { name: 'Contact Us' })).toHaveAttribute('href', 'mailto:[email protected]')
expect(screen.getByRole('link', { name: '(818) 491-6884' })).toHaveAttribute('href', 'tel:+18184916884')
})
it('renders copyright information', () => {
render(<Footer />)
expect(screen.getByText(/© 2025 Miracles in Motion. All rights reserved./)).toBeInTheDocument()
expect(screen.getByText(/501\(c\)\(3\) nonprofit organization./)).toBeInTheDocument()
})
it('has proper accessibility structure', () => {
render(<Footer />)
const footer = screen.getByRole('contentinfo')
expect(footer).toBeInTheDocument()
const headings = screen.getAllByRole('heading', { level: 3 })
expect(headings).toHaveLength(2) // "Get Involved" and "Organization"
})
})
+86 -86
View File
@@ -1,87 +1,87 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render } from '@testing-library/react'
import { screen, fireEvent } from '@testing-library/dom'
import '@testing-library/jest-dom'
import { Navigation } from '../Navigation'
// Mock the UI components
vi.mock('../ui', () => ({
LogoMark: () => <div data-testid="logo-mark">Logo</div>,
Magnetic: ({ children }: { children: React.ReactNode }) => <div>{children}</div>
}))
describe('Navigation Component', () => {
const mockProps = {
darkMode: false,
setDarkMode: vi.fn(),
mobileMenuOpen: false,
setMobileMenuOpen: vi.fn()
}
beforeEach(() => {
vi.clearAllMocks()
})
it('renders navigation with logo and brand name', () => {
render(<Navigation {...mockProps} />)
expect(screen.getByTestId('logo-mark')).toBeInTheDocument()
expect(screen.getByText('Miracles in Motion')).toBeInTheDocument()
expect(screen.getByText('Essentials for every student')).toBeInTheDocument()
})
it('renders desktop navigation links', () => {
render(<Navigation {...mockProps} />)
expect(screen.getByLabelText('Read success stories')).toBeInTheDocument()
expect(screen.getByLabelText('View testimonies')).toBeInTheDocument()
expect(screen.getByLabelText('Volunteer opportunities')).toBeInTheDocument()
expect(screen.getByLabelText('Corporate partnerships')).toBeInTheDocument()
expect(screen.getByLabelText('Request assistance')).toBeInTheDocument()
expect(screen.getByLabelText('Portal login')).toBeInTheDocument()
})
it('toggles dark mode when button is clicked', () => {
render(<Navigation {...mockProps} />)
const darkModeButtons = screen.getAllByLabelText('Switch to dark mode')
fireEvent.click(darkModeButtons[0])
expect(mockProps.setDarkMode).toHaveBeenCalledWith(true)
})
it('toggles mobile menu when hamburger button is clicked', () => {
render(<Navigation {...mockProps} />)
const mobileMenuButton = screen.getByLabelText('Open navigation menu')
fireEvent.click(mobileMenuButton)
expect(mockProps.setMobileMenuOpen).toHaveBeenCalledWith(true)
})
it('displays mobile menu when mobileMenuOpen is true', () => {
render(<Navigation {...mockProps} mobileMenuOpen={true} />)
const storiesLinks = screen.getAllByText('Stories')
expect(storiesLinks).toHaveLength(2) // One in desktop nav, one in mobile nav
expect(screen.getByText('Testimonies')).toBeInTheDocument()
expect(screen.getByText('Volunteers')).toBeInTheDocument()
})
it('handles keyboard navigation correctly', () => {
render(<Navigation {...mockProps} />)
const darkModeButton = screen.getAllByLabelText('Switch to dark mode')[0]
fireEvent.keyDown(darkModeButton, { key: 'Enter', code: 'Enter' })
expect(mockProps.setDarkMode).toHaveBeenCalledWith(true)
})
it('displays correct icon based on dark mode state', () => {
const { rerender } = render(<Navigation {...mockProps} darkMode={false} />)
expect(screen.getAllByLabelText('Switch to dark mode')).toHaveLength(2)
rerender(<Navigation {...mockProps} darkMode={true} />)
expect(screen.getAllByLabelText('Switch to light mode')).toHaveLength(2)
})
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render } from '@testing-library/react'
import { screen, fireEvent } from '@testing-library/dom'
import '@testing-library/jest-dom'
import { Navigation } from '../Navigation'
// Mock the UI components
vi.mock('../ui', () => ({
LogoMark: () => <div data-testid="logo-mark">Logo</div>,
Magnetic: ({ children }: { children: React.ReactNode }) => <div>{children}</div>
}))
describe('Navigation Component', () => {
const mockProps = {
darkMode: false,
setDarkMode: vi.fn(),
mobileMenuOpen: false,
setMobileMenuOpen: vi.fn()
}
beforeEach(() => {
vi.clearAllMocks()
})
it('renders navigation with logo and brand name', () => {
render(<Navigation {...mockProps} />)
expect(screen.getByTestId('logo-mark')).toBeInTheDocument()
expect(screen.getByText('Miracles in Motion')).toBeInTheDocument()
expect(screen.getByText('Essentials for every student')).toBeInTheDocument()
})
it('renders desktop navigation links', () => {
render(<Navigation {...mockProps} />)
expect(screen.getByLabelText('Read success stories')).toBeInTheDocument()
expect(screen.getByLabelText('View testimonies')).toBeInTheDocument()
expect(screen.getByLabelText('Volunteer opportunities')).toBeInTheDocument()
expect(screen.getByLabelText('Corporate partnerships')).toBeInTheDocument()
expect(screen.getByLabelText('Request assistance')).toBeInTheDocument()
expect(screen.getByLabelText('Portal login')).toBeInTheDocument()
})
it('toggles dark mode when button is clicked', () => {
render(<Navigation {...mockProps} />)
const darkModeButtons = screen.getAllByLabelText('Switch to dark mode')
fireEvent.click(darkModeButtons[0])
expect(mockProps.setDarkMode).toHaveBeenCalledWith(true)
})
it('toggles mobile menu when hamburger button is clicked', () => {
render(<Navigation {...mockProps} />)
const mobileMenuButton = screen.getByLabelText('Open navigation menu')
fireEvent.click(mobileMenuButton)
expect(mockProps.setMobileMenuOpen).toHaveBeenCalledWith(true)
})
it('displays mobile menu when mobileMenuOpen is true', () => {
render(<Navigation {...mockProps} mobileMenuOpen={true} />)
const storiesLinks = screen.getAllByText('Stories')
expect(storiesLinks).toHaveLength(2) // One in desktop nav, one in mobile nav
expect(screen.getByText('Testimonies')).toBeInTheDocument()
expect(screen.getByText('Volunteers')).toBeInTheDocument()
})
it('handles keyboard navigation correctly', () => {
render(<Navigation {...mockProps} />)
const darkModeButton = screen.getAllByLabelText('Switch to dark mode')[0]
fireEvent.keyDown(darkModeButton, { key: 'Enter', code: 'Enter' })
expect(mockProps.setDarkMode).toHaveBeenCalledWith(true)
})
it('displays correct icon based on dark mode state', () => {
const { rerender } = render(<Navigation {...mockProps} darkMode={false} />)
expect(screen.getAllByLabelText('Switch to dark mode')).toHaveLength(2)
rerender(<Navigation {...mockProps} darkMode={true} />)
expect(screen.getAllByLabelText('Switch to light mode')).toHaveLength(2)
})
})
+282 -282
View File
@@ -1,283 +1,283 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { motion, AnimatePresence } from 'framer-motion'
import {
Elements,
CardElement,
useStripe,
useElements
} from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
import {
CreditCard,
Lock,
CheckCircle2,
AlertCircle,
Heart,
Users,
Sparkles
} from 'lucide-react'
// Initialize Stripe
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY || '')
interface PaymentFormProps {
amount: number
isRecurring?: boolean
onSuccess?: (paymentIntent: any) => void
onError?: (error: string) => void
}
function PaymentForm({ amount, isRecurring = false, onSuccess, onError }: PaymentFormProps) {
const { t, i18n } = useTranslation()
const stripe = useStripe()
const elements = useElements()
const [isLoading, setIsLoading] = useState(false)
const [paymentStatus, setPaymentStatus] = useState<'idle' | 'processing' | 'succeeded' | 'failed'>('idle')
const [errorMessage, setErrorMessage] = useState('')
const [customerInfo, setCustomerInfo] = useState({
name: '',
email: '',
phone: ''
})
const formatCurrency = (value: number) => {
const formatter = new Intl.NumberFormat(i18n.language, {
style: 'currency',
currency: 'USD'
})
return formatter.format(value / 100) // Stripe uses cents
}
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault()
if (!stripe || !elements) {
return
}
setIsLoading(true)
setPaymentStatus('processing')
setErrorMessage('')
const cardElement = elements.getElement(CardElement)
if (!cardElement) return
try {
// Create payment intent
const response = await fetch('/api/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount,
currency: 'usd',
recurring: isRecurring,
customer: customerInfo
})
})
const { client_secret } = await response.json()
// Confirm payment
const { error, paymentIntent } = await stripe.confirmCardPayment(client_secret, {
payment_method: {
card: cardElement,
billing_details: {
name: customerInfo.name,
email: customerInfo.email
}
}
})
if (error) {
setErrorMessage(error.message || t('donate.error'))
setPaymentStatus('failed')
onError?.(error.message || t('donate.error'))
} else {
setPaymentStatus('succeeded')
onSuccess?.(paymentIntent)
}
} catch (error: any) {
setErrorMessage(error.message || t('donate.error'))
setPaymentStatus('failed')
onError?.(error.message)
}
setIsLoading(false)
}
const cardElementOptions = {
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': {
color: '#aab7c4'
}
}
},
hidePostalCode: false
}
if (paymentStatus === 'succeeded') {
return (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center py-12"
>
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle2 className="w-8 h-8 text-green-600" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">
{t('donate.success')}
</h3>
<p className="text-gray-600 mb-6">
Your donation of {formatCurrency(amount)} will help students in need.
</p>
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg p-6">
<div className="flex items-center justify-center gap-2 text-purple-600 mb-2">
<Sparkles className="w-5 h-5" />
<span className="font-semibold">Impact Preview</span>
</div>
<p className="text-sm text-gray-600">
You've just provided school supplies for {Math.floor(amount / 2500)} students!
</p>
</div>
</motion.div>
)
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Customer Information */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<Users className="w-5 h-5" />
Donor Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Name <span className="text-red-500">*</span>
</label>
<input
type="text"
required
value={customerInfo.name}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="John Doe"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email Address <span className="text-red-500">*</span>
</label>
<input
type="email"
required
value={customerInfo.email}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="[email protected]"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Phone Number (Optional)
</label>
<input
type="tel"
value={customerInfo.phone}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, phone: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="(555) 123-4567"
/>
</div>
</div>
{/* Payment Information */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<CreditCard className="w-5 h-5" />
Payment Information
</h3>
<div className="p-4 border border-gray-300 rounded-lg">
<CardElement options={cardElementOptions} />
</div>
</div>
{/* Donation Summary */}
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<span className="text-lg font-semibold text-gray-900">
{isRecurring ? 'Monthly' : 'One-time'} Donation
</span>
<span className="text-2xl font-bold text-purple-600">
{formatCurrency(amount)}
</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Lock className="w-4 h-4" />
<span>Secure payment powered by Stripe</span>
</div>
</div>
{/* Error Message */}
<AnimatePresence>
{errorMessage && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3"
>
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
<p className="text-red-700 text-sm">{errorMessage}</p>
</motion.div>
)}
</AnimatePresence>
{/* Submit Button */}
<button
type="submit"
disabled={!stripe || isLoading || paymentStatus === 'processing'}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 text-white py-4 px-6 rounded-lg font-semibold text-lg hover:from-purple-700 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2"
>
{isLoading ? (
<>
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
{t('donate.processing')}
</>
) : (
<>
<Heart className="w-5 h-5" />
Donate {formatCurrency(amount)}
</>
)}
</button>
</form>
)
}
export function StripePaymentForm(props: PaymentFormProps) {
return (
<Elements stripe={stripePromise}>
<PaymentForm {...props} />
</Elements>
)
}
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { motion, AnimatePresence } from 'framer-motion'
import {
Elements,
CardElement,
useStripe,
useElements
} from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
import {
CreditCard,
Lock,
CheckCircle2,
AlertCircle,
Heart,
Users,
Sparkles
} from 'lucide-react'
// Initialize Stripe
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY || '')
interface PaymentFormProps {
amount: number
isRecurring?: boolean
onSuccess?: (paymentIntent: any) => void
onError?: (error: string) => void
}
function PaymentForm({ amount, isRecurring = false, onSuccess, onError }: PaymentFormProps) {
const { t, i18n } = useTranslation()
const stripe = useStripe()
const elements = useElements()
const [isLoading, setIsLoading] = useState(false)
const [paymentStatus, setPaymentStatus] = useState<'idle' | 'processing' | 'succeeded' | 'failed'>('idle')
const [errorMessage, setErrorMessage] = useState('')
const [customerInfo, setCustomerInfo] = useState({
name: '',
email: '',
phone: ''
})
const formatCurrency = (value: number) => {
const formatter = new Intl.NumberFormat(i18n.language, {
style: 'currency',
currency: 'USD'
})
return formatter.format(value / 100) // Stripe uses cents
}
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault()
if (!stripe || !elements) {
return
}
setIsLoading(true)
setPaymentStatus('processing')
setErrorMessage('')
const cardElement = elements.getElement(CardElement)
if (!cardElement) return
try {
// Create payment intent
const response = await fetch('/api/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount,
currency: 'usd',
recurring: isRecurring,
customer: customerInfo
})
})
const { client_secret } = await response.json()
// Confirm payment
const { error, paymentIntent } = await stripe.confirmCardPayment(client_secret, {
payment_method: {
card: cardElement,
billing_details: {
name: customerInfo.name,
email: customerInfo.email
}
}
})
if (error) {
setErrorMessage(error.message || t('donate.error'))
setPaymentStatus('failed')
onError?.(error.message || t('donate.error'))
} else {
setPaymentStatus('succeeded')
onSuccess?.(paymentIntent)
}
} catch (error: any) {
setErrorMessage(error.message || t('donate.error'))
setPaymentStatus('failed')
onError?.(error.message)
}
setIsLoading(false)
}
const cardElementOptions = {
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': {
color: '#aab7c4'
}
}
},
hidePostalCode: false
}
if (paymentStatus === 'succeeded') {
return (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center py-12"
>
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle2 className="w-8 h-8 text-green-600" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">
{t('donate.success')}
</h3>
<p className="text-gray-600 mb-6">
Your donation of {formatCurrency(amount)} will help students in need.
</p>
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg p-6">
<div className="flex items-center justify-center gap-2 text-purple-600 mb-2">
<Sparkles className="w-5 h-5" />
<span className="font-semibold">Impact Preview</span>
</div>
<p className="text-sm text-gray-600">
You've just provided school supplies for {Math.floor(amount / 2500)} students!
</p>
</div>
</motion.div>
)
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Customer Information */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<Users className="w-5 h-5" />
Donor Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Name <span className="text-red-500">*</span>
</label>
<input
type="text"
required
value={customerInfo.name}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="John Doe"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email Address <span className="text-red-500">*</span>
</label>
<input
type="email"
required
value={customerInfo.email}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="[email protected]"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Phone Number (Optional)
</label>
<input
type="tel"
value={customerInfo.phone}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, phone: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="(555) 123-4567"
/>
</div>
</div>
{/* Payment Information */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<CreditCard className="w-5 h-5" />
Payment Information
</h3>
<div className="p-4 border border-gray-300 rounded-lg">
<CardElement options={cardElementOptions} />
</div>
</div>
{/* Donation Summary */}
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<span className="text-lg font-semibold text-gray-900">
{isRecurring ? 'Monthly' : 'One-time'} Donation
</span>
<span className="text-2xl font-bold text-purple-600">
{formatCurrency(amount)}
</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Lock className="w-4 h-4" />
<span>Secure payment powered by Stripe</span>
</div>
</div>
{/* Error Message */}
<AnimatePresence>
{errorMessage && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3"
>
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
<p className="text-red-700 text-sm">{errorMessage}</p>
</motion.div>
)}
</AnimatePresence>
{/* Submit Button */}
<button
type="submit"
disabled={!stripe || isLoading || paymentStatus === 'processing'}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 text-white py-4 px-6 rounded-lg font-semibold text-lg hover:from-purple-700 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2"
>
{isLoading ? (
<>
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
{t('donate.processing')}
</>
) : (
<>
<Heart className="w-5 h-5" />
Donate {formatCurrency(amount)}
</>
)}
</button>
</form>
)
}
export function StripePaymentForm(props: PaymentFormProps) {
return (
<Elements stripe={stripePromise}>
<PaymentForm {...props} />
</Elements>
)
}
export default StripePaymentForm
+100 -100
View File
@@ -1,101 +1,101 @@
import { useRef } from 'react'
import { motion, useScroll, useTransform } from 'framer-motion'
import { ArrowRight, Heart, Sparkles } from 'lucide-react'
export function HeroSection() {
const containerRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['start start', 'end start']
})
const y = useTransform(scrollYProgress, [0, 1], ['0%', '50%'])
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0])
return (
<section ref={containerRef} className="relative min-h-screen flex items-center justify-center overflow-hidden">
{/* Animated Background */}
<motion.div
className="absolute inset-0 bg-gradient-to-br from-primary-50 via-white to-secondary-50 dark:from-gray-900 dark:to-purple-900"
style={{ y }}
/>
{/* Floating Elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{[...Array(20)].map((_, i) => (
<motion.div
key={i}
className="absolute w-2 h-2 bg-primary-300/20 rounded-full"
initial={{
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight
}}
animate={{
y: [null, Math.random() * -100 - 50],
opacity: [0, 1, 0]
}}
transition={{
duration: Math.random() * 3 + 2,
repeat: Infinity,
delay: Math.random() * 5
}}
/>
))}
</div>
{/* Main Content */}
<motion.div
className="relative z-10 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 text-center"
style={{ opacity }}
>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<div className="flex items-center justify-center gap-2 mb-6">
<Sparkles className="w-8 h-8 text-primary-600 animate-pulse" />
<span className="text-lg font-medium text-primary-600 uppercase tracking-wider">
501(c)3 Non-Profit Organization
</span>
</div>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold text-gray-900 dark:text-white mb-6 leading-tight">
Miracles in{' '}
<span className="bg-gradient-to-r from-primary-600 via-secondary-600 to-primary-800 bg-clip-text text-transparent animate-pulse">
Motion
</span>
</h1>
<p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 mb-8 max-w-4xl mx-auto leading-relaxed">
Empowering students with essential supplies, clothing, and support to succeed in school and life.
Every child deserves the tools they need to learn and grow.
</p>
<div className="flex flex-col sm:flex-row gap-6 justify-center">
<motion.a
href="#/donate"
className="btn-primary inline-flex items-center justify-center text-lg px-8 py-4"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Heart className="mr-3 h-6 w-6" />
Donate Now
</motion.a>
<motion.a
href="#/request-assistance"
className="btn-secondary inline-flex items-center justify-center text-lg px-8 py-4"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Get Help
<ArrowRight className="ml-3 h-6 w-6" />
</motion.a>
</div>
</motion.div>
</motion.div>
</section>
)
}
import { useRef } from 'react'
import { motion, useScroll, useTransform } from 'framer-motion'
import { ArrowRight, Heart, Sparkles } from 'lucide-react'
export function HeroSection() {
const containerRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['start start', 'end start']
})
const y = useTransform(scrollYProgress, [0, 1], ['0%', '50%'])
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0])
return (
<section ref={containerRef} className="relative min-h-screen flex items-center justify-center overflow-hidden">
{/* Animated Background */}
<motion.div
className="absolute inset-0 bg-gradient-to-br from-primary-50 via-white to-secondary-50 dark:from-gray-900 dark:to-purple-900"
style={{ y }}
/>
{/* Floating Elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{[...Array(20)].map((_, i) => (
<motion.div
key={i}
className="absolute w-2 h-2 bg-primary-300/20 rounded-full"
initial={{
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight
}}
animate={{
y: [null, Math.random() * -100 - 50],
opacity: [0, 1, 0]
}}
transition={{
duration: Math.random() * 3 + 2,
repeat: Infinity,
delay: Math.random() * 5
}}
/>
))}
</div>
{/* Main Content */}
<motion.div
className="relative z-10 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 text-center"
style={{ opacity }}
>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<div className="flex items-center justify-center gap-2 mb-6">
<Sparkles className="w-8 h-8 text-primary-600 animate-pulse" />
<span className="text-lg font-medium text-primary-600 uppercase tracking-wider">
501(c)3 Non-Profit Organization
</span>
</div>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold text-gray-900 dark:text-white mb-6 leading-tight">
Miracles in{' '}
<span className="bg-gradient-to-r from-primary-600 via-secondary-600 to-primary-800 bg-clip-text text-transparent animate-pulse">
Motion
</span>
</h1>
<p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 mb-8 max-w-4xl mx-auto leading-relaxed">
Empowering students with essential supplies, clothing, and support to succeed in school and life.
Every child deserves the tools they need to learn and grow.
</p>
<div className="flex flex-col sm:flex-row gap-6 justify-center">
<motion.a
href="#/donate"
className="btn-primary inline-flex items-center justify-center text-lg px-8 py-4"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Heart className="mr-3 h-6 w-6" />
Donate Now
</motion.a>
<motion.a
href="#/request-assistance"
className="btn-secondary inline-flex items-center justify-center text-lg px-8 py-4"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Get Help
<ArrowRight className="ml-3 h-6 w-6" />
</motion.a>
</div>
</motion.div>
</motion.div>
</section>
)
}
export default HeroSection
+149 -149
View File
@@ -1,150 +1,150 @@
import { motion, useSpring, useMotionValue, useTransform } from 'framer-motion'
import { useEffect, useRef } from 'react'
import { Users, Heart, Backpack, Star, TrendingUp, Award } from 'lucide-react'
import { SectionHeader } from '../ui/SectionHeader'
function AnimatedCounter({ target, suffix = '' }: { target: number, suffix?: string }) {
const ref = useRef<HTMLSpanElement>(null)
const motionValue = useMotionValue(0)
const springValue = useSpring(motionValue, { duration: 2000 })
const displayed = useTransform(springValue, (latest) =>
Math.round(latest).toLocaleString() + suffix
)
useEffect(() => {
motionValue.set(target)
}, [motionValue, target])
useEffect(() => {
return displayed.onChange((latest) => {
if (ref.current) {
ref.current.textContent = latest
}
})
}, [displayed])
return <span ref={ref} />
}
export function ImpactSection() {
const stats = [
{
icon: Users,
value: 2847,
label: "Students Helped",
description: "Individual students who received direct support",
trend: "+23% this year",
color: "from-blue-500 to-blue-600"
},
{
icon: Heart,
value: 1203,
label: "Families Supported",
description: "Complete family units assisted with comprehensive care",
trend: "+15% this year",
color: "from-red-500 to-red-600"
},
{
icon: Backpack,
value: 15624,
label: "Items Distributed",
description: "School supplies, clothing, and essential items provided",
trend: "+31% this year",
color: "from-green-500 to-green-600"
},
{
icon: Star,
value: 8456,
label: "Volunteer Hours",
description: "Dedicated community service hours contributed",
trend: "+42% this year",
color: "from-yellow-500 to-yellow-600"
},
{
icon: TrendingUp,
value: 94,
suffix: "%",
label: "Success Rate",
description: "Students showing improved academic performance",
trend: "Consistent excellence",
color: "from-purple-500 to-purple-600"
},
{
icon: Award,
value: 156,
label: "Partner Organizations",
description: "Schools, nonprofits, and businesses in our network",
trend: "+67% this year",
color: "from-orange-500 to-orange-600"
}
]
return (
<section className="py-20 bg-gradient-to-br from-gray-50 to-white dark:from-gray-800 dark:to-gray-900">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionHeader
eyebrow="Our Impact"
title="Making a Measurable Difference"
subtitle="Real numbers, real change, real lives transformed through community support"
/>
<div className="mt-16 grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{stats.map((stat, index) => (
<motion.div
key={index}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
>
{/* Icon */}
<div className={`w-16 h-16 mb-6 bg-gradient-to-br ${stat.color} rounded-xl flex items-center justify-center`}>
<stat.icon className="h-8 w-8 text-white" />
</div>
{/* Main Number */}
<div className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
<AnimatedCounter target={stat.value} suffix={stat.suffix} />
</div>
{/* Label */}
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{stat.label}
</h3>
{/* Description */}
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4 leading-relaxed">
{stat.description}
</p>
{/* Trend */}
<div className="flex items-center text-sm font-medium text-green-600 dark:text-green-400">
<TrendingUp className="h-4 w-4 mr-1" />
{stat.trend}
</div>
</motion.div>
))}
</div>
{/* Additional Impact Statement */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="mt-16 text-center bg-gradient-to-r from-primary-500 to-secondary-600 rounded-2xl p-8 text-white"
>
<h3 className="text-2xl font-bold mb-4">
Every Number Represents a Life Changed
</h3>
<p className="text-lg opacity-90 max-w-3xl mx-auto">
Behind every statistic is a student who can now focus on learning, a family with renewed hope,
and a community growing stronger together.
</p>
</motion.div>
</div>
</section>
)
}
import { motion, useSpring, useMotionValue, useTransform } from 'framer-motion'
import { useEffect, useRef } from 'react'
import { Users, Heart, Backpack, Star, TrendingUp, Award } from 'lucide-react'
import { SectionHeader } from '../ui/SectionHeader'
function AnimatedCounter({ target, suffix = '' }: { target: number, suffix?: string }) {
const ref = useRef<HTMLSpanElement>(null)
const motionValue = useMotionValue(0)
const springValue = useSpring(motionValue, { duration: 2000 })
const displayed = useTransform(springValue, (latest) =>
Math.round(latest).toLocaleString() + suffix
)
useEffect(() => {
motionValue.set(target)
}, [motionValue, target])
useEffect(() => {
return displayed.onChange((latest) => {
if (ref.current) {
ref.current.textContent = latest
}
})
}, [displayed])
return <span ref={ref} />
}
export function ImpactSection() {
const stats = [
{
icon: Users,
value: 2847,
label: "Students Helped",
description: "Individual students who received direct support",
trend: "+23% this year",
color: "from-blue-500 to-blue-600"
},
{
icon: Heart,
value: 1203,
label: "Families Supported",
description: "Complete family units assisted with comprehensive care",
trend: "+15% this year",
color: "from-red-500 to-red-600"
},
{
icon: Backpack,
value: 15624,
label: "Items Distributed",
description: "School supplies, clothing, and essential items provided",
trend: "+31% this year",
color: "from-green-500 to-green-600"
},
{
icon: Star,
value: 8456,
label: "Volunteer Hours",
description: "Dedicated community service hours contributed",
trend: "+42% this year",
color: "from-yellow-500 to-yellow-600"
},
{
icon: TrendingUp,
value: 94,
suffix: "%",
label: "Success Rate",
description: "Students showing improved academic performance",
trend: "Consistent excellence",
color: "from-purple-500 to-purple-600"
},
{
icon: Award,
value: 156,
label: "Partner Organizations",
description: "Schools, nonprofits, and businesses in our network",
trend: "+67% this year",
color: "from-orange-500 to-orange-600"
}
]
return (
<section className="py-20 bg-gradient-to-br from-gray-50 to-white dark:from-gray-800 dark:to-gray-900">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionHeader
eyebrow="Our Impact"
title="Making a Measurable Difference"
subtitle="Real numbers, real change, real lives transformed through community support"
/>
<div className="mt-16 grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{stats.map((stat, index) => (
<motion.div
key={index}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
>
{/* Icon */}
<div className={`w-16 h-16 mb-6 bg-gradient-to-br ${stat.color} rounded-xl flex items-center justify-center`}>
<stat.icon className="h-8 w-8 text-white" />
</div>
{/* Main Number */}
<div className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
<AnimatedCounter target={stat.value} suffix={stat.suffix} />
</div>
{/* Label */}
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{stat.label}
</h3>
{/* Description */}
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4 leading-relaxed">
{stat.description}
</p>
{/* Trend */}
<div className="flex items-center text-sm font-medium text-green-600 dark:text-green-400">
<TrendingUp className="h-4 w-4 mr-1" />
{stat.trend}
</div>
</motion.div>
))}
</div>
{/* Additional Impact Statement */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="mt-16 text-center bg-gradient-to-r from-primary-500 to-secondary-600 rounded-2xl p-8 text-white"
>
<h3 className="text-2xl font-bold mb-4">
Every Number Represents a Life Changed
</h3>
<p className="text-lg opacity-90 max-w-3xl mx-auto">
Behind every statistic is a student who can now focus on learning, a family with renewed hope,
and a community growing stronger together.
</p>
</motion.div>
</div>
</section>
)
}
export default ImpactSection
+110 -110
View File
@@ -1,111 +1,111 @@
import { motion } from 'framer-motion'
import { Backpack, Shirt, Users, Heart, School, Home } from 'lucide-react'
import { SectionHeader } from '../ui/SectionHeader'
export function ProgramsSection() {
const programs = [
{
icon: Backpack,
title: "School Supplies",
description: "Essential learning materials including notebooks, pens, calculators, and art supplies",
impact: "2,847 students equipped",
color: "from-blue-500 to-blue-600"
},
{
icon: Shirt,
title: "Clothing Support",
description: "Quality clothing, shoes, and seasonal items to help students feel confident",
impact: "1,203 wardrobes completed",
color: "from-green-500 to-green-600"
},
{
icon: Users,
title: "Emergency Assistance",
description: "Rapid response for urgent family needs including food, shelter, and utilities",
impact: "856 families supported",
color: "from-red-500 to-red-600"
},
{
icon: School,
title: "Educational Technology",
description: "Laptops, tablets, and internet access for remote learning success",
impact: "645 devices provided",
color: "from-purple-500 to-purple-600"
},
{
icon: Heart,
title: "Mentorship Programs",
description: "One-on-one support and guidance for academic and personal growth",
impact: "432 mentor relationships",
color: "from-pink-500 to-pink-600"
},
{
icon: Home,
title: "Family Support Services",
description: "Comprehensive assistance for housing, transportation, and childcare",
impact: "298 families stabilized",
color: "from-orange-500 to-orange-600"
}
]
return (
<section className="py-20 bg-white dark:bg-gray-900">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionHeader
eyebrow="Our Programs"
title="Comprehensive Student Support"
subtitle="We provide holistic assistance that addresses the full spectrum of student needs"
/>
<div className="mt-16 grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{programs.map((program, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="card group hover:shadow-xl transition-all duration-300"
>
<div className={`w-16 h-16 mb-6 bg-gradient-to-br ${program.color} rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300`}>
<program.icon className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-semibold mb-3 text-gray-900 dark:text-white">
{program.title}
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4 leading-relaxed">
{program.description}
</p>
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<p className="text-sm font-medium text-primary-600 dark:text-primary-400">
📊 {program.impact}
</p>
</div>
</motion.div>
))}
</div>
{/* Call to Action */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="mt-16 text-center"
>
<a
href="#/request-assistance"
className="btn-primary inline-flex items-center justify-center"
>
Request Program Support
<Backpack className="ml-2 h-5 w-5" />
</a>
</motion.div>
</div>
</section>
)
}
import { motion } from 'framer-motion'
import { Backpack, Shirt, Users, Heart, School, Home } from 'lucide-react'
import { SectionHeader } from '../ui/SectionHeader'
export function ProgramsSection() {
const programs = [
{
icon: Backpack,
title: "School Supplies",
description: "Essential learning materials including notebooks, pens, calculators, and art supplies",
impact: "2,847 students equipped",
color: "from-blue-500 to-blue-600"
},
{
icon: Shirt,
title: "Clothing Support",
description: "Quality clothing, shoes, and seasonal items to help students feel confident",
impact: "1,203 wardrobes completed",
color: "from-green-500 to-green-600"
},
{
icon: Users,
title: "Emergency Assistance",
description: "Rapid response for urgent family needs including food, shelter, and utilities",
impact: "856 families supported",
color: "from-red-500 to-red-600"
},
{
icon: School,
title: "Educational Technology",
description: "Laptops, tablets, and internet access for remote learning success",
impact: "645 devices provided",
color: "from-purple-500 to-purple-600"
},
{
icon: Heart,
title: "Mentorship Programs",
description: "One-on-one support and guidance for academic and personal growth",
impact: "432 mentor relationships",
color: "from-pink-500 to-pink-600"
},
{
icon: Home,
title: "Family Support Services",
description: "Comprehensive assistance for housing, transportation, and childcare",
impact: "298 families stabilized",
color: "from-orange-500 to-orange-600"
}
]
return (
<section className="py-20 bg-white dark:bg-gray-900">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionHeader
eyebrow="Our Programs"
title="Comprehensive Student Support"
subtitle="We provide holistic assistance that addresses the full spectrum of student needs"
/>
<div className="mt-16 grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{programs.map((program, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="card group hover:shadow-xl transition-all duration-300"
>
<div className={`w-16 h-16 mb-6 bg-gradient-to-br ${program.color} rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300`}>
<program.icon className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-semibold mb-3 text-gray-900 dark:text-white">
{program.title}
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4 leading-relaxed">
{program.description}
</p>
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<p className="text-sm font-medium text-primary-600 dark:text-primary-400">
📊 {program.impact}
</p>
</div>
</motion.div>
))}
</div>
{/* Call to Action */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="mt-16 text-center"
>
<a
href="#/request-assistance"
className="btn-primary inline-flex items-center justify-center"
>
Request Program Support
<Backpack className="ml-2 h-5 w-5" />
</a>
</motion.div>
</div>
</section>
)
}
export default ProgramsSection
@@ -1,64 +1,64 @@
import { describe, it, expect, vi } from 'vitest'
import { render } from '@testing-library/react'
import { screen } from '@testing-library/dom'
import '@testing-library/jest-dom'
import { HeroSection } from '../HeroSection'
// Mock framer-motion
vi.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
a: ({ children, ...props }: any) => <a {...props}>{children}</a>
},
useScroll: () => ({ scrollYProgress: { get: () => 0 } }),
useTransform: () => ({ get: () => 0 }),
}))
describe('HeroSection Component', () => {
it('renders hero section with main heading', () => {
render(<HeroSection />)
expect(screen.getByText('Miracles in')).toBeInTheDocument()
expect(screen.getByText('Motion')).toBeInTheDocument()
})
it('displays 501c3 organization badge', () => {
render(<HeroSection />)
expect(screen.getByText('501(c)3 Non-Profit Organization')).toBeInTheDocument()
})
it('renders main description text', () => {
render(<HeroSection />)
expect(screen.getByText(/Empowering students with essential supplies/)).toBeInTheDocument()
expect(screen.getByText(/Every child deserves the tools they need/)).toBeInTheDocument()
})
it('renders call-to-action buttons', () => {
render(<HeroSection />)
const donateButton = screen.getByRole('link', { name: /Donate Now/ })
const helpButton = screen.getByRole('link', { name: /Get Help/ })
expect(donateButton).toHaveAttribute('href', '#/donate')
expect(helpButton).toHaveAttribute('href', '#/request-assistance')
})
it('has proper semantic structure', () => {
render(<HeroSection />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toBeInTheDocument()
expect(heading).toHaveTextContent('Miracles in Motion')
})
it('includes accessibility features', () => {
render(<HeroSection />)
const buttons = screen.getAllByRole('link')
buttons.forEach((button: HTMLElement) => {
expect(button).toHaveClass(/btn-/)
})
})
import { describe, it, expect, vi } from 'vitest'
import { render } from '@testing-library/react'
import { screen } from '@testing-library/dom'
import '@testing-library/jest-dom'
import { HeroSection } from '../HeroSection'
// Mock framer-motion
vi.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
a: ({ children, ...props }: any) => <a {...props}>{children}</a>
},
useScroll: () => ({ scrollYProgress: { get: () => 0 } }),
useTransform: () => ({ get: () => 0 }),
}))
describe('HeroSection Component', () => {
it('renders hero section with main heading', () => {
render(<HeroSection />)
expect(screen.getByText('Miracles in')).toBeInTheDocument()
expect(screen.getByText('Motion')).toBeInTheDocument()
})
it('displays 501c3 organization badge', () => {
render(<HeroSection />)
expect(screen.getByText('501(c)3 Non-Profit Organization')).toBeInTheDocument()
})
it('renders main description text', () => {
render(<HeroSection />)
expect(screen.getByText(/Empowering students with essential supplies/)).toBeInTheDocument()
expect(screen.getByText(/Every child deserves the tools they need/)).toBeInTheDocument()
})
it('renders call-to-action buttons', () => {
render(<HeroSection />)
const donateButton = screen.getByRole('link', { name: /Donate Now/ })
const helpButton = screen.getByRole('link', { name: /Get Help/ })
expect(donateButton).toHaveAttribute('href', '#/donate')
expect(helpButton).toHaveAttribute('href', '#/request-assistance')
})
it('has proper semantic structure', () => {
render(<HeroSection />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toBeInTheDocument()
expect(heading).toHaveTextContent('Miracles in Motion')
})
it('includes accessibility features', () => {
render(<HeroSection />)
const buttons = screen.getAllByRole('link')
buttons.forEach((button: HTMLElement) => {
expect(button).toHaveClass(/btn-/)
})
})
})
+8 -8
View File
@@ -1,9 +1,9 @@
// Section Components Export
export { HeroSection } from './HeroSection'
export { ProgramsSection } from './ProgramsSection'
export { ImpactSection } from './ImpactSection'
// Re-export all
export * from './HeroSection'
export * from './ProgramsSection'
// Section Components Export
export { HeroSection } from './HeroSection'
export { ProgramsSection } from './ProgramsSection'
export { ImpactSection } from './ImpactSection'
// Re-export all
export * from './HeroSection'
export * from './ProgramsSection'
export * from './ImpactSection'
+22 -22
View File
@@ -1,23 +1,23 @@
import { LucideIcon } from 'lucide-react'
interface CardProps {
title: string
icon: LucideIcon
children: React.ReactNode
}
export 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>
)
}
import { LucideIcon } from 'lucide-react'
interface CardProps {
title: string
icon: LucideIcon
children: React.ReactNode
}
export 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>
)
}
export default Card
+225 -225
View File
@@ -1,226 +1,226 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { motion, AnimatePresence } from 'framer-motion'
import { Globe, ChevronDown } from 'lucide-react'
import { languages } from '@/i18n/config'
interface LanguageSwitcherProps {
className?: string
variant?: 'default' | 'minimal' | 'mobile'
}
export function LanguageSwitcher({
className = '',
variant = 'default'
}: LanguageSwitcherProps) {
const { i18n, t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const currentLang = i18n.language || 'en'
const currentLanguage = languages[currentLang as keyof typeof languages]
const handleLanguageChange = (langCode: string) => {
i18n.changeLanguage(langCode)
setIsOpen(false)
// Update document direction for RTL languages
const langConfig = languages[langCode as keyof typeof languages]
document.documentElement.dir = langConfig.dir
document.documentElement.lang = langCode
// Store preference
localStorage.setItem('i18nextLng', langCode)
}
const dropdownVariants = {
hidden: {
opacity: 0,
scale: 0.95,
y: -10
},
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: {
duration: 0.2,
ease: 'easeOut'
}
},
exit: {
opacity: 0,
scale: 0.95,
y: -10,
transition: {
duration: 0.15,
ease: 'easeIn'
}
}
}
if (variant === 'minimal') {
return (
<div className={`relative ${className}`}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1 p-2 rounded-lg hover:bg-gray-100 transition-colors"
aria-label={t('accessibility.toggleLanguage')}
>
<span className="text-lg">{currentLanguage?.flag || '🌐'}</span>
<span className="text-sm font-medium">{currentLang.toUpperCase()}</span>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
variants={dropdownVariants}
initial="hidden"
animate="visible"
exit="exit"
className="absolute top-full right-0 mt-1 bg-white rounded-lg shadow-lg border border-gray-200 py-1 min-w-[140px] z-50"
>
{Object.entries(languages).map(([code, lang]) => (
<button
key={code}
onClick={() => handleLanguageChange(code)}
className={`w-full px-3 py-2 text-left hover:bg-gray-50 transition-colors flex items-center gap-2 ${
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
}`}
>
<span className="text-base">{lang.flag}</span>
<span className="text-sm">{lang.name}</span>
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
)
}
if (variant === 'mobile') {
return (
<div className={`w-full ${className}`}>
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200 hover:border-purple-300 transition-colors"
>
<div className="flex items-center gap-3">
<Globe className="w-5 h-5 text-gray-500" />
<div className="text-left">
<div className="text-sm font-medium text-gray-900">
Language / Idioma
</div>
<div className="text-xs text-gray-500">
{currentLanguage?.flag} {currentLanguage?.name}
</div>
</div>
</div>
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${
isOpen ? 'rotate-180' : ''
}`} />
</button>
<AnimatePresence>
{isOpen && (
<motion.div
variants={dropdownVariants}
initial="hidden"
animate="visible"
exit="exit"
className="mt-2 bg-white rounded-lg shadow-lg border border-gray-200 overflow-hidden"
>
{Object.entries(languages).map(([code, lang]) => (
<button
key={code}
onClick={() => handleLanguageChange(code)}
className={`w-full px-4 py-3 text-left hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0 ${
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
}`}
>
<div className="flex items-center gap-3">
<span className="text-xl">{lang.flag}</span>
<div>
<div className="font-medium">{lang.name}</div>
<div className="text-sm text-gray-500">{code.toUpperCase()}</div>
</div>
{code === currentLang && (
<div className="ml-auto w-2 h-2 bg-purple-600 rounded-full" />
)}
</div>
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
)
}
// Default variant
return (
<div className={`relative ${className}`}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-2 bg-white rounded-lg border border-gray-200 hover:border-purple-300 transition-colors shadow-sm"
aria-label={t('accessibility.toggleLanguage')}
>
<Globe className="w-4 h-4 text-gray-500" />
<span className="text-lg">{currentLanguage?.flag || '🌐'}</span>
<span className="text-sm font-medium text-gray-700">
{currentLanguage?.name || 'English'}
</span>
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${
isOpen ? 'rotate-180' : ''
}`} />
</button>
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
/>
{/* Dropdown */}
<motion.div
variants={dropdownVariants}
initial="hidden"
animate="visible"
exit="exit"
className="absolute top-full right-0 mt-2 bg-white rounded-lg shadow-xl border border-gray-200 py-2 min-w-[200px] z-50"
>
<div className="px-3 py-2 border-b border-gray-100">
<div className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Select Language
</div>
</div>
{Object.entries(languages).map(([code, lang]) => (
<button
key={code}
onClick={() => handleLanguageChange(code)}
className={`w-full px-4 py-2 text-left hover:bg-gray-50 transition-colors flex items-center gap-3 ${
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
}`}
>
<span className="text-xl">{lang.flag}</span>
<div className="flex-1">
<div className="font-medium">{lang.name}</div>
<div className="text-xs text-gray-500">{code.toUpperCase()}</div>
</div>
{code === currentLang && (
<div className="w-2 h-2 bg-purple-600 rounded-full" />
)}
</button>
))}
</motion.div>
</>
)}
</AnimatePresence>
</div>
)
}
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { motion, AnimatePresence } from 'framer-motion'
import { Globe, ChevronDown } from 'lucide-react'
import { languages } from '@/i18n/config'
interface LanguageSwitcherProps {
className?: string
variant?: 'default' | 'minimal' | 'mobile'
}
export function LanguageSwitcher({
className = '',
variant = 'default'
}: LanguageSwitcherProps) {
const { i18n, t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const currentLang = i18n.language || 'en'
const currentLanguage = languages[currentLang as keyof typeof languages]
const handleLanguageChange = (langCode: string) => {
i18n.changeLanguage(langCode)
setIsOpen(false)
// Update document direction for RTL languages
const langConfig = languages[langCode as keyof typeof languages]
document.documentElement.dir = langConfig.dir
document.documentElement.lang = langCode
// Store preference
localStorage.setItem('i18nextLng', langCode)
}
const dropdownVariants = {
hidden: {
opacity: 0,
scale: 0.95,
y: -10
},
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: {
duration: 0.2,
ease: 'easeOut'
}
},
exit: {
opacity: 0,
scale: 0.95,
y: -10,
transition: {
duration: 0.15,
ease: 'easeIn'
}
}
}
if (variant === 'minimal') {
return (
<div className={`relative ${className}`}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1 p-2 rounded-lg hover:bg-gray-100 transition-colors"
aria-label={t('accessibility.toggleLanguage')}
>
<span className="text-lg">{currentLanguage?.flag || '🌐'}</span>
<span className="text-sm font-medium">{currentLang.toUpperCase()}</span>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
variants={dropdownVariants}
initial="hidden"
animate="visible"
exit="exit"
className="absolute top-full right-0 mt-1 bg-white rounded-lg shadow-lg border border-gray-200 py-1 min-w-[140px] z-50"
>
{Object.entries(languages).map(([code, lang]) => (
<button
key={code}
onClick={() => handleLanguageChange(code)}
className={`w-full px-3 py-2 text-left hover:bg-gray-50 transition-colors flex items-center gap-2 ${
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
}`}
>
<span className="text-base">{lang.flag}</span>
<span className="text-sm">{lang.name}</span>
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
)
}
if (variant === 'mobile') {
return (
<div className={`w-full ${className}`}>
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200 hover:border-purple-300 transition-colors"
>
<div className="flex items-center gap-3">
<Globe className="w-5 h-5 text-gray-500" />
<div className="text-left">
<div className="text-sm font-medium text-gray-900">
Language / Idioma
</div>
<div className="text-xs text-gray-500">
{currentLanguage?.flag} {currentLanguage?.name}
</div>
</div>
</div>
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${
isOpen ? 'rotate-180' : ''
}`} />
</button>
<AnimatePresence>
{isOpen && (
<motion.div
variants={dropdownVariants}
initial="hidden"
animate="visible"
exit="exit"
className="mt-2 bg-white rounded-lg shadow-lg border border-gray-200 overflow-hidden"
>
{Object.entries(languages).map(([code, lang]) => (
<button
key={code}
onClick={() => handleLanguageChange(code)}
className={`w-full px-4 py-3 text-left hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0 ${
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
}`}
>
<div className="flex items-center gap-3">
<span className="text-xl">{lang.flag}</span>
<div>
<div className="font-medium">{lang.name}</div>
<div className="text-sm text-gray-500">{code.toUpperCase()}</div>
</div>
{code === currentLang && (
<div className="ml-auto w-2 h-2 bg-purple-600 rounded-full" />
)}
</div>
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
)
}
// Default variant
return (
<div className={`relative ${className}`}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-2 bg-white rounded-lg border border-gray-200 hover:border-purple-300 transition-colors shadow-sm"
aria-label={t('accessibility.toggleLanguage')}
>
<Globe className="w-4 h-4 text-gray-500" />
<span className="text-lg">{currentLanguage?.flag || '🌐'}</span>
<span className="text-sm font-medium text-gray-700">
{currentLanguage?.name || 'English'}
</span>
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${
isOpen ? 'rotate-180' : ''
}`} />
</button>
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
/>
{/* Dropdown */}
<motion.div
variants={dropdownVariants}
initial="hidden"
animate="visible"
exit="exit"
className="absolute top-full right-0 mt-2 bg-white rounded-lg shadow-xl border border-gray-200 py-2 min-w-[200px] z-50"
>
<div className="px-3 py-2 border-b border-gray-100">
<div className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Select Language
</div>
</div>
{Object.entries(languages).map(([code, lang]) => (
<button
key={code}
onClick={() => handleLanguageChange(code)}
className={`w-full px-4 py-2 text-left hover:bg-gray-50 transition-colors flex items-center gap-3 ${
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
}`}
>
<span className="text-xl">{lang.flag}</span>
<div className="flex-1">
<div className="font-medium">{lang.name}</div>
<div className="text-xs text-gray-500">{code.toUpperCase()}</div>
</div>
{code === currentLang && (
<div className="w-2 h-2 bg-purple-600 rounded-full" />
)}
</button>
))}
</motion.div>
</>
)}
</AnimatePresence>
</div>
)
}
export default LanguageSwitcher
+135 -135
View File
@@ -1,136 +1,136 @@
import { useState, useRef, useEffect, ImgHTMLAttributes } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
interface LazyImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'loading'> {
src: string
alt: string
placeholder?: string
blurDataURL?: string
priority?: boolean
sizes?: string
quality?: number
onLoadComplete?: () => void
}
export function LazyImage({
src,
alt,
placeholder = '/placeholder.svg',
blurDataURL,
priority = false,
className = '',
onLoadComplete,
...props
}: LazyImageProps) {
const [isLoaded, setIsLoaded] = useState(false)
const [error, setError] = useState(false)
const imgRef = useRef<HTMLImageElement>(null)
const [imageSrc, setImageSrc] = useState(priority ? src : placeholder)
// Intersection Observer for lazy loading
useEffect(() => {
if (priority) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setImageSrc(src)
observer.disconnect()
}
},
{ rootMargin: '50px' }
)
const currentImg = imgRef.current
if (currentImg) {
observer.observe(currentImg)
}
return () => {
if (currentImg) observer.unobserve(currentImg)
}
}, [src, priority])
// Handle image load
const handleLoad = () => {
setIsLoaded(true)
onLoadComplete?.()
}
// Handle image error
const handleError = () => {
setError(true)
setImageSrc(placeholder)
}
// Generate optimized src with quality and format
const getOptimizedSrc = (originalSrc: string, quality = 85) => {
// Check if it's already optimized or external
if (originalSrc.includes('?') || originalSrc.startsWith('http')) {
return originalSrc
}
// Add quality parameter for supported formats
if (originalSrc.includes('.jpg') || originalSrc.includes('.jpeg')) {
return `${originalSrc}?quality=${quality}&format=webp`
}
return originalSrc
}
const optimizedSrc = getOptimizedSrc(imageSrc)
return (
<div className={`relative overflow-hidden ${className}`}>
<AnimatePresence>
{blurDataURL && !isLoaded && (
<motion.div
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="absolute inset-0 z-10"
style={{
backgroundImage: `url(${blurDataURL})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
filter: 'blur(10px)',
transform: 'scale(1.1)'
}}
/>
)}
</AnimatePresence>
<motion.img
ref={imgRef}
src={optimizedSrc}
alt={alt}
loading={priority ? 'eager' : 'lazy'}
onLoad={handleLoad}
onError={handleError}
initial={{ opacity: 0 }}
animate={{ opacity: isLoaded ? 1 : 0 }}
transition={{ duration: 0.3 }}
className={`w-full h-full object-cover ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
style={props.style}
/>
{/* Loading skeleton */}
{!isLoaded && !error && (
<div className="absolute inset-0 bg-gray-200 animate-pulse flex items-center justify-center">
<div className="w-8 h-8 bg-gray-300 rounded-full animate-pulse" />
</div>
)}
{/* Error state */}
{error && (
<div className="absolute inset-0 bg-gray-100 flex items-center justify-center text-gray-400">
<div className="text-center">
<div className="w-12 h-12 mx-auto mb-2 bg-gray-300 rounded" />
<p className="text-sm">Failed to load image</p>
</div>
</div>
)}
</div>
)
}
import { useState, useRef, useEffect, ImgHTMLAttributes } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
interface LazyImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'loading'> {
src: string
alt: string
placeholder?: string
blurDataURL?: string
priority?: boolean
sizes?: string
quality?: number
onLoadComplete?: () => void
}
export function LazyImage({
src,
alt,
placeholder = '/placeholder.svg',
blurDataURL,
priority = false,
className = '',
onLoadComplete,
...props
}: LazyImageProps) {
const [isLoaded, setIsLoaded] = useState(false)
const [error, setError] = useState(false)
const imgRef = useRef<HTMLImageElement>(null)
const [imageSrc, setImageSrc] = useState(priority ? src : placeholder)
// Intersection Observer for lazy loading
useEffect(() => {
if (priority) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setImageSrc(src)
observer.disconnect()
}
},
{ rootMargin: '50px' }
)
const currentImg = imgRef.current
if (currentImg) {
observer.observe(currentImg)
}
return () => {
if (currentImg) observer.unobserve(currentImg)
}
}, [src, priority])
// Handle image load
const handleLoad = () => {
setIsLoaded(true)
onLoadComplete?.()
}
// Handle image error
const handleError = () => {
setError(true)
setImageSrc(placeholder)
}
// Generate optimized src with quality and format
const getOptimizedSrc = (originalSrc: string, quality = 85) => {
// Check if it's already optimized or external
if (originalSrc.includes('?') || originalSrc.startsWith('http')) {
return originalSrc
}
// Add quality parameter for supported formats
if (originalSrc.includes('.jpg') || originalSrc.includes('.jpeg')) {
return `${originalSrc}?quality=${quality}&format=webp`
}
return originalSrc
}
const optimizedSrc = getOptimizedSrc(imageSrc)
return (
<div className={`relative overflow-hidden ${className}`}>
<AnimatePresence>
{blurDataURL && !isLoaded && (
<motion.div
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="absolute inset-0 z-10"
style={{
backgroundImage: `url(${blurDataURL})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
filter: 'blur(10px)',
transform: 'scale(1.1)'
}}
/>
)}
</AnimatePresence>
<motion.img
ref={imgRef}
src={optimizedSrc}
alt={alt}
loading={priority ? 'eager' : 'lazy'}
onLoad={handleLoad}
onError={handleError}
initial={{ opacity: 0 }}
animate={{ opacity: isLoaded ? 1 : 0 }}
transition={{ duration: 0.3 }}
className={`w-full h-full object-cover ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
style={props.style}
/>
{/* Loading skeleton */}
{!isLoaded && !error && (
<div className="absolute inset-0 bg-gray-200 animate-pulse flex items-center justify-center">
<div className="w-8 h-8 bg-gray-300 rounded-full animate-pulse" />
</div>
)}
{/* Error state */}
{error && (
<div className="absolute inset-0 bg-gray-100 flex items-center justify-center text-gray-400">
<div className="text-center">
<div className="w-12 h-12 mx-auto mb-2 bg-gray-300 rounded" />
<p className="text-sm">Failed to load image</p>
</div>
</div>
)}
</div>
)
}
export default LazyImage
+23 -23
View File
@@ -1,24 +1,24 @@
// LogoMark component
import { motion } from 'framer-motion'
import { Sparkles } from 'lucide-react'
export 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>
)
}
// LogoMark component
import { motion } from 'framer-motion'
import { Sparkles } from 'lucide-react'
export 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>
)
}
export default LogoMark
+14 -14
View File
@@ -1,15 +1,15 @@
import React from 'react'
interface MagneticProps {
children: React.ReactNode
}
export function Magnetic({ children }: MagneticProps) {
return (
<div className="relative">
{children}
</div>
)
}
import React from 'react'
interface MagneticProps {
children: React.ReactNode
}
export function Magnetic({ children }: MagneticProps) {
return (
<div className="relative">
{children}
</div>
)
}
export default Magnetic
+210 -210
View File
@@ -1,211 +1,211 @@
import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import { usePerformance, useBundlePerformance } from '@/hooks/usePerformance'
import { Activity, Zap, Globe, Image, Code } from 'lucide-react'
interface PerformanceMonitorProps {
showDetailed?: boolean
className?: string
}
export function PerformanceMonitor({
showDetailed = false,
className = ''
}: PerformanceMonitorProps) {
const { metrics, isLoading } = usePerformance()
const bundleMetrics = useBundlePerformance()
const [isVisible, setIsVisible] = useState(false)
// Toggle visibility in development mode
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.ctrlKey && e.shiftKey && e.key === 'P') {
setIsVisible(!isVisible)
}
}
if (process.env.NODE_ENV === 'development') {
window.addEventListener('keydown', handleKeyPress)
return () => window.removeEventListener('keydown', handleKeyPress)
}
}, [isVisible])
// Don't render in production unless explicitly requested
if (process.env.NODE_ENV === 'production' && !showDetailed) return null
if (!isVisible && process.env.NODE_ENV === 'development') return null
const getScoreColor = (value: number | null, thresholds: [number, number]) => {
if (value === null) return 'text-gray-400'
if (value <= thresholds[0]) return 'text-green-500'
if (value <= thresholds[1]) return 'text-yellow-500'
return 'text-red-500'
}
const formatMetric = (value: number | null, suffix = 'ms') => {
return value ? `${Math.round(value)}${suffix}` : 'N/A'
}
const webVitalsData = [
{
label: 'FCP',
description: 'First Contentful Paint',
value: metrics.FCP,
threshold: [1800, 3000] as [number, number],
icon: Activity,
good: '< 1.8s',
poor: '> 3.0s'
},
{
label: 'LCP',
description: 'Largest Contentful Paint',
value: metrics.LCP,
threshold: [2500, 4000] as [number, number],
icon: Globe,
good: '< 2.5s',
poor: '> 4.0s'
},
{
label: 'FID',
description: 'First Input Delay',
value: metrics.FID,
threshold: [100, 300] as [number, number],
icon: Zap,
good: '< 100ms',
poor: '> 300ms'
},
{
label: 'CLS',
description: 'Cumulative Layout Shift',
value: metrics.CLS,
threshold: [0.1, 0.25] as [number, number],
icon: Activity,
good: '< 0.1',
poor: '> 0.25',
suffix: ''
},
{
label: 'TTFB',
description: 'Time to First Byte',
value: metrics.TTFB,
threshold: [800, 1800] as [number, number],
icon: Globe,
good: '< 800ms',
poor: '> 1.8s'
}
]
return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className={`fixed top-4 right-4 z-50 bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg p-4 max-w-sm ${className}`}
>
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-sm text-gray-900 flex items-center gap-2">
<Activity className="w-4 h-4" />
Performance Monitor
</h3>
<button
onClick={() => setIsVisible(false)}
className="text-gray-400 hover:text-gray-600 text-sm"
>
×
</button>
</div>
{isLoading ? (
<div className="flex items-center gap-2 text-sm text-gray-500">
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
Measuring performance...
</div>
) : (
<div className="space-y-3">
{/* Web Vitals */}
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2">Core Web Vitals</h4>
<div className="grid grid-cols-3 gap-2">
{webVitalsData.slice(0, 3).map((metric) => {
const IconComponent = metric.icon
const colorClass = getScoreColor(metric.value, metric.threshold)
return (
<div key={metric.label} className="text-center">
<div className="flex items-center justify-center mb-1">
<IconComponent className={`w-3 h-3 ${colorClass}`} />
</div>
<div className={`text-xs font-mono ${colorClass}`}>
{formatMetric(metric.value, metric.suffix || 'ms')}
</div>
<div className="text-xs text-gray-500">{metric.label}</div>
</div>
)
})}
</div>
</div>
{/* Additional Metrics */}
{showDetailed && (
<>
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2">Additional Metrics</h4>
<div className="grid grid-cols-2 gap-2">
{webVitalsData.slice(3).map((metric) => {
const colorClass = getScoreColor(metric.value, metric.threshold)
return (
<div key={metric.label} className="text-center">
<div className={`text-xs font-mono ${colorClass}`}>
{formatMetric(metric.value, metric.suffix || 'ms')}
</div>
<div className="text-xs text-gray-500">{metric.label}</div>
</div>
)
})}
</div>
</div>
{/* Bundle Metrics */}
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2">Bundle Size</h4>
<div className="space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1">
<Code className="w-3 h-3 text-blue-500" />
JavaScript
</span>
<span className="font-mono">{bundleMetrics.jsSize}KB</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1">
<div className="w-3 h-3 bg-green-500 rounded-sm" />
CSS
</span>
<span className="font-mono">{bundleMetrics.cssSize}KB</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1">
<Image className="w-3 h-3 text-purple-500" />
Images
</span>
<span className="font-mono">{bundleMetrics.imageSize}KB</span>
</div>
<div className="border-t pt-1 flex items-center justify-between text-xs font-medium">
<span>Total</span>
<span className="font-mono">{bundleMetrics.totalSize}KB</span>
</div>
</div>
</div>
</>
)}
{/* Tips */}
<div className="text-xs text-gray-500 border-t pt-2">
Press <kbd className="px-1 py-0.5 bg-gray-100 rounded text-xs">Ctrl+Shift+P</kbd> to toggle
</div>
</div>
)}
</motion.div>
)
}
import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import { usePerformance, useBundlePerformance } from '@/hooks/usePerformance'
import { Activity, Zap, Globe, Image, Code } from 'lucide-react'
interface PerformanceMonitorProps {
showDetailed?: boolean
className?: string
}
export function PerformanceMonitor({
showDetailed = false,
className = ''
}: PerformanceMonitorProps) {
const { metrics, isLoading } = usePerformance()
const bundleMetrics = useBundlePerformance()
const [isVisible, setIsVisible] = useState(false)
// Toggle visibility in development mode
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.ctrlKey && e.shiftKey && e.key === 'P') {
setIsVisible(!isVisible)
}
}
if (process.env.NODE_ENV === 'development') {
window.addEventListener('keydown', handleKeyPress)
return () => window.removeEventListener('keydown', handleKeyPress)
}
}, [isVisible])
// Don't render in production unless explicitly requested
if (process.env.NODE_ENV === 'production' && !showDetailed) return null
if (!isVisible && process.env.NODE_ENV === 'development') return null
const getScoreColor = (value: number | null, thresholds: [number, number]) => {
if (value === null) return 'text-gray-400'
if (value <= thresholds[0]) return 'text-green-500'
if (value <= thresholds[1]) return 'text-yellow-500'
return 'text-red-500'
}
const formatMetric = (value: number | null, suffix = 'ms') => {
return value ? `${Math.round(value)}${suffix}` : 'N/A'
}
const webVitalsData = [
{
label: 'FCP',
description: 'First Contentful Paint',
value: metrics.FCP,
threshold: [1800, 3000] as [number, number],
icon: Activity,
good: '< 1.8s',
poor: '> 3.0s'
},
{
label: 'LCP',
description: 'Largest Contentful Paint',
value: metrics.LCP,
threshold: [2500, 4000] as [number, number],
icon: Globe,
good: '< 2.5s',
poor: '> 4.0s'
},
{
label: 'FID',
description: 'First Input Delay',
value: metrics.FID,
threshold: [100, 300] as [number, number],
icon: Zap,
good: '< 100ms',
poor: '> 300ms'
},
{
label: 'CLS',
description: 'Cumulative Layout Shift',
value: metrics.CLS,
threshold: [0.1, 0.25] as [number, number],
icon: Activity,
good: '< 0.1',
poor: '> 0.25',
suffix: ''
},
{
label: 'TTFB',
description: 'Time to First Byte',
value: metrics.TTFB,
threshold: [800, 1800] as [number, number],
icon: Globe,
good: '< 800ms',
poor: '> 1.8s'
}
]
return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className={`fixed top-4 right-4 z-50 bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg p-4 max-w-sm ${className}`}
>
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-sm text-gray-900 flex items-center gap-2">
<Activity className="w-4 h-4" />
Performance Monitor
</h3>
<button
onClick={() => setIsVisible(false)}
className="text-gray-400 hover:text-gray-600 text-sm"
>
×
</button>
</div>
{isLoading ? (
<div className="flex items-center gap-2 text-sm text-gray-500">
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
Measuring performance...
</div>
) : (
<div className="space-y-3">
{/* Web Vitals */}
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2">Core Web Vitals</h4>
<div className="grid grid-cols-3 gap-2">
{webVitalsData.slice(0, 3).map((metric) => {
const IconComponent = metric.icon
const colorClass = getScoreColor(metric.value, metric.threshold)
return (
<div key={metric.label} className="text-center">
<div className="flex items-center justify-center mb-1">
<IconComponent className={`w-3 h-3 ${colorClass}`} />
</div>
<div className={`text-xs font-mono ${colorClass}`}>
{formatMetric(metric.value, metric.suffix || 'ms')}
</div>
<div className="text-xs text-gray-500">{metric.label}</div>
</div>
)
})}
</div>
</div>
{/* Additional Metrics */}
{showDetailed && (
<>
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2">Additional Metrics</h4>
<div className="grid grid-cols-2 gap-2">
{webVitalsData.slice(3).map((metric) => {
const colorClass = getScoreColor(metric.value, metric.threshold)
return (
<div key={metric.label} className="text-center">
<div className={`text-xs font-mono ${colorClass}`}>
{formatMetric(metric.value, metric.suffix || 'ms')}
</div>
<div className="text-xs text-gray-500">{metric.label}</div>
</div>
)
})}
</div>
</div>
{/* Bundle Metrics */}
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2">Bundle Size</h4>
<div className="space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1">
<Code className="w-3 h-3 text-blue-500" />
JavaScript
</span>
<span className="font-mono">{bundleMetrics.jsSize}KB</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1">
<div className="w-3 h-3 bg-green-500 rounded-sm" />
CSS
</span>
<span className="font-mono">{bundleMetrics.cssSize}KB</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1">
<Image className="w-3 h-3 text-purple-500" />
Images
</span>
<span className="font-mono">{bundleMetrics.imageSize}KB</span>
</div>
<div className="border-t pt-1 flex items-center justify-between text-xs font-medium">
<span>Total</span>
<span className="font-mono">{bundleMetrics.totalSize}KB</span>
</div>
</div>
</div>
</>
)}
{/* Tips */}
<div className="text-xs text-gray-500 border-t pt-2">
Press <kbd className="px-1 py-0.5 bg-gray-100 rounded text-xs">Ctrl+Shift+P</kbd> to toggle
</div>
</div>
)}
</motion.div>
)
}
export default PerformanceMonitor
+16 -16
View File
@@ -1,17 +1,17 @@
interface SectionHeaderProps {
eyebrow?: string
title: string
subtitle?: string
}
export 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>
)
}
interface SectionHeaderProps {
eyebrow?: string
title: string
subtitle?: string
}
export 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>
)
}
export default SectionHeader
+10 -10
View File
@@ -1,11 +1,11 @@
// UI Component Exports
export { LogoMark } from './LogoMark'
export { Magnetic } from './Magnetic'
export { SectionHeader } from './SectionHeader'
export { Card } from './Card'
// Re-export everything
export * from './LogoMark'
export * from './Magnetic'
export * from './SectionHeader'
// UI Component Exports
export { LogoMark } from './LogoMark'
export { Magnetic } from './Magnetic'
export { SectionHeader } from './SectionHeader'
export { Card } from './Card'
// Re-export everything
export * from './LogoMark'
export * from './Magnetic'
export * from './SectionHeader'
export * from './Card'
+102 -102
View File
@@ -1,103 +1,103 @@
import React, { createContext, useContext, useState, ReactNode } from 'react'
// Types
export interface AuthUser {
id: string
email: string
role: 'admin' | 'volunteer' | 'resource'
name: string
lastLogin: Date
permissions: string[]
}
export interface AuthContextType {
user: AuthUser | null
login: (email: string, password: string) => Promise<boolean>
logout: () => void
isLoading: boolean
}
// Create Context
const AuthContext = createContext<AuthContextType | null>(null)
// Auth Provider Component
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<AuthUser | null>(null)
const [isLoading, setIsLoading] = useState(false)
const login = async (email: string, password: string): Promise<boolean> => {
setIsLoading(true)
try {
// Mock authentication - replace with real API call
console.log('🔐 Attempting login for:', email)
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
// Mock user data based on email
const mockUsers: Record<string, AuthUser> = {
'[email protected]': {
id: '1',
email: '[email protected]',
role: 'admin',
name: 'Admin User',
lastLogin: new Date(),
permissions: ['all']
},
'[email protected]': {
id: '2',
email: '[email protected]',
role: 'volunteer',
name: 'Volunteer User',
lastLogin: new Date(),
permissions: ['view_requests', 'update_assignments']
}
}
const authenticatedUser = mockUsers[email]
if (authenticatedUser && password === 'demo123') {
setUser(authenticatedUser)
localStorage.setItem('authToken', `token-${authenticatedUser.id}`)
console.log('✅ Login successful')
return true
} else {
console.log('❌ Login failed')
return false
}
} catch (error) {
console.error('Login error:', error)
return false
} finally {
setIsLoading(false)
}
}
const logout = (): void => {
setUser(null)
localStorage.removeItem('authToken')
console.log('👋 User logged out')
}
const value: AuthContextType = {
user,
login,
logout,
isLoading
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
// Custom hook for using auth context
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
import React, { createContext, useContext, useState, ReactNode } from 'react'
// Types
export interface AuthUser {
id: string
email: string
role: 'admin' | 'volunteer' | 'resource'
name: string
lastLogin: Date
permissions: string[]
}
export interface AuthContextType {
user: AuthUser | null
login: (email: string, password: string) => Promise<boolean>
logout: () => void
isLoading: boolean
}
// Create Context
const AuthContext = createContext<AuthContextType | null>(null)
// Auth Provider Component
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<AuthUser | null>(null)
const [isLoading, setIsLoading] = useState(false)
const login = async (email: string, password: string): Promise<boolean> => {
setIsLoading(true)
try {
// Mock authentication - replace with real API call
console.log('🔐 Attempting login for:', email)
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
// Mock user data based on email
const mockUsers: Record<string, AuthUser> = {
'[email protected]': {
id: '1',
email: '[email protected]',
role: 'admin',
name: 'Admin User',
lastLogin: new Date(),
permissions: ['all']
},
'[email protected]': {
id: '2',
email: '[email protected]',
role: 'volunteer',
name: 'Volunteer User',
lastLogin: new Date(),
permissions: ['view_requests', 'update_assignments']
}
}
const authenticatedUser = mockUsers[email]
if (authenticatedUser && password === 'demo123') {
setUser(authenticatedUser)
localStorage.setItem('authToken', `token-${authenticatedUser.id}`)
console.log('✅ Login successful')
return true
} else {
console.log('❌ Login failed')
return false
}
} catch (error) {
console.error('Login error:', error)
return false
} finally {
setIsLoading(false)
}
}
const logout = (): void => {
setUser(null)
localStorage.removeItem('authToken')
console.log('👋 User logged out')
}
const value: AuthContextType = {
user,
login,
logout,
isLoading
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
// Custom hook for using auth context
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
+84 -84
View File
@@ -1,85 +1,85 @@
import React, { createContext, useContext, useState, ReactNode } from 'react'
// Types
export interface Notification {
id: string
type: 'success' | 'info' | 'warning' | 'error'
title: string
message: string
timestamp: Date
read: boolean
actions?: { label: string; action: () => void }[]
}
export interface NotificationContextType {
notifications: Notification[]
addNotification: (notif: Omit<Notification, 'id' | 'timestamp' | 'read'>) => void
markAsRead: (id: string) => void
removeNotification: (id: string) => void
clearAll: () => void
}
// Create Context
const NotificationContext = createContext<NotificationContextType | null>(null)
// Notification Provider Component
export const NotificationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [notifications, setNotifications] = useState<Notification[]>([])
const addNotification = (notif: Omit<Notification, 'id' | 'timestamp' | 'read'>): void => {
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): void => {
setNotifications(prev =>
prev.map(notif =>
notif.id === id ? { ...notif, read: true } : notif
)
)
}
const removeNotification = (id: string): void => {
setNotifications(prev => prev.filter(notif => notif.id !== id))
}
const clearAll = (): void => {
setNotifications([])
}
const value: NotificationContextType = {
notifications,
addNotification,
markAsRead,
removeNotification,
clearAll
}
return (
<NotificationContext.Provider value={value}>
{children}
</NotificationContext.Provider>
)
}
// Custom hook for using notification context
export const useNotifications = (): NotificationContextType => {
const context = useContext(NotificationContext)
if (!context) {
throw new Error('useNotifications must be used within a NotificationProvider')
}
return context
import React, { createContext, useContext, useState, ReactNode } from 'react'
// Types
export interface Notification {
id: string
type: 'success' | 'info' | 'warning' | 'error'
title: string
message: string
timestamp: Date
read: boolean
actions?: { label: string; action: () => void }[]
}
export interface NotificationContextType {
notifications: Notification[]
addNotification: (notif: Omit<Notification, 'id' | 'timestamp' | 'read'>) => void
markAsRead: (id: string) => void
removeNotification: (id: string) => void
clearAll: () => void
}
// Create Context
const NotificationContext = createContext<NotificationContextType | null>(null)
// Notification Provider Component
export const NotificationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [notifications, setNotifications] = useState<Notification[]>([])
const addNotification = (notif: Omit<Notification, 'id' | 'timestamp' | 'read'>): void => {
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): void => {
setNotifications(prev =>
prev.map(notif =>
notif.id === id ? { ...notif, read: true } : notif
)
)
}
const removeNotification = (id: string): void => {
setNotifications(prev => prev.filter(notif => notif.id !== id))
}
const clearAll = (): void => {
setNotifications([])
}
const value: NotificationContextType = {
notifications,
addNotification,
markAsRead,
removeNotification,
clearAll
}
return (
<NotificationContext.Provider value={value}>
{children}
</NotificationContext.Provider>
)
}
// Custom hook for using notification context
export const useNotifications = (): NotificationContextType => {
const context = useContext(NotificationContext)
if (!context) {
throw new Error('useNotifications must be used within a NotificationProvider')
}
return context
}
+314 -314
View File
@@ -1,315 +1,315 @@
// Phase 3B: Browser-Compatible CRM Integration (Mock Implementation)
import type { StudentRequest, MatchResult } from '../ai/types'
// Note: In a real implementation, this would connect to Salesforce via REST API
// For browser demo, we use mock implementations
export interface SalesforceConfig {
instanceUrl: string
clientId: string
clientSecret: string
username: string
password: string
securityToken: string
apiVersion: string
}
export interface SalesforceContact {
Id: string
Name: string
Email: string
Phone: string
Account: {
Id: string
Name: string
}
}
export interface SalesforceCase {
Id: string
Subject: string
Description: string
Status: 'New' | 'In Progress' | 'Closed' | 'Escalated'
Priority: 'Low' | 'Medium' | 'High' | 'Critical'
ContactId: string
CaseNumber: string
CreatedDate: string
LastModifiedDate: string
}
export interface NPSPAllocation {
Id: string
Amount: number
GAU__c: string // General Accounting Unit
Opportunity__c: string
Percent: number
}
class SalesforceConnector {
private config: SalesforceConfig
private accessToken: string | null = null
private instanceUrl: string = ''
constructor(config: SalesforceConfig) {
this.config = config
this.instanceUrl = config.instanceUrl
}
// Authentication with Salesforce
async authenticate(): Promise<boolean> {
try {
const response = await fetch(`${this.config.instanceUrl}/services/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'password',
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
username: this.config.username,
password: this.config.password + this.config.securityToken
})
})
if (!response.ok) {
throw new Error(`Authentication failed: ${response.statusText}`)
}
const data = await response.json()
this.accessToken = data.access_token
this.instanceUrl = data.instance_url
return true
} catch (error) {
console.error('Salesforce authentication error:', error)
return false
}
}
// Create assistance request case in Salesforce
async createAssistanceCase(request: StudentRequest): Promise<string | null> {
if (!this.accessToken) {
await this.authenticate()
}
try {
const caseData = {
Subject: `Student Assistance Request - ${request.category}`,
Description: this.formatRequestDescription(request),
Status: 'New',
Priority: this.determinePriority(request),
Origin: 'AI Portal',
Type: 'Student Assistance',
// Custom fields for nonprofit
Student_Name__c: request.studentName,
Student_ID__c: request.studentId,
Need_Category__c: request.category,
Urgency_Level__c: request.urgency,
Location_City__c: request.location.city,
Location_State__c: request.location.state,
Location_Zip__c: request.location.zipCode
}
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Case`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(caseData)
})
if (!response.ok) {
throw new Error(`Failed to create case: ${response.statusText}`)
}
const result = await response.json()
return result.id
} catch (error) {
console.error('Error creating Salesforce case:', error)
return null
}
}
// Update case with AI matching results
async updateCaseWithMatching(caseId: string, matchResult: MatchResult): Promise<boolean> {
if (!this.accessToken) {
await this.authenticate()
}
try {
const updateData = {
AI_Match_Confidence__c: matchResult.confidenceScore,
Recommended_Resource_Id__c: matchResult.resourceId,
Recommended_Resource_Name__c: matchResult.resourceName,
Resource_Type__c: matchResult.resourceType,
Estimated_Impact__c: matchResult.estimatedImpact,
Estimated_Cost__c: matchResult.estimatedCost,
Fulfillment_Timeline__c: matchResult.fulfillmentTimeline,
Last_AI_Update__c: new Date().toISOString()
}
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Case/${caseId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updateData)
})
return response.ok
} catch (error) {
console.error('Error updating Salesforce case:', error)
return false
}
}
// Get nonprofit contacts (volunteers, donors, partners)
async getContacts(recordType?: string): Promise<SalesforceContact[]> {
if (!this.accessToken) {
await this.authenticate()
}
try {
let query = `SELECT Id, Name, Email, Phone, Account.Id, Account.Name FROM Contact`
if (recordType) {
query += ` WHERE RecordType.Name = '${recordType}'`
}
const response = await fetch(
`${this.instanceUrl}/services/data/v${this.config.apiVersion}/query?q=${encodeURIComponent(query)}`,
{
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
}
)
if (!response.ok) {
throw new Error(`Failed to fetch contacts: ${response.statusText}`)
}
const data = await response.json()
return data.records
} catch (error) {
console.error('Error fetching Salesforce contacts:', error)
return []
}
}
// Create NPSP allocation for resource tracking
async createResourceAllocation(opportunityId: string, amount: number, gauId: string): Promise<string | null> {
if (!this.accessToken) {
await this.authenticate()
}
try {
const allocationData = {
Amount__c: amount,
GAU__c: gauId,
Opportunity__c: opportunityId,
Percent__c: 100
}
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Allocation__c`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(allocationData)
})
if (!response.ok) {
throw new Error(`Failed to create allocation: ${response.statusText}`)
}
const result = await response.json()
return result.id
} catch (error) {
console.error('Error creating resource allocation:', error)
return null
}
}
// Get donation opportunities for matching
async getDonationOpportunities(category?: string): Promise<any[]> {
if (!this.accessToken) {
await this.authenticate()
}
try {
let query = `SELECT Id, Name, Amount, StageName, CloseDate, Account.Name
FROM Opportunity
WHERE StageName IN ('Pledged', 'Posted')
AND CloseDate >= TODAY`
if (category) {
query += ` AND Category__c = '${category}'`
}
const response = await fetch(
`${this.instanceUrl}/services/data/v${this.config.apiVersion}/query?q=${encodeURIComponent(query)}`,
{
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
}
)
if (!response.ok) {
throw new Error(`Failed to fetch opportunities: ${response.statusText}`)
}
const data = await response.json()
return data.records
} catch (error) {
console.error('Error fetching donation opportunities:', error)
return []
}
}
private formatRequestDescription(request: StudentRequest): string {
return `
AI-Generated Student Assistance Request
Student Information:
- Name: ${request.studentName}
- ID: ${request.studentId}
- Location: ${request.location.city}, ${request.location.state} ${request.location.zipCode}
Request Details:
- Category: ${request.category}
- Urgency: ${request.urgency}
- Description: ${request.description}
Additional Information:
- Estimated Cost: $${request.estimatedCost || 0}
- Required Skills: ${request.requiredSkills?.join(', ') || 'None specified'}
- Deadline: ${request.deadline ? new Date(request.deadline).toLocaleDateString() : 'Not specified'}
Submission Details:
- Submitted: ${new Date(request.submittedAt).toLocaleString()}
- Request ID: ${request.id}
`.trim()
}
private determinePriority(request: StudentRequest): 'Low' | 'Medium' | 'High' | 'Critical' {
switch (request.urgency) {
case 'emergency':
return 'Critical'
case 'high':
return 'High'
case 'medium':
return 'Medium'
case 'low':
default:
return 'Low'
}
}
}
// Phase 3B: Browser-Compatible CRM Integration (Mock Implementation)
import type { StudentRequest, MatchResult } from '../ai/types'
// Note: In a real implementation, this would connect to Salesforce via REST API
// For browser demo, we use mock implementations
export interface SalesforceConfig {
instanceUrl: string
clientId: string
clientSecret: string
username: string
password: string
securityToken: string
apiVersion: string
}
export interface SalesforceContact {
Id: string
Name: string
Email: string
Phone: string
Account: {
Id: string
Name: string
}
}
export interface SalesforceCase {
Id: string
Subject: string
Description: string
Status: 'New' | 'In Progress' | 'Closed' | 'Escalated'
Priority: 'Low' | 'Medium' | 'High' | 'Critical'
ContactId: string
CaseNumber: string
CreatedDate: string
LastModifiedDate: string
}
export interface NPSPAllocation {
Id: string
Amount: number
GAU__c: string // General Accounting Unit
Opportunity__c: string
Percent: number
}
class SalesforceConnector {
private config: SalesforceConfig
private accessToken: string | null = null
private instanceUrl: string = ''
constructor(config: SalesforceConfig) {
this.config = config
this.instanceUrl = config.instanceUrl
}
// Authentication with Salesforce
async authenticate(): Promise<boolean> {
try {
const response = await fetch(`${this.config.instanceUrl}/services/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'password',
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
username: this.config.username,
password: this.config.password + this.config.securityToken
})
})
if (!response.ok) {
throw new Error(`Authentication failed: ${response.statusText}`)
}
const data = await response.json()
this.accessToken = data.access_token
this.instanceUrl = data.instance_url
return true
} catch (error) {
console.error('Salesforce authentication error:', error)
return false
}
}
// Create assistance request case in Salesforce
async createAssistanceCase(request: StudentRequest): Promise<string | null> {
if (!this.accessToken) {
await this.authenticate()
}
try {
const caseData = {
Subject: `Student Assistance Request - ${request.category}`,
Description: this.formatRequestDescription(request),
Status: 'New',
Priority: this.determinePriority(request),
Origin: 'AI Portal',
Type: 'Student Assistance',
// Custom fields for nonprofit
Student_Name__c: request.studentName,
Student_ID__c: request.studentId,
Need_Category__c: request.category,
Urgency_Level__c: request.urgency,
Location_City__c: request.location.city,
Location_State__c: request.location.state,
Location_Zip__c: request.location.zipCode
}
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Case`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(caseData)
})
if (!response.ok) {
throw new Error(`Failed to create case: ${response.statusText}`)
}
const result = await response.json()
return result.id
} catch (error) {
console.error('Error creating Salesforce case:', error)
return null
}
}
// Update case with AI matching results
async updateCaseWithMatching(caseId: string, matchResult: MatchResult): Promise<boolean> {
if (!this.accessToken) {
await this.authenticate()
}
try {
const updateData = {
AI_Match_Confidence__c: matchResult.confidenceScore,
Recommended_Resource_Id__c: matchResult.resourceId,
Recommended_Resource_Name__c: matchResult.resourceName,
Resource_Type__c: matchResult.resourceType,
Estimated_Impact__c: matchResult.estimatedImpact,
Estimated_Cost__c: matchResult.estimatedCost,
Fulfillment_Timeline__c: matchResult.fulfillmentTimeline,
Last_AI_Update__c: new Date().toISOString()
}
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Case/${caseId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updateData)
})
return response.ok
} catch (error) {
console.error('Error updating Salesforce case:', error)
return false
}
}
// Get nonprofit contacts (volunteers, donors, partners)
async getContacts(recordType?: string): Promise<SalesforceContact[]> {
if (!this.accessToken) {
await this.authenticate()
}
try {
let query = `SELECT Id, Name, Email, Phone, Account.Id, Account.Name FROM Contact`
if (recordType) {
query += ` WHERE RecordType.Name = '${recordType}'`
}
const response = await fetch(
`${this.instanceUrl}/services/data/v${this.config.apiVersion}/query?q=${encodeURIComponent(query)}`,
{
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
}
)
if (!response.ok) {
throw new Error(`Failed to fetch contacts: ${response.statusText}`)
}
const data = await response.json()
return data.records
} catch (error) {
console.error('Error fetching Salesforce contacts:', error)
return []
}
}
// Create NPSP allocation for resource tracking
async createResourceAllocation(opportunityId: string, amount: number, gauId: string): Promise<string | null> {
if (!this.accessToken) {
await this.authenticate()
}
try {
const allocationData = {
Amount__c: amount,
GAU__c: gauId,
Opportunity__c: opportunityId,
Percent__c: 100
}
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Allocation__c`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(allocationData)
})
if (!response.ok) {
throw new Error(`Failed to create allocation: ${response.statusText}`)
}
const result = await response.json()
return result.id
} catch (error) {
console.error('Error creating resource allocation:', error)
return null
}
}
// Get donation opportunities for matching
async getDonationOpportunities(category?: string): Promise<any[]> {
if (!this.accessToken) {
await this.authenticate()
}
try {
let query = `SELECT Id, Name, Amount, StageName, CloseDate, Account.Name
FROM Opportunity
WHERE StageName IN ('Pledged', 'Posted')
AND CloseDate >= TODAY`
if (category) {
query += ` AND Category__c = '${category}'`
}
const response = await fetch(
`${this.instanceUrl}/services/data/v${this.config.apiVersion}/query?q=${encodeURIComponent(query)}`,
{
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
}
)
if (!response.ok) {
throw new Error(`Failed to fetch opportunities: ${response.statusText}`)
}
const data = await response.json()
return data.records
} catch (error) {
console.error('Error fetching donation opportunities:', error)
return []
}
}
private formatRequestDescription(request: StudentRequest): string {
return `
AI-Generated Student Assistance Request
Student Information:
- Name: ${request.studentName}
- ID: ${request.studentId}
- Location: ${request.location.city}, ${request.location.state} ${request.location.zipCode}
Request Details:
- Category: ${request.category}
- Urgency: ${request.urgency}
- Description: ${request.description}
Additional Information:
- Estimated Cost: $${request.estimatedCost || 0}
- Required Skills: ${request.requiredSkills?.join(', ') || 'None specified'}
- Deadline: ${request.deadline ? new Date(request.deadline).toLocaleDateString() : 'Not specified'}
Submission Details:
- Submitted: ${new Date(request.submittedAt).toLocaleString()}
- Request ID: ${request.id}
`.trim()
}
private determinePriority(request: StudentRequest): 'Low' | 'Medium' | 'High' | 'Critical' {
switch (request.urgency) {
case 'emergency':
return 'Critical'
case 'high':
return 'High'
case 'medium':
return 'Medium'
case 'low':
default:
return 'Low'
}
}
}
export { SalesforceConnector }
+83 -83
View File
@@ -1,84 +1,84 @@
import { useState, useEffect } from 'react'
// Hash-based routing hook
export const useHashRoute = () => {
const parseRoute = (): string => window.location.hash?.slice(1) || "/"
const [route, setRoute] = useState<string>(parseRoute())
useEffect(() => {
const handleHashChange = (): void => setRoute(parseRoute())
window.addEventListener("hashchange", handleHashChange)
return () => window.removeEventListener("hashchange", handleHashChange)
}, [])
const navigate = (newRoute: string): void => {
window.location.hash = newRoute
}
return { route, navigate }
}
// Local storage hook with TypeScript support
export const useLocalStorage = <T>(key: string, initialValue: T) => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
const setValue = (value: T | ((val: T) => T)): void => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error)
}
}
return [storedValue, setValue] as const
}
// Media query hook
export const useMediaQuery = (query: string): boolean => {
const [matches, setMatches] = useState<boolean>(false)
useEffect(() => {
const media = window.matchMedia(query)
if (media.matches !== matches) {
setMatches(media.matches)
}
const listener = (event: MediaQueryListEvent): void => {
setMatches(event.matches)
}
media.addEventListener('change', listener)
return () => media.removeEventListener('change', listener)
}, [matches, query])
return matches
}
// Debounce hook
export const useDebounce = <T>(value: T, delay: number): T => {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
import { useState, useEffect } from 'react'
// Hash-based routing hook
export const useHashRoute = () => {
const parseRoute = (): string => window.location.hash?.slice(1) || "/"
const [route, setRoute] = useState<string>(parseRoute())
useEffect(() => {
const handleHashChange = (): void => setRoute(parseRoute())
window.addEventListener("hashchange", handleHashChange)
return () => window.removeEventListener("hashchange", handleHashChange)
}, [])
const navigate = (newRoute: string): void => {
window.location.hash = newRoute
}
return { route, navigate }
}
// Local storage hook with TypeScript support
export const useLocalStorage = <T>(key: string, initialValue: T) => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
const setValue = (value: T | ((val: T) => T)): void => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error)
}
}
return [storedValue, setValue] as const
}
// Media query hook
export const useMediaQuery = (query: string): boolean => {
const [matches, setMatches] = useState<boolean>(false)
useEffect(() => {
const media = window.matchMedia(query)
if (media.matches !== matches) {
setMatches(media.matches)
}
const listener = (event: MediaQueryListEvent): void => {
setMatches(event.matches)
}
media.addEventListener('change', listener)
return () => media.removeEventListener('change', listener)
}, [matches, query])
return matches
}
// Debounce hook
export const useDebounce = <T>(value: T, delay: number): T => {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
+107 -107
View File
@@ -1,108 +1,108 @@
import { useState, useEffect } from 'react'
// Types
export interface ImpactCalculation {
students: number
families: number
backpacks: number
clothing: number
emergency: number
annual: {
students: number
families: number
totalImpact: string
}
}
// Custom hook for donation impact calculations
export const useDonationImpact = (amount: number): ImpactCalculation => {
const [impact, setImpact] = useState<ImpactCalculation>({
students: 0,
families: 0,
backpacks: 0,
clothing: 0,
emergency: 0,
annual: {
students: 0,
families: 0,
totalImpact: '0 students supported annually'
}
})
useEffect(() => {
const calculateImpact = (donationAmount: number): ImpactCalculation => {
const students = Math.floor(donationAmount / 25) // $25 per student for basic supplies
const families = Math.floor(donationAmount / 50) // $50 per family for comprehensive support
const backpacks = Math.floor(donationAmount / 30) // $30 for complete backpack kit
const clothing = Math.floor(donationAmount / 45) // $45 for clothing items
const emergency = Math.floor(donationAmount / 75) // $75 for emergency assistance
return {
students,
families,
backpacks,
clothing,
emergency,
annual: {
students: Math.floor((donationAmount * 12) / 25),
families: Math.floor((donationAmount * 12) / 50),
totalImpact: `${Math.floor((donationAmount * 12) / 25)} students supported annually`
}
}
}
setImpact(calculateImpact(amount))
}, [amount])
return impact
}
// Hook for form validation
export const useFormValidation = <T extends Record<string, any>>(
initialValues: T,
validationRules: Record<keyof T, (value: any) => string | null>
) => {
const [values, setValues] = useState<T>(initialValues)
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({})
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({})
const validate = (fieldName?: keyof T): boolean => {
const fieldsToValidate = fieldName ? [fieldName] : Object.keys(validationRules) as (keyof T)[]
const newErrors: Partial<Record<keyof T, string>> = { ...errors }
let isValid = true
fieldsToValidate.forEach((field) => {
const error = validationRules[field](values[field])
if (error) {
newErrors[field] = error
isValid = false
} else {
delete newErrors[field]
}
})
setErrors(newErrors)
return isValid
}
const setValue = (fieldName: keyof T, value: any): void => {
setValues(prev => ({ ...prev, [fieldName]: value }))
setTouched(prev => ({ ...prev, [fieldName]: true }))
}
const resetForm = (): void => {
setValues(initialValues)
setErrors({})
setTouched({})
}
return {
values,
errors,
touched,
setValue,
validate,
resetForm,
isValid: Object.keys(errors).length === 0
}
import { useState, useEffect } from 'react'
// Types
export interface ImpactCalculation {
students: number
families: number
backpacks: number
clothing: number
emergency: number
annual: {
students: number
families: number
totalImpact: string
}
}
// Custom hook for donation impact calculations
export const useDonationImpact = (amount: number): ImpactCalculation => {
const [impact, setImpact] = useState<ImpactCalculation>({
students: 0,
families: 0,
backpacks: 0,
clothing: 0,
emergency: 0,
annual: {
students: 0,
families: 0,
totalImpact: '0 students supported annually'
}
})
useEffect(() => {
const calculateImpact = (donationAmount: number): ImpactCalculation => {
const students = Math.floor(donationAmount / 25) // $25 per student for basic supplies
const families = Math.floor(donationAmount / 50) // $50 per family for comprehensive support
const backpacks = Math.floor(donationAmount / 30) // $30 for complete backpack kit
const clothing = Math.floor(donationAmount / 45) // $45 for clothing items
const emergency = Math.floor(donationAmount / 75) // $75 for emergency assistance
return {
students,
families,
backpacks,
clothing,
emergency,
annual: {
students: Math.floor((donationAmount * 12) / 25),
families: Math.floor((donationAmount * 12) / 50),
totalImpact: `${Math.floor((donationAmount * 12) / 25)} students supported annually`
}
}
}
setImpact(calculateImpact(amount))
}, [amount])
return impact
}
// Hook for form validation
export const useFormValidation = <T extends Record<string, any>>(
initialValues: T,
validationRules: Record<keyof T, (value: any) => string | null>
) => {
const [values, setValues] = useState<T>(initialValues)
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({})
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({})
const validate = (fieldName?: keyof T): boolean => {
const fieldsToValidate = fieldName ? [fieldName] : Object.keys(validationRules) as (keyof T)[]
const newErrors: Partial<Record<keyof T, string>> = { ...errors }
let isValid = true
fieldsToValidate.forEach((field) => {
const error = validationRules[field](values[field])
if (error) {
newErrors[field] = error
isValid = false
} else {
delete newErrors[field]
}
})
setErrors(newErrors)
return isValid
}
const setValue = (fieldName: keyof T, value: any): void => {
setValues(prev => ({ ...prev, [fieldName]: value }))
setTouched(prev => ({ ...prev, [fieldName]: true }))
}
const resetForm = (): void => {
setValues(initialValues)
setErrors({})
setTouched({})
}
return {
values,
errors,
touched,
setValue,
validate,
resetForm,
isValid: Object.keys(errors).length === 0
}
}
+224 -224
View File
@@ -1,225 +1,225 @@
import { useEffect, useState, useCallback } from 'react'
interface PerformanceMetrics {
FCP: number | null // First Contentful Paint
LCP: number | null // Largest Contentful Paint
FID: number | null // First Input Delay
CLS: number | null // Cumulative Layout Shift
TTFB: number | null // Time to First Byte
}
interface WebVitals {
metrics: PerformanceMetrics
isLoading: boolean
}
// Performance monitoring hook
export function usePerformance(): WebVitals {
const [metrics, setMetrics] = useState<PerformanceMetrics>({
FCP: null,
LCP: null,
FID: null,
CLS: null,
TTFB: null
})
const [isLoading, setIsLoading] = useState(true)
const updateMetric = useCallback((name: keyof PerformanceMetrics, value: number) => {
setMetrics(prev => ({ ...prev, [name]: value }))
}, [])
useEffect(() => {
// Check if Web Vitals API is available
if (typeof window === 'undefined') return
// Measure TTFB from Navigation Timing API
const measureTTFB = () => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
if (navigation) {
const ttfb = navigation.responseStart - navigation.requestStart
updateMetric('TTFB', ttfb)
}
}
// Measure FCP using PerformanceObserver
const measureFCP = () => {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByName('first-contentful-paint')
if (entries.length > 0) {
updateMetric('FCP', entries[0].startTime)
}
})
observer.observe({ entryTypes: ['paint'] })
return () => observer.disconnect()
} catch (error) {
console.warn('FCP measurement not supported')
return () => {}
}
}
// Measure LCP using PerformanceObserver
const measureLCP = () => {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
if (entries.length > 0) {
const lastEntry = entries[entries.length - 1]
updateMetric('LCP', lastEntry.startTime)
}
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
return () => observer.disconnect()
} catch (error) {
console.warn('LCP measurement not supported')
return () => {}
}
}
// Measure FID using PerformanceObserver
const measureFID = () => {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
entries.forEach((entry: any) => {
if (entry.name === 'first-input') {
const fid = entry.processingStart - entry.startTime
updateMetric('FID', fid)
}
})
})
observer.observe({ entryTypes: ['first-input'] })
return () => observer.disconnect()
} catch (error) {
console.warn('FID measurement not supported')
return () => {}
}
}
// Measure CLS using PerformanceObserver
const measureCLS = () => {
try {
let clsValue = 0
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
entries.forEach((entry: any) => {
if (!entry.hadRecentInput) {
clsValue += entry.value
updateMetric('CLS', clsValue)
}
})
})
observer.observe({ entryTypes: ['layout-shift'] })
return () => observer.disconnect()
} catch (error) {
console.warn('CLS measurement not supported')
return () => {}
}
}
// Initialize measurements
measureTTFB()
const cleanupFCP = measureFCP()
const cleanupLCP = measureLCP()
const cleanupFID = measureFID()
const cleanupCLS = measureCLS()
// Set loading to false after initial measurements
const timeout = setTimeout(() => setIsLoading(false), 2000)
return () => {
clearTimeout(timeout)
cleanupFCP()
cleanupLCP()
cleanupFID()
cleanupCLS()
}
}, [])
return { metrics, isLoading }
}
// Hook for monitoring bundle size and loading performance
export function useBundlePerformance() {
const [bundleMetrics, setBundleMetrics] = useState({
totalSize: 0,
jsSize: 0,
cssSize: 0,
imageSize: 0,
loadTime: 0
})
useEffect(() => {
if (typeof window === 'undefined') return
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByType('resource')
let jsSize = 0
let cssSize = 0
let imageSize = 0
let totalSize = 0
entries.forEach((entry: any) => {
const size = entry.encodedBodySize || entry.transferSize || 0
totalSize += size
if (entry.name.includes('.js')) jsSize += size
else if (entry.name.includes('.css')) cssSize += size
else if (entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) imageSize += size
})
setBundleMetrics({
totalSize: Math.round(totalSize / 1024), // KB
jsSize: Math.round(jsSize / 1024),
cssSize: Math.round(cssSize / 1024),
imageSize: Math.round(imageSize / 1024),
loadTime: performance.now()
})
})
observer.observe({ entryTypes: ['resource'] })
return () => observer.disconnect()
}, [])
return bundleMetrics
}
// Analytics tracking for performance metrics
export function trackPerformanceMetrics(metrics: PerformanceMetrics) {
if (typeof window === 'undefined' || !(window as any).gtag) return
const { FCP, LCP, FID, CLS, TTFB } = metrics
// Send to Google Analytics
if (FCP) (window as any).gtag('event', 'timing_complete', {
name: 'FCP',
value: Math.round(FCP)
})
if (LCP) (window as any).gtag('event', 'timing_complete', {
name: 'LCP',
value: Math.round(LCP)
})
if (FID) (window as any).gtag('event', 'timing_complete', {
name: 'FID',
value: Math.round(FID)
})
if (CLS) (window as any).gtag('event', 'timing_complete', {
name: 'CLS',
value: Math.round(CLS * 1000) // Convert to ms
})
if (TTFB) (window as any).gtag('event', 'timing_complete', {
name: 'TTFB',
value: Math.round(TTFB)
})
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.group('Performance Metrics:')
console.table(metrics)
console.groupEnd()
}
import { useEffect, useState, useCallback } from 'react'
interface PerformanceMetrics {
FCP: number | null // First Contentful Paint
LCP: number | null // Largest Contentful Paint
FID: number | null // First Input Delay
CLS: number | null // Cumulative Layout Shift
TTFB: number | null // Time to First Byte
}
interface WebVitals {
metrics: PerformanceMetrics
isLoading: boolean
}
// Performance monitoring hook
export function usePerformance(): WebVitals {
const [metrics, setMetrics] = useState<PerformanceMetrics>({
FCP: null,
LCP: null,
FID: null,
CLS: null,
TTFB: null
})
const [isLoading, setIsLoading] = useState(true)
const updateMetric = useCallback((name: keyof PerformanceMetrics, value: number) => {
setMetrics(prev => ({ ...prev, [name]: value }))
}, [])
useEffect(() => {
// Check if Web Vitals API is available
if (typeof window === 'undefined') return
// Measure TTFB from Navigation Timing API
const measureTTFB = () => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
if (navigation) {
const ttfb = navigation.responseStart - navigation.requestStart
updateMetric('TTFB', ttfb)
}
}
// Measure FCP using PerformanceObserver
const measureFCP = () => {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByName('first-contentful-paint')
if (entries.length > 0) {
updateMetric('FCP', entries[0].startTime)
}
})
observer.observe({ entryTypes: ['paint'] })
return () => observer.disconnect()
} catch (error) {
console.warn('FCP measurement not supported')
return () => {}
}
}
// Measure LCP using PerformanceObserver
const measureLCP = () => {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
if (entries.length > 0) {
const lastEntry = entries[entries.length - 1]
updateMetric('LCP', lastEntry.startTime)
}
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
return () => observer.disconnect()
} catch (error) {
console.warn('LCP measurement not supported')
return () => {}
}
}
// Measure FID using PerformanceObserver
const measureFID = () => {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
entries.forEach((entry: any) => {
if (entry.name === 'first-input') {
const fid = entry.processingStart - entry.startTime
updateMetric('FID', fid)
}
})
})
observer.observe({ entryTypes: ['first-input'] })
return () => observer.disconnect()
} catch (error) {
console.warn('FID measurement not supported')
return () => {}
}
}
// Measure CLS using PerformanceObserver
const measureCLS = () => {
try {
let clsValue = 0
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
entries.forEach((entry: any) => {
if (!entry.hadRecentInput) {
clsValue += entry.value
updateMetric('CLS', clsValue)
}
})
})
observer.observe({ entryTypes: ['layout-shift'] })
return () => observer.disconnect()
} catch (error) {
console.warn('CLS measurement not supported')
return () => {}
}
}
// Initialize measurements
measureTTFB()
const cleanupFCP = measureFCP()
const cleanupLCP = measureLCP()
const cleanupFID = measureFID()
const cleanupCLS = measureCLS()
// Set loading to false after initial measurements
const timeout = setTimeout(() => setIsLoading(false), 2000)
return () => {
clearTimeout(timeout)
cleanupFCP()
cleanupLCP()
cleanupFID()
cleanupCLS()
}
}, [])
return { metrics, isLoading }
}
// Hook for monitoring bundle size and loading performance
export function useBundlePerformance() {
const [bundleMetrics, setBundleMetrics] = useState({
totalSize: 0,
jsSize: 0,
cssSize: 0,
imageSize: 0,
loadTime: 0
})
useEffect(() => {
if (typeof window === 'undefined') return
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByType('resource')
let jsSize = 0
let cssSize = 0
let imageSize = 0
let totalSize = 0
entries.forEach((entry: any) => {
const size = entry.encodedBodySize || entry.transferSize || 0
totalSize += size
if (entry.name.includes('.js')) jsSize += size
else if (entry.name.includes('.css')) cssSize += size
else if (entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) imageSize += size
})
setBundleMetrics({
totalSize: Math.round(totalSize / 1024), // KB
jsSize: Math.round(jsSize / 1024),
cssSize: Math.round(cssSize / 1024),
imageSize: Math.round(imageSize / 1024),
loadTime: performance.now()
})
})
observer.observe({ entryTypes: ['resource'] })
return () => observer.disconnect()
}, [])
return bundleMetrics
}
// Analytics tracking for performance metrics
export function trackPerformanceMetrics(metrics: PerformanceMetrics) {
if (typeof window === 'undefined' || !(window as any).gtag) return
const { FCP, LCP, FID, CLS, TTFB } = metrics
// Send to Google Analytics
if (FCP) (window as any).gtag('event', 'timing_complete', {
name: 'FCP',
value: Math.round(FCP)
})
if (LCP) (window as any).gtag('event', 'timing_complete', {
name: 'LCP',
value: Math.round(LCP)
})
if (FID) (window as any).gtag('event', 'timing_complete', {
name: 'FID',
value: Math.round(FID)
})
if (CLS) (window as any).gtag('event', 'timing_complete', {
name: 'CLS',
value: Math.round(CLS * 1000) // Convert to ms
})
if (TTFB) (window as any).gtag('event', 'timing_complete', {
name: 'TTFB',
value: Math.round(TTFB)
})
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.group('Performance Metrics:')
console.table(metrics)
console.groupEnd()
}
}
+80 -80
View File
@@ -1,81 +1,81 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import Backend from 'i18next-http-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
// Import translation files
import enTranslations from './locales/en.json'
import esTranslations from './locales/es.json'
import frTranslations from './locales/fr.json'
import deTranslations from './locales/de.json'
import zhTranslations from './locales/zh.json'
import arTranslations from './locales/ar.json'
import ptTranslations from './locales/pt.json'
import ruTranslations from './locales/ru.json'
// Language configuration
export const languages = {
en: { name: 'English', flag: '🇺🇸', dir: 'ltr' },
es: { name: 'Español', flag: '🇪🇸', dir: 'ltr' },
fr: { name: 'Français', flag: '🇫🇷', dir: 'ltr' },
de: { name: 'Deutsch', flag: '🇩🇪', dir: 'ltr' },
zh: { name: '中文', flag: '🇨🇳', dir: 'ltr' },
ar: { name: 'العربية', flag: '🇸🇦', dir: 'rtl' },
pt: { name: 'Português', flag: '🇧🇷', dir: 'ltr' },
ru: { name: 'Русский', flag: '🇷🇺', dir: 'ltr' }
}
// Resources object
const resources = {
en: { translation: enTranslations },
es: { translation: esTranslations },
fr: { translation: frTranslations },
de: { translation: deTranslations },
zh: { translation: zhTranslations },
ar: { translation: arTranslations },
pt: { translation: ptTranslations },
ru: { translation: ruTranslations }
}
// Initialize i18n
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage'],
lookupLocalStorage: 'i18nextLng'
},
interpolation: {
escapeValue: false // React already does escaping
},
resources,
// Namespace configuration
defaultNS: 'translation',
ns: ['translation'],
// React options
react: {
useSuspense: false,
bindI18n: 'languageChanged',
bindI18nStore: '',
transEmptyNodeValue: '',
transSupportBasicHtmlNodes: true,
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'em', 'span']
},
// Backend options for loading translations
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
}
})
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import Backend from 'i18next-http-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
// Import translation files
import enTranslations from './locales/en.json'
import esTranslations from './locales/es.json'
import frTranslations from './locales/fr.json'
import deTranslations from './locales/de.json'
import zhTranslations from './locales/zh.json'
import arTranslations from './locales/ar.json'
import ptTranslations from './locales/pt.json'
import ruTranslations from './locales/ru.json'
// Language configuration
export const languages = {
en: { name: 'English', flag: '🇺🇸', dir: 'ltr' },
es: { name: 'Español', flag: '🇪🇸', dir: 'ltr' },
fr: { name: 'Français', flag: '🇫🇷', dir: 'ltr' },
de: { name: 'Deutsch', flag: '🇩🇪', dir: 'ltr' },
zh: { name: '中文', flag: '🇨🇳', dir: 'ltr' },
ar: { name: 'العربية', flag: '🇸🇦', dir: 'rtl' },
pt: { name: 'Português', flag: '🇧🇷', dir: 'ltr' },
ru: { name: 'Русский', flag: '🇷🇺', dir: 'ltr' }
}
// Resources object
const resources = {
en: { translation: enTranslations },
es: { translation: esTranslations },
fr: { translation: frTranslations },
de: { translation: deTranslations },
zh: { translation: zhTranslations },
ar: { translation: arTranslations },
pt: { translation: ptTranslations },
ru: { translation: ruTranslations }
}
// Initialize i18n
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage'],
lookupLocalStorage: 'i18nextLng'
},
interpolation: {
escapeValue: false // React already does escaping
},
resources,
// Namespace configuration
defaultNS: 'translation',
ns: ['translation'],
// React options
react: {
useSuspense: false,
bindI18n: 'languageChanged',
bindI18nStore: '',
transEmptyNodeValue: '',
transSupportBasicHtmlNodes: true,
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'em', 'span']
},
// Backend options for loading translations
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
}
})
export default i18n
+20 -20
View File
@@ -1,21 +1,21 @@
{
"navigation": {
"home": "الرئيسية",
"about": "حولنا",
"programs": "البرامج",
"donate": "تبرع",
"volunteer": "متطوع",
"contact": "اتصل بنا"
},
"hero": {
"title": "تمكين الطلاب بـ",
"titleHighlight": "الدعم الأساسي",
"subtitle": "منظمة غير ربحية 501(c)(3) تقدم للطلاب المستلزمات المدرسية والملابس والدعم الطارئ لمساعدتهم على النجاح في المدرسة والحياة."
},
"ai": {
"assistant": {
"title": "مساعد الطلاب الذكي",
"placeholder": "كيف يمكنني مساعدتك اليوم؟"
}
}
{
"navigation": {
"home": "الرئيسية",
"about": "حولنا",
"programs": "البرامج",
"donate": "تبرع",
"volunteer": "متطوع",
"contact": "اتصل بنا"
},
"hero": {
"title": "تمكين الطلاب بـ",
"titleHighlight": "الدعم الأساسي",
"subtitle": "منظمة غير ربحية 501(c)(3) تقدم للطلاب المستلزمات المدرسية والملابس والدعم الطارئ لمساعدتهم على النجاح في المدرسة والحياة."
},
"ai": {
"assistant": {
"title": "مساعد الطلاب الذكي",
"placeholder": "كيف يمكنني مساعدتك اليوم؟"
}
}
}
+20 -20
View File
@@ -1,21 +1,21 @@
{
"navigation": {
"home": "Startseite",
"about": "Über uns",
"programs": "Programme",
"donate": "Spenden",
"volunteer": "Freiwilliger",
"contact": "Kontakt"
},
"hero": {
"title": "Studenten stärken mit",
"titleHighlight": "wesentlicher Unterstützung",
"subtitle": "Eine gemeinnützige 501(c)(3) Organisation, die Studenten mit Schulmaterialien, Kleidung und Notfallunterstützung versorgt."
},
"ai": {
"assistant": {
"title": "KI-Studienassistent",
"placeholder": "Wie kann ich Ihnen heute helfen?"
}
}
{
"navigation": {
"home": "Startseite",
"about": "Über uns",
"programs": "Programme",
"donate": "Spenden",
"volunteer": "Freiwilliger",
"contact": "Kontakt"
},
"hero": {
"title": "Studenten stärken mit",
"titleHighlight": "wesentlicher Unterstützung",
"subtitle": "Eine gemeinnützige 501(c)(3) Organisation, die Studenten mit Schulmaterialien, Kleidung und Notfallunterstützung versorgt."
},
"ai": {
"assistant": {
"title": "KI-Studienassistent",
"placeholder": "Wie kann ich Ihnen heute helfen?"
}
}
}
+190 -190
View File
@@ -1,191 +1,191 @@
{
"navigation": {
"home": "Home",
"about": "About",
"programs": "Programs",
"donate": "Donate",
"volunteer": "Volunteer",
"contact": "Contact",
"dashboard": "Dashboard",
"login": "Login",
"logout": "Logout",
"profile": "Profile",
"settings": "Settings"
},
"hero": {
"title": "Empowering Students with",
"titleHighlight": "Essential Support",
"subtitle": "A 501(c)(3) nonprofit providing students with school supplies, clothing, and emergency support to help them succeed in school and life.",
"primaryButton": "Make a Donation",
"secondaryButton": "Learn More",
"stats": {
"studentsHelped": "Students Helped",
"suppliesDistributed": "Supplies Distributed",
"schoolsPartnered": "School Partners",
"volunteersActive": "Active Volunteers"
}
},
"programs": {
"title": "Our Programs",
"subtitle": "Comprehensive support systems designed to help students thrive",
"schoolSupplies": {
"title": "School Supplies",
"description": "Essential learning materials for academic success",
"impact": "12,000+ students equipped"
},
"clothing": {
"title": "Clothing Assistance",
"description": "Proper attire to boost confidence and school attendance",
"impact": "8,500+ students clothed"
},
"emergency": {
"title": "Emergency Support",
"description": "Rapid assistance for urgent student needs",
"impact": "2,100+ emergencies resolved"
},
"mentalHealth": {
"title": "Mental Health",
"description": "Counseling and wellness support services",
"impact": "5,200+ students counseled"
},
"tutoring": {
"title": "Academic Tutoring",
"description": "One-on-one and group learning support",
"impact": "3,800+ tutoring hours"
},
"meals": {
"title": "Meal Programs",
"description": "Nutrition support for better learning outcomes",
"impact": "45,000+ meals provided"
},
"cta": "Join Our Mission"
},
"impact": {
"title": "Our Impact",
"subtitle": "Real numbers, real change in students' lives",
"metrics": {
"totalStudents": "Total Students Helped",
"totalSupplies": "Supplies Distributed",
"totalVolunteers": "Volunteer Hours",
"totalDonations": "Funds Raised"
}
},
"donate": {
"title": "Make a Difference Today",
"subtitle": "Your donation directly supports students in need",
"amounts": {
"25": "$25 - Supplies for 1 student",
"50": "$50 - Clothing for 1 student",
"100": "$100 - Emergency support",
"250": "$250 - Monthly tutoring"
},
"customAmount": "Custom Amount",
"monthly": "Monthly",
"oneTime": "One-time",
"processing": "Processing...",
"success": "Thank you for your donation!",
"error": "Payment failed. Please try again."
},
"footer": {
"mission": "Our Mission",
"missionText": "Empowering students with essential resources for educational success and personal growth.",
"quickLinks": "Quick Links",
"contact": "Contact Info",
"address": "123 Hope Street, Education City, EC 12345",
"phone": "+1 (555) 123-4567",
"email": "[email protected]",
"social": "Follow Us",
"newsletter": "Newsletter",
"newsletterText": "Stay updated with our latest programs and impact stories.",
"subscribe": "Subscribe",
"copyright": "© 2024 Miracles in Motion. All rights reserved.",
"privacy": "Privacy Policy",
"terms": "Terms of Service"
},
"ai": {
"assistant": {
"title": "AI Student Assistant",
"placeholder": "How can I help you today?",
"thinking": "Thinking...",
"error": "I'm having trouble right now. Please try again.",
"suggestions": [
"How do I apply for school supplies?",
"What clothing assistance is available?",
"How can I get emergency support?",
"Tell me about tutoring programs"
]
},
"chat": {
"welcome": "Hi! I'm here to help you learn about our programs and services. What would you like to know?",
"typing": "Assistant is typing...",
"send": "Send",
"clear": "Clear Chat",
"minimize": "Minimize",
"maximize": "Maximize"
}
},
"dashboard": {
"analytics": {
"title": "Analytics Dashboard",
"overview": "Overview",
"students": "Students",
"donations": "Donations",
"volunteers": "Volunteers",
"programs": "Programs",
"engagement": "Engagement",
"performance": "Performance"
},
"realtime": {
"title": "Real-time Updates",
"newDonation": "New donation received",
"newStudent": "New student registered",
"programUpdate": "Program update available",
"volunteerJoined": "New volunteer joined"
}
},
"forms": {
"required": "Required",
"optional": "Optional",
"submit": "Submit",
"cancel": "Cancel",
"save": "Save",
"edit": "Edit",
"delete": "Delete",
"confirm": "Confirm",
"loading": "Loading...",
"success": "Success!",
"error": "An error occurred",
"validation": {
"email": "Please enter a valid email",
"phone": "Please enter a valid phone number",
"required": "This field is required",
"minLength": "Minimum {{count}} characters required",
"maxLength": "Maximum {{count}} characters allowed"
}
},
"accessibility": {
"skipToContent": "Skip to main content",
"toggleMenu": "Toggle navigation menu",
"toggleLanguage": "Toggle language menu",
"closeModal": "Close modal",
"loading": "Loading content",
"imageAlt": "Image description: {{description}}"
},
"common": {
"yes": "Yes",
"no": "No",
"ok": "OK",
"back": "Back",
"next": "Next",
"previous": "Previous",
"close": "Close",
"open": "Open",
"search": "Search",
"filter": "Filter",
"sort": "Sort",
"clear": "Clear",
"refresh": "Refresh",
"retry": "Retry",
"continue": "Continue"
}
{
"navigation": {
"home": "Home",
"about": "About",
"programs": "Programs",
"donate": "Donate",
"volunteer": "Volunteer",
"contact": "Contact",
"dashboard": "Dashboard",
"login": "Login",
"logout": "Logout",
"profile": "Profile",
"settings": "Settings"
},
"hero": {
"title": "Empowering Students with",
"titleHighlight": "Essential Support",
"subtitle": "A 501(c)(3) nonprofit providing students with school supplies, clothing, and emergency support to help them succeed in school and life.",
"primaryButton": "Make a Donation",
"secondaryButton": "Learn More",
"stats": {
"studentsHelped": "Students Helped",
"suppliesDistributed": "Supplies Distributed",
"schoolsPartnered": "School Partners",
"volunteersActive": "Active Volunteers"
}
},
"programs": {
"title": "Our Programs",
"subtitle": "Comprehensive support systems designed to help students thrive",
"schoolSupplies": {
"title": "School Supplies",
"description": "Essential learning materials for academic success",
"impact": "12,000+ students equipped"
},
"clothing": {
"title": "Clothing Assistance",
"description": "Proper attire to boost confidence and school attendance",
"impact": "8,500+ students clothed"
},
"emergency": {
"title": "Emergency Support",
"description": "Rapid assistance for urgent student needs",
"impact": "2,100+ emergencies resolved"
},
"mentalHealth": {
"title": "Mental Health",
"description": "Counseling and wellness support services",
"impact": "5,200+ students counseled"
},
"tutoring": {
"title": "Academic Tutoring",
"description": "One-on-one and group learning support",
"impact": "3,800+ tutoring hours"
},
"meals": {
"title": "Meal Programs",
"description": "Nutrition support for better learning outcomes",
"impact": "45,000+ meals provided"
},
"cta": "Join Our Mission"
},
"impact": {
"title": "Our Impact",
"subtitle": "Real numbers, real change in students' lives",
"metrics": {
"totalStudents": "Total Students Helped",
"totalSupplies": "Supplies Distributed",
"totalVolunteers": "Volunteer Hours",
"totalDonations": "Funds Raised"
}
},
"donate": {
"title": "Make a Difference Today",
"subtitle": "Your donation directly supports students in need",
"amounts": {
"25": "$25 - Supplies for 1 student",
"50": "$50 - Clothing for 1 student",
"100": "$100 - Emergency support",
"250": "$250 - Monthly tutoring"
},
"customAmount": "Custom Amount",
"monthly": "Monthly",
"oneTime": "One-time",
"processing": "Processing...",
"success": "Thank you for your donation!",
"error": "Payment failed. Please try again."
},
"footer": {
"mission": "Our Mission",
"missionText": "Empowering students with essential resources for educational success and personal growth.",
"quickLinks": "Quick Links",
"contact": "Contact Info",
"address": "123 Hope Street, Education City, EC 12345",
"phone": "+1 (555) 123-4567",
"email": "[email protected]",
"social": "Follow Us",
"newsletter": "Newsletter",
"newsletterText": "Stay updated with our latest programs and impact stories.",
"subscribe": "Subscribe",
"copyright": "© 2024 Miracles in Motion. All rights reserved.",
"privacy": "Privacy Policy",
"terms": "Terms of Service"
},
"ai": {
"assistant": {
"title": "AI Student Assistant",
"placeholder": "How can I help you today?",
"thinking": "Thinking...",
"error": "I'm having trouble right now. Please try again.",
"suggestions": [
"How do I apply for school supplies?",
"What clothing assistance is available?",
"How can I get emergency support?",
"Tell me about tutoring programs"
]
},
"chat": {
"welcome": "Hi! I'm here to help you learn about our programs and services. What would you like to know?",
"typing": "Assistant is typing...",
"send": "Send",
"clear": "Clear Chat",
"minimize": "Minimize",
"maximize": "Maximize"
}
},
"dashboard": {
"analytics": {
"title": "Analytics Dashboard",
"overview": "Overview",
"students": "Students",
"donations": "Donations",
"volunteers": "Volunteers",
"programs": "Programs",
"engagement": "Engagement",
"performance": "Performance"
},
"realtime": {
"title": "Real-time Updates",
"newDonation": "New donation received",
"newStudent": "New student registered",
"programUpdate": "Program update available",
"volunteerJoined": "New volunteer joined"
}
},
"forms": {
"required": "Required",
"optional": "Optional",
"submit": "Submit",
"cancel": "Cancel",
"save": "Save",
"edit": "Edit",
"delete": "Delete",
"confirm": "Confirm",
"loading": "Loading...",
"success": "Success!",
"error": "An error occurred",
"validation": {
"email": "Please enter a valid email",
"phone": "Please enter a valid phone number",
"required": "This field is required",
"minLength": "Minimum {{count}} characters required",
"maxLength": "Maximum {{count}} characters allowed"
}
},
"accessibility": {
"skipToContent": "Skip to main content",
"toggleMenu": "Toggle navigation menu",
"toggleLanguage": "Toggle language menu",
"closeModal": "Close modal",
"loading": "Loading content",
"imageAlt": "Image description: {{description}}"
},
"common": {
"yes": "Yes",
"no": "No",
"ok": "OK",
"back": "Back",
"next": "Next",
"previous": "Previous",
"close": "Close",
"open": "Open",
"search": "Search",
"filter": "Filter",
"sort": "Sort",
"clear": "Clear",
"refresh": "Refresh",
"retry": "Retry",
"continue": "Continue"
}
}
+190 -190
View File
@@ -1,191 +1,191 @@
{
"navigation": {
"home": "Inicio",
"about": "Acerca de",
"programs": "Programas",
"donate": "Donar",
"volunteer": "Voluntario",
"contact": "Contacto",
"dashboard": "Panel",
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"profile": "Perfil",
"settings": "Configuración"
},
"hero": {
"title": "Empoderando Estudiantes con",
"titleHighlight": "Apoyo Esencial",
"subtitle": "Una organización sin fines de lucro 501(c)(3) que proporciona a los estudiantes útiles escolares, ropa y apoyo de emergencia para ayudarlos a tener éxito en la escuela y la vida.",
"primaryButton": "Hacer una Donación",
"secondaryButton": "Saber Más",
"stats": {
"studentsHelped": "Estudiantes Ayudados",
"suppliesDistributed": "Suministros Distribuidos",
"schoolsPartnered": "Escuelas Asociadas",
"volunteersActive": "Voluntarios Activos"
}
},
"programs": {
"title": "Nuestros Programas",
"subtitle": "Sistemas de apoyo integral diseñados para ayudar a los estudiantes a prosperar",
"schoolSupplies": {
"title": "Útiles Escolares",
"description": "Materiales de aprendizaje esenciales para el éxito académico",
"impact": "12,000+ estudiantes equipados"
},
"clothing": {
"title": "Asistencia de Ropa",
"description": "Vestimenta adecuada para aumentar la confianza y la asistencia escolar",
"impact": "8,500+ estudiantes vestidos"
},
"emergency": {
"title": "Apoyo de Emergencia",
"description": "Asistencia rápida para necesidades urgentes de estudiantes",
"impact": "2,100+ emergencias resueltas"
},
"mentalHealth": {
"title": "Salud Mental",
"description": "Servicios de consejería y apoyo al bienestar",
"impact": "5,200+ estudiantes aconsejados"
},
"tutoring": {
"title": "Tutoría Académica",
"description": "Apoyo de aprendizaje individual y grupal",
"impact": "3,800+ horas de tutoría"
},
"meals": {
"title": "Programas de Comidas",
"description": "Apoyo nutricional para mejores resultados de aprendizaje",
"impact": "45,000+ comidas proporcionadas"
},
"cta": "Únete a Nuestra Misión"
},
"impact": {
"title": "Nuestro Impacto",
"subtitle": "Números reales, cambios reales en las vidas de los estudiantes",
"metrics": {
"totalStudents": "Total de Estudiantes Ayudados",
"totalSupplies": "Suministros Distribuidos",
"totalVolunteers": "Horas de Voluntariado",
"totalDonations": "Fondos Recaudados"
}
},
"donate": {
"title": "Haz la Diferencia Hoy",
"subtitle": "Tu donación apoya directamente a estudiantes necesitados",
"amounts": {
"25": "$25 - Suministros para 1 estudiante",
"50": "$50 - Ropa para 1 estudiante",
"100": "$100 - Apoyo de emergencia",
"250": "$250 - Tutoría mensual"
},
"customAmount": "Cantidad Personalizada",
"monthly": "Mensual",
"oneTime": "Una vez",
"processing": "Procesando...",
"success": "¡Gracias por tu donación!",
"error": "El pago falló. Por favor intenta de nuevo."
},
"footer": {
"mission": "Nuestra Misión",
"missionText": "Empoderando estudiantes con recursos esenciales para el éxito educativo y crecimiento personal.",
"quickLinks": "Enlaces Rápidos",
"contact": "Información de Contacto",
"address": "123 Hope Street, Education City, EC 12345",
"phone": "+1 (555) 123-4567",
"email": "[email protected]",
"social": "Síguenos",
"newsletter": "Boletín",
"newsletterText": "Mantente actualizado con nuestros últimos programas e historias de impacto.",
"subscribe": "Suscribirse",
"copyright": "© 2024 Miracles in Motion. Todos los derechos reservados.",
"privacy": "Política de Privacidad",
"terms": "Términos de Servicio"
},
"ai": {
"assistant": {
"title": "Asistente de IA para Estudiantes",
"placeholder": "¿Cómo puedo ayudarte hoy?",
"thinking": "Pensando...",
"error": "Tengo problemas ahora. Por favor intenta de nuevo.",
"suggestions": [
"¿Cómo solicito útiles escolares?",
"¿Qué asistencia de ropa está disponible?",
"¿Cómo puedo obtener apoyo de emergencia?",
"Cuéntame sobre los programas de tutoría"
]
},
"chat": {
"welcome": "¡Hola! Estoy aquí para ayudarte a aprender sobre nuestros programas y servicios. ¿Qué te gustaría saber?",
"typing": "El asistente está escribiendo...",
"send": "Enviar",
"clear": "Limpiar Chat",
"minimize": "Minimizar",
"maximize": "Maximizar"
}
},
"dashboard": {
"analytics": {
"title": "Panel de Análisis",
"overview": "Resumen",
"students": "Estudiantes",
"donations": "Donaciones",
"volunteers": "Voluntarios",
"programs": "Programas",
"engagement": "Participación",
"performance": "Rendimiento"
},
"realtime": {
"title": "Actualizaciones en Tiempo Real",
"newDonation": "Nueva donación recibida",
"newStudent": "Nuevo estudiante registrado",
"programUpdate": "Actualización de programa disponible",
"volunteerJoined": "Nuevo voluntario se unió"
}
},
"forms": {
"required": "Requerido",
"optional": "Opcional",
"submit": "Enviar",
"cancel": "Cancelar",
"save": "Guardar",
"edit": "Editar",
"delete": "Eliminar",
"confirm": "Confirmar",
"loading": "Cargando...",
"success": "¡Éxito!",
"error": "Ocurrió un error",
"validation": {
"email": "Por favor ingresa un email válido",
"phone": "Por favor ingresa un número de teléfono válido",
"required": "Este campo es requerido",
"minLength": "Se requieren mínimo {{count}} caracteres",
"maxLength": "Máximo {{count}} caracteres permitidos"
}
},
"accessibility": {
"skipToContent": "Saltar al contenido principal",
"toggleMenu": "Alternar menú de navegación",
"toggleLanguage": "Alternar menú de idioma",
"closeModal": "Cerrar modal",
"loading": "Cargando contenido",
"imageAlt": "Descripción de imagen: {{description}}"
},
"common": {
"yes": "Sí",
"no": "No",
"ok": "OK",
"back": "Atrás",
"next": "Siguiente",
"previous": "Anterior",
"close": "Cerrar",
"open": "Abrir",
"search": "Buscar",
"filter": "Filtrar",
"sort": "Ordenar",
"clear": "Limpiar",
"refresh": "Actualizar",
"retry": "Reintentar",
"continue": "Continuar"
}
{
"navigation": {
"home": "Inicio",
"about": "Acerca de",
"programs": "Programas",
"donate": "Donar",
"volunteer": "Voluntario",
"contact": "Contacto",
"dashboard": "Panel",
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"profile": "Perfil",
"settings": "Configuración"
},
"hero": {
"title": "Empoderando Estudiantes con",
"titleHighlight": "Apoyo Esencial",
"subtitle": "Una organización sin fines de lucro 501(c)(3) que proporciona a los estudiantes útiles escolares, ropa y apoyo de emergencia para ayudarlos a tener éxito en la escuela y la vida.",
"primaryButton": "Hacer una Donación",
"secondaryButton": "Saber Más",
"stats": {
"studentsHelped": "Estudiantes Ayudados",
"suppliesDistributed": "Suministros Distribuidos",
"schoolsPartnered": "Escuelas Asociadas",
"volunteersActive": "Voluntarios Activos"
}
},
"programs": {
"title": "Nuestros Programas",
"subtitle": "Sistemas de apoyo integral diseñados para ayudar a los estudiantes a prosperar",
"schoolSupplies": {
"title": "Útiles Escolares",
"description": "Materiales de aprendizaje esenciales para el éxito académico",
"impact": "12,000+ estudiantes equipados"
},
"clothing": {
"title": "Asistencia de Ropa",
"description": "Vestimenta adecuada para aumentar la confianza y la asistencia escolar",
"impact": "8,500+ estudiantes vestidos"
},
"emergency": {
"title": "Apoyo de Emergencia",
"description": "Asistencia rápida para necesidades urgentes de estudiantes",
"impact": "2,100+ emergencias resueltas"
},
"mentalHealth": {
"title": "Salud Mental",
"description": "Servicios de consejería y apoyo al bienestar",
"impact": "5,200+ estudiantes aconsejados"
},
"tutoring": {
"title": "Tutoría Académica",
"description": "Apoyo de aprendizaje individual y grupal",
"impact": "3,800+ horas de tutoría"
},
"meals": {
"title": "Programas de Comidas",
"description": "Apoyo nutricional para mejores resultados de aprendizaje",
"impact": "45,000+ comidas proporcionadas"
},
"cta": "Únete a Nuestra Misión"
},
"impact": {
"title": "Nuestro Impacto",
"subtitle": "Números reales, cambios reales en las vidas de los estudiantes",
"metrics": {
"totalStudents": "Total de Estudiantes Ayudados",
"totalSupplies": "Suministros Distribuidos",
"totalVolunteers": "Horas de Voluntariado",
"totalDonations": "Fondos Recaudados"
}
},
"donate": {
"title": "Haz la Diferencia Hoy",
"subtitle": "Tu donación apoya directamente a estudiantes necesitados",
"amounts": {
"25": "$25 - Suministros para 1 estudiante",
"50": "$50 - Ropa para 1 estudiante",
"100": "$100 - Apoyo de emergencia",
"250": "$250 - Tutoría mensual"
},
"customAmount": "Cantidad Personalizada",
"monthly": "Mensual",
"oneTime": "Una vez",
"processing": "Procesando...",
"success": "¡Gracias por tu donación!",
"error": "El pago falló. Por favor intenta de nuevo."
},
"footer": {
"mission": "Nuestra Misión",
"missionText": "Empoderando estudiantes con recursos esenciales para el éxito educativo y crecimiento personal.",
"quickLinks": "Enlaces Rápidos",
"contact": "Información de Contacto",
"address": "123 Hope Street, Education City, EC 12345",
"phone": "+1 (555) 123-4567",
"email": "[email protected]",
"social": "Síguenos",
"newsletter": "Boletín",
"newsletterText": "Mantente actualizado con nuestros últimos programas e historias de impacto.",
"subscribe": "Suscribirse",
"copyright": "© 2024 Miracles in Motion. Todos los derechos reservados.",
"privacy": "Política de Privacidad",
"terms": "Términos de Servicio"
},
"ai": {
"assistant": {
"title": "Asistente de IA para Estudiantes",
"placeholder": "¿Cómo puedo ayudarte hoy?",
"thinking": "Pensando...",
"error": "Tengo problemas ahora. Por favor intenta de nuevo.",
"suggestions": [
"¿Cómo solicito útiles escolares?",
"¿Qué asistencia de ropa está disponible?",
"¿Cómo puedo obtener apoyo de emergencia?",
"Cuéntame sobre los programas de tutoría"
]
},
"chat": {
"welcome": "¡Hola! Estoy aquí para ayudarte a aprender sobre nuestros programas y servicios. ¿Qué te gustaría saber?",
"typing": "El asistente está escribiendo...",
"send": "Enviar",
"clear": "Limpiar Chat",
"minimize": "Minimizar",
"maximize": "Maximizar"
}
},
"dashboard": {
"analytics": {
"title": "Panel de Análisis",
"overview": "Resumen",
"students": "Estudiantes",
"donations": "Donaciones",
"volunteers": "Voluntarios",
"programs": "Programas",
"engagement": "Participación",
"performance": "Rendimiento"
},
"realtime": {
"title": "Actualizaciones en Tiempo Real",
"newDonation": "Nueva donación recibida",
"newStudent": "Nuevo estudiante registrado",
"programUpdate": "Actualización de programa disponible",
"volunteerJoined": "Nuevo voluntario se unió"
}
},
"forms": {
"required": "Requerido",
"optional": "Opcional",
"submit": "Enviar",
"cancel": "Cancelar",
"save": "Guardar",
"edit": "Editar",
"delete": "Eliminar",
"confirm": "Confirmar",
"loading": "Cargando...",
"success": "¡Éxito!",
"error": "Ocurrió un error",
"validation": {
"email": "Por favor ingresa un email válido",
"phone": "Por favor ingresa un número de teléfono válido",
"required": "Este campo es requerido",
"minLength": "Se requieren mínimo {{count}} caracteres",
"maxLength": "Máximo {{count}} caracteres permitidos"
}
},
"accessibility": {
"skipToContent": "Saltar al contenido principal",
"toggleMenu": "Alternar menú de navegación",
"toggleLanguage": "Alternar menú de idioma",
"closeModal": "Cerrar modal",
"loading": "Cargando contenido",
"imageAlt": "Descripción de imagen: {{description}}"
},
"common": {
"yes": "Sí",
"no": "No",
"ok": "OK",
"back": "Atrás",
"next": "Siguiente",
"previous": "Anterior",
"close": "Cerrar",
"open": "Abrir",
"search": "Buscar",
"filter": "Filtrar",
"sort": "Ordenar",
"clear": "Limpiar",
"refresh": "Actualizar",
"retry": "Reintentar",
"continue": "Continuar"
}
}
+33 -33
View File
@@ -1,34 +1,34 @@
{
"navigation": {
"home": "Accueil",
"about": "À propos",
"programs": "Programmes",
"donate": "Faire un don",
"volunteer": "Bénévole",
"contact": "Contact",
"dashboard": "Tableau de bord",
"login": "Connexion",
"logout": "Déconnexion",
"profile": "Profil",
"settings": "Paramètres"
},
"hero": {
"title": "Autonomiser les étudiants avec",
"titleHighlight": "un soutien essentiel",
"subtitle": "Une organisation à but non lucratif 501(c)(3) fournissant aux étudiants des fournitures scolaires, des vêtements et un soutien d'urgence pour les aider à réussir à l'école et dans la vie.",
"primaryButton": "Faire un don",
"secondaryButton": "En savoir plus"
},
"programs": {
"title": "Nos programmes",
"subtitle": "Systèmes de soutien complets conçus pour aider les étudiants à s'épanouir"
},
"ai": {
"assistant": {
"title": "Assistant IA pour étudiants",
"placeholder": "Comment puis-je vous aider aujourd'hui ?",
"thinking": "Réflexion...",
"error": "J'ai des difficultés en ce moment. Veuillez réessayer."
}
}
{
"navigation": {
"home": "Accueil",
"about": "À propos",
"programs": "Programmes",
"donate": "Faire un don",
"volunteer": "Bénévole",
"contact": "Contact",
"dashboard": "Tableau de bord",
"login": "Connexion",
"logout": "Déconnexion",
"profile": "Profil",
"settings": "Paramètres"
},
"hero": {
"title": "Autonomiser les étudiants avec",
"titleHighlight": "un soutien essentiel",
"subtitle": "Une organisation à but non lucratif 501(c)(3) fournissant aux étudiants des fournitures scolaires, des vêtements et un soutien d'urgence pour les aider à réussir à l'école et dans la vie.",
"primaryButton": "Faire un don",
"secondaryButton": "En savoir plus"
},
"programs": {
"title": "Nos programmes",
"subtitle": "Systèmes de soutien complets conçus pour aider les étudiants à s'épanouir"
},
"ai": {
"assistant": {
"title": "Assistant IA pour étudiants",
"placeholder": "Comment puis-je vous aider aujourd'hui ?",
"thinking": "Réflexion...",
"error": "J'ai des difficultés en ce moment. Veuillez réessayer."
}
}
}
+20 -20
View File
@@ -1,21 +1,21 @@
{
"navigation": {
"home": "Início",
"about": "Sobre",
"programs": "Programas",
"donate": "Doar",
"volunteer": "Voluntário",
"contact": "Contato"
},
"hero": {
"title": "Capacitando estudantes com",
"titleHighlight": "apoio essencial",
"subtitle": "Uma organização sem fins lucrativos 501(c)(3) fornecendo aos estudantes materiais escolares, roupas e apoio de emergência para ajudá-los a ter sucesso na escola e na vida."
},
"ai": {
"assistant": {
"title": "Assistente de IA para Estudantes",
"placeholder": "Como posso ajudar você hoje?"
}
}
{
"navigation": {
"home": "Início",
"about": "Sobre",
"programs": "Programas",
"donate": "Doar",
"volunteer": "Voluntário",
"contact": "Contato"
},
"hero": {
"title": "Capacitando estudantes com",
"titleHighlight": "apoio essencial",
"subtitle": "Uma organização sem fins lucrativos 501(c)(3) fornecendo aos estudantes materiais escolares, roupas e apoio de emergência para ajudá-los a ter sucesso na escola e na vida."
},
"ai": {
"assistant": {
"title": "Assistente de IA para Estudantes",
"placeholder": "Como posso ajudar você hoje?"
}
}
}
+20 -20
View File
@@ -1,21 +1,21 @@
{
"navigation": {
"home": "Главная",
"about": "О нас",
"programs": "Программы",
"donate": "Пожертвовать",
"volunteer": "Волонтер",
"contact": "Контакты"
},
"hero": {
"title": "Расширяем возможности студентов с",
"titleHighlight": "необходимой поддержкой",
"subtitle": "Некоммерческая организация 501(c)(3), предоставляющая студентам школьные принадлежности, одежду и экстренную поддержку, чтобы помочь им добиться успеха в школе и жизни."
},
"ai": {
"assistant": {
"title": "ИИ-помощник для студентов",
"placeholder": "Как я могу помочь вам сегодня?"
}
}
{
"navigation": {
"home": "Главная",
"about": "О нас",
"programs": "Программы",
"donate": "Пожертвовать",
"volunteer": "Волонтер",
"contact": "Контакты"
},
"hero": {
"title": "Расширяем возможности студентов с",
"titleHighlight": "необходимой поддержкой",
"subtitle": "Некоммерческая организация 501(c)(3), предоставляющая студентам школьные принадлежности, одежду и экстренную поддержку, чтобы помочь им добиться успеха в школе и жизни."
},
"ai": {
"assistant": {
"title": "ИИ-помощник для студентов",
"placeholder": "Как я могу помочь вам сегодня?"
}
}
}
+20 -20
View File
@@ -1,21 +1,21 @@
{
"navigation": {
"home": "首页",
"about": "关于我们",
"programs": "项目",
"donate": "捐赠",
"volunteer": "志愿者",
"contact": "联系我们"
},
"hero": {
"title": "为学生提供",
"titleHighlight": "基本支持",
"subtitle": "501(c)(3)非营利组织,为学生提供学习用品、衣物和紧急支持,帮助他们在学校和生活中取得成功。"
},
"ai": {
"assistant": {
"title": "AI学生助手",
"placeholder": "今天我能为您做些什么?"
}
}
{
"navigation": {
"home": "首页",
"about": "关于我们",
"programs": "项目",
"donate": "捐赠",
"volunteer": "志愿者",
"contact": "联系我们"
},
"hero": {
"title": "为学生提供",
"titleHighlight": "基本支持",
"subtitle": "501(c)(3)非营利组织,为学生提供学习用品、衣物和紧急支持,帮助他们在学校和生活中取得成功。"
},
"ai": {
"assistant": {
"title": "AI学生助手",
"placeholder": "今天我能为您做些什么?"
}
}
}
+425 -425
View File
@@ -1,426 +1,426 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
}
body {
font-feature-settings: 'rlig' 1, 'calt' 1;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #ec4899, #8b5cf6);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #db2777, #7c3aed);
}
}
@layer components {
/* Button Components */
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 9999px;
background-image: linear-gradient(to right, #db2777, #7c3aed); /* Replace with your primary-600 and secondary-600 colors */
padding-left: 1.5rem;
padding-right: 1.5rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 0.875rem;
font-weight: 500;
color: #fff;
box-shadow: 0 10px 15px -3px rgba(236, 72, 153, 0.25), 0 4px 6px -4px rgba(236, 72, 153, 0.25);
transition: transform 0.2s, box-shadow 0.2s;
outline: none;
}
.btn-primary:hover {
transform: scale(1.05);
box-shadow: 0 20px 25px -5px rgba(236, 72, 153, 0.25), 0 8px 10px -6px rgba(236, 72, 153, 0.25);
}
.btn-primary:focus {
outline: none;
box-shadow: 0 0 0 2px #ec4899, 0 10px 15px -3px rgba(236, 72, 153, 0.25), 0 4px 6px -4px rgba(236, 72, 153, 0.25);
}
.btn-primary:focus-visible {
outline: 2px solid #ec4899;
outline-offset: 2px;
}
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 9999px;
border: 1px solid #d4d4d8; /* border-neutral-300 */
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
padding-left: 1.5rem; /* px-6 */
padding-right: 1.5rem;
padding-top: 0.75rem; /* py-3 */
padding-bottom: 0.75rem;
font-size: 0.875rem; /* text-sm */
font-weight: 500; /* font-medium */
color: #52525b; /* text-neutral-700 */
backdrop-filter: blur(8px); /* backdrop-blur */
transition: background 0.2s, box-shadow 0.2s;
outline: none;
}
.btn-secondary:hover {
background-color: #fff;
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
}
.btn-secondary:focus {
outline: none;
box-shadow: 0 0 0 2px #737373, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
}
.btn-secondary:focus-visible {
outline: 2px solid #737373; /* focus:ring-neutral-500 */
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.btn-secondary {
border: 1px solid rgba(255,255,255,0.2); /* dark:border-white/20 */
background-color: rgba(255,255,255,0.1); /* dark:bg-white/10 */
color: #e5e5e5; /* dark:text-neutral-200 */
}
.btn-secondary:hover {
background-color: rgba(255,255,255,0.2); /* dark:hover:bg-white/20 */
}
}
.btn-white {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 9999px;
background-color: #fff;
padding-left: 1.5rem;
padding-right: 1.5rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 0.875rem;
font-weight: 500;
color: #18181b;
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
outline: none;
}
.btn-white:hover {
transform: scale(1.05);
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
}
.btn-white:focus {
outline: none;
box-shadow: 0 0 0 2px #fff, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
}
.btn-white:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
}
.navlink {
font-size: 0.875rem; /* text-sm */
font-weight: 500; /* font-medium */
color: #52525b; /* text-neutral-600 */
transition: color 0.2s;
}
.navlink:hover {
color: #ec4899; /* text-primary-600 */
}
@media (prefers-color-scheme: dark) {
.navlink {
color: #d4d4d8; /* dark:text-neutral-300 */
}
.navlink:hover {
color: #a78bfa; /* dark:hover:text-primary-400 */
}
}
/* Form Components */
.input-field {
width: 100%;
border-radius: 0.75rem;
border: 1px solid #d1d5db; /* gray-300 */
background-color: #fff;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 0.875rem;
color: #111827; /* gray-900 */
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-field::placeholder {
color: #6b7280; /* gray-500 */
opacity: 1;
}
.input-field:focus {
border-color: #ec4899; /* primary-500 */
outline: none;
box-shadow: 0 0 0 2px #ec4899, 0 0 0 2px rgba(236, 72, 153, 0.2);
}
.input-field:focus-visible {
outline: 2px solid #ec4899;
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.input-field {
border: 1px solid #4b5563; /* gray-600 */
background-color: #1f2937; /* gray-800 */
color: #f3f4f6; /* gray-100 */
}
.input-field::placeholder {
color: #9ca3af; /* gray-400 */
}
.input-field:focus {
border-color: #a21caf; /* primary-400 */
}
}
/* Text Utilities */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.input {
width: 100%;
border-radius: 0.75rem; /* rounded-xl */
border: 1px solid rgba(255,255,255,0.3); /* border-white/30 */
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
padding-left: 0.75rem; /* px-3 */
padding-right: 0.75rem;
padding-top: 0.5rem; /* py-2 */
padding-bottom: 0.5rem;
font-size: 0.875rem; /* text-sm */
backdrop-filter: blur(8px); /* backdrop-blur */
transition: border-color 0.2s, box-shadow 0.2s;
outline: none;
}
.input:focus {
border-color: #ec4899; /* primary-500 */
outline: none;
box-shadow: 0 0 0 2px #ec4899, 0 0 0 2px rgba(236, 72, 153, 0.2);
}
.input:focus-visible {
outline: 2px solid #ec4899;
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.input {
border: 1px solid rgba(255,255,255,0.1); /* dark:border-white/10 */
background-color: rgba(255,255,255,0.1); /* dark:bg-white/10 */
}
.input:focus {
border-color: #a21caf; /* dark:focus:border-primary-400 */
}
}
/* Card Components */
.card {
border-radius: 1rem; /* rounded-2xl */
border: 1px solid rgba(255,255,255,0.3); /* border-white/30 */
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
padding: 1.5rem; /* p-6 */
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1); /* shadow-xl */
backdrop-filter: blur(8px); /* backdrop-blur */
}
@media (prefers-color-scheme: dark) {
.card {
border: 1px solid rgba(255,255,255,0.1); /* dark:border-white/10 */
background-color: rgba(255,255,255,0.05); /* dark:bg-white/5 */
}
}
.card-hover {
transition: transform 0.2s, box-shadow 0.2s;
}
.card-hover:hover {
transform: translateY(-0.25rem); /* -translate-y-1 */
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25), 0 8px 10px -6px rgba(0,0,0,0.1); /* shadow-2xl */
}
/* Section Header */
.section-header {
margin-left: auto;
margin-right: auto;
max-width: 48rem; /* 3xl = 48rem */
text-align: center;
}
.section-eyebrow {
font-size: 0.875rem; /* text-sm */
text-transform: uppercase;
letter-spacing: 0.05em; /* tracking-wider */
color: #db2777; /* text-primary-600 */
}
@media (prefers-color-scheme: dark) {
.section-eyebrow {
color: #a78bfa; /* dark:text-primary-400 */
}
}
.section-title {
margin-top: 0.5rem; /* mt-2 */
font-size: 1.875rem; /* text-3xl */
font-weight: 700; /* font-bold */
letter-spacing: -0.025em; /* tracking-tight */
line-height: 2.25rem;
}
@media (min-width: 640px) {
.section-title {
font-size: 2.25rem; /* sm:text-4xl */
line-height: 2.5rem;
}
}
.section-subtitle {
margin-top: 1rem; /* mt-4 */
font-size: 1.125rem; /* text-lg */
color: #52525b; /* text-neutral-600 */
}
@media (prefers-color-scheme: dark) {
.section-subtitle {
color: #d4d4d8; /* dark:text-neutral-300 */
}
}
}
@layer utilities {
/* Gradient Text */
.gradient-text {
background-image: linear-gradient(to right, #ec4899, #a21caf, #7c3aed); /* Replace with your Tailwind color values */
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* Glass Effect */
.glass {
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
}
.glass-dark {
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(12px);
}
/* Text Balance */
.text-balance {
text-wrap: balance;
}
/* Animation Utilities */
.animate-in {
animation: animate-in 0.6s ease-out;
}
@keyframes animate-in {
from {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Focus States for Accessibility */
.focus-visible\:ring-2:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px rgba(236, 72, 153, 0.5);
}
}
/* High Contrast Mode Support */
@media (prefers-contrast: high) {
.btn-primary {
border-width: 2px;
border-style: solid;
border-color: currentColor;
}
.btn-secondary {
border-width: 2px;
border-style: solid;
border-color: currentColor;
}
.card {
border-width: 2px;
border-style: solid;
border-color: currentColor;
}
}
/* Reduced Motion Support */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.animate-marquee {
animation: none;
}
.animate-float {
animation: none;
}
.animate-pulse-slow {
animation: none;
}
}
/* Print Styles */
@media print {
.no-print {
display: none !important;
}
body {
color: black !important;
background: white !important;
}
.gradient-text {
color: black !important;
background: none !important;
-webkit-text-fill-color: initial !important;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
}
body {
font-feature-settings: 'rlig' 1, 'calt' 1;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #ec4899, #8b5cf6);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #db2777, #7c3aed);
}
}
@layer components {
/* Button Components */
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 9999px;
background-image: linear-gradient(to right, #db2777, #7c3aed); /* Replace with your primary-600 and secondary-600 colors */
padding-left: 1.5rem;
padding-right: 1.5rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 0.875rem;
font-weight: 500;
color: #fff;
box-shadow: 0 10px 15px -3px rgba(236, 72, 153, 0.25), 0 4px 6px -4px rgba(236, 72, 153, 0.25);
transition: transform 0.2s, box-shadow 0.2s;
outline: none;
}
.btn-primary:hover {
transform: scale(1.05);
box-shadow: 0 20px 25px -5px rgba(236, 72, 153, 0.25), 0 8px 10px -6px rgba(236, 72, 153, 0.25);
}
.btn-primary:focus {
outline: none;
box-shadow: 0 0 0 2px #ec4899, 0 10px 15px -3px rgba(236, 72, 153, 0.25), 0 4px 6px -4px rgba(236, 72, 153, 0.25);
}
.btn-primary:focus-visible {
outline: 2px solid #ec4899;
outline-offset: 2px;
}
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 9999px;
border: 1px solid #d4d4d8; /* border-neutral-300 */
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
padding-left: 1.5rem; /* px-6 */
padding-right: 1.5rem;
padding-top: 0.75rem; /* py-3 */
padding-bottom: 0.75rem;
font-size: 0.875rem; /* text-sm */
font-weight: 500; /* font-medium */
color: #52525b; /* text-neutral-700 */
backdrop-filter: blur(8px); /* backdrop-blur */
transition: background 0.2s, box-shadow 0.2s;
outline: none;
}
.btn-secondary:hover {
background-color: #fff;
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
}
.btn-secondary:focus {
outline: none;
box-shadow: 0 0 0 2px #737373, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
}
.btn-secondary:focus-visible {
outline: 2px solid #737373; /* focus:ring-neutral-500 */
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.btn-secondary {
border: 1px solid rgba(255,255,255,0.2); /* dark:border-white/20 */
background-color: rgba(255,255,255,0.1); /* dark:bg-white/10 */
color: #e5e5e5; /* dark:text-neutral-200 */
}
.btn-secondary:hover {
background-color: rgba(255,255,255,0.2); /* dark:hover:bg-white/20 */
}
}
.btn-white {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 9999px;
background-color: #fff;
padding-left: 1.5rem;
padding-right: 1.5rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 0.875rem;
font-weight: 500;
color: #18181b;
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
outline: none;
}
.btn-white:hover {
transform: scale(1.05);
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
}
.btn-white:focus {
outline: none;
box-shadow: 0 0 0 2px #fff, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
}
.btn-white:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
}
.navlink {
font-size: 0.875rem; /* text-sm */
font-weight: 500; /* font-medium */
color: #52525b; /* text-neutral-600 */
transition: color 0.2s;
}
.navlink:hover {
color: #ec4899; /* text-primary-600 */
}
@media (prefers-color-scheme: dark) {
.navlink {
color: #d4d4d8; /* dark:text-neutral-300 */
}
.navlink:hover {
color: #a78bfa; /* dark:hover:text-primary-400 */
}
}
/* Form Components */
.input-field {
width: 100%;
border-radius: 0.75rem;
border: 1px solid #d1d5db; /* gray-300 */
background-color: #fff;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 0.875rem;
color: #111827; /* gray-900 */
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-field::placeholder {
color: #6b7280; /* gray-500 */
opacity: 1;
}
.input-field:focus {
border-color: #ec4899; /* primary-500 */
outline: none;
box-shadow: 0 0 0 2px #ec4899, 0 0 0 2px rgba(236, 72, 153, 0.2);
}
.input-field:focus-visible {
outline: 2px solid #ec4899;
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.input-field {
border: 1px solid #4b5563; /* gray-600 */
background-color: #1f2937; /* gray-800 */
color: #f3f4f6; /* gray-100 */
}
.input-field::placeholder {
color: #9ca3af; /* gray-400 */
}
.input-field:focus {
border-color: #a21caf; /* primary-400 */
}
}
/* Text Utilities */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.input {
width: 100%;
border-radius: 0.75rem; /* rounded-xl */
border: 1px solid rgba(255,255,255,0.3); /* border-white/30 */
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
padding-left: 0.75rem; /* px-3 */
padding-right: 0.75rem;
padding-top: 0.5rem; /* py-2 */
padding-bottom: 0.5rem;
font-size: 0.875rem; /* text-sm */
backdrop-filter: blur(8px); /* backdrop-blur */
transition: border-color 0.2s, box-shadow 0.2s;
outline: none;
}
.input:focus {
border-color: #ec4899; /* primary-500 */
outline: none;
box-shadow: 0 0 0 2px #ec4899, 0 0 0 2px rgba(236, 72, 153, 0.2);
}
.input:focus-visible {
outline: 2px solid #ec4899;
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.input {
border: 1px solid rgba(255,255,255,0.1); /* dark:border-white/10 */
background-color: rgba(255,255,255,0.1); /* dark:bg-white/10 */
}
.input:focus {
border-color: #a21caf; /* dark:focus:border-primary-400 */
}
}
/* Card Components */
.card {
border-radius: 1rem; /* rounded-2xl */
border: 1px solid rgba(255,255,255,0.3); /* border-white/30 */
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
padding: 1.5rem; /* p-6 */
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1); /* shadow-xl */
backdrop-filter: blur(8px); /* backdrop-blur */
}
@media (prefers-color-scheme: dark) {
.card {
border: 1px solid rgba(255,255,255,0.1); /* dark:border-white/10 */
background-color: rgba(255,255,255,0.05); /* dark:bg-white/5 */
}
}
.card-hover {
transition: transform 0.2s, box-shadow 0.2s;
}
.card-hover:hover {
transform: translateY(-0.25rem); /* -translate-y-1 */
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25), 0 8px 10px -6px rgba(0,0,0,0.1); /* shadow-2xl */
}
/* Section Header */
.section-header {
margin-left: auto;
margin-right: auto;
max-width: 48rem; /* 3xl = 48rem */
text-align: center;
}
.section-eyebrow {
font-size: 0.875rem; /* text-sm */
text-transform: uppercase;
letter-spacing: 0.05em; /* tracking-wider */
color: #db2777; /* text-primary-600 */
}
@media (prefers-color-scheme: dark) {
.section-eyebrow {
color: #a78bfa; /* dark:text-primary-400 */
}
}
.section-title {
margin-top: 0.5rem; /* mt-2 */
font-size: 1.875rem; /* text-3xl */
font-weight: 700; /* font-bold */
letter-spacing: -0.025em; /* tracking-tight */
line-height: 2.25rem;
}
@media (min-width: 640px) {
.section-title {
font-size: 2.25rem; /* sm:text-4xl */
line-height: 2.5rem;
}
}
.section-subtitle {
margin-top: 1rem; /* mt-4 */
font-size: 1.125rem; /* text-lg */
color: #52525b; /* text-neutral-600 */
}
@media (prefers-color-scheme: dark) {
.section-subtitle {
color: #d4d4d8; /* dark:text-neutral-300 */
}
}
}
@layer utilities {
/* Gradient Text */
.gradient-text {
background-image: linear-gradient(to right, #ec4899, #a21caf, #7c3aed); /* Replace with your Tailwind color values */
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* Glass Effect */
.glass {
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
}
.glass-dark {
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(12px);
}
/* Text Balance */
.text-balance {
text-wrap: balance;
}
/* Animation Utilities */
.animate-in {
animation: animate-in 0.6s ease-out;
}
@keyframes animate-in {
from {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Focus States for Accessibility */
.focus-visible\:ring-2:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px rgba(236, 72, 153, 0.5);
}
}
/* High Contrast Mode Support */
@media (prefers-contrast: high) {
.btn-primary {
border-width: 2px;
border-style: solid;
border-color: currentColor;
}
.btn-secondary {
border-width: 2px;
border-style: solid;
border-color: currentColor;
}
.card {
border-width: 2px;
border-style: solid;
border-color: currentColor;
}
}
/* Reduced Motion Support */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.animate-marquee {
animation: none;
}
.animate-float {
animation: none;
}
.animate-pulse-slow {
animation: none;
}
}
/* Print Styles */
@media print {
.no-print {
display: none !important;
}
body {
color: black !important;
background: white !important;
}
.gradient-text {
color: black !important;
background: none !important;
-webkit-text-fill-color: initial !important;
}
}
+178 -178
View File
@@ -1,179 +1,179 @@
import React, { ReactNode, useState } from 'react'
import { motion } from 'framer-motion'
import { LucideIcon } from 'lucide-react'
import { Navigation } from '../components/Navigation'
import { Footer } from '../components/Footer'
interface MainLayoutProps {
children: ReactNode
className?: string
darkMode?: boolean
setDarkMode?: (value: boolean) => void
}
interface PageShellProps {
title: string
icon?: LucideIcon
eyebrow?: string
subtitle?: string
cta?: ReactNode
children: ReactNode
className?: string
}
// Main layout wrapper with Navigation and Footer
export const MainLayout: React.FC<MainLayoutProps> = ({
children,
className = '',
darkMode = false,
setDarkMode = () => {}
}) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return (
<div className={`min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 dark:from-gray-900 dark:via-gray-800 dark:to-purple-900 ${className}`}>
<header className="sticky top-0 z-40 bg-white/80 backdrop-blur dark:bg-black/80">
<Navigation
darkMode={darkMode}
setDarkMode={setDarkMode}
mobileMenuOpen={mobileMenuOpen}
setMobileMenuOpen={setMobileMenuOpen}
/>
</header>
<main id="content">
{children}
</main>
<Footer />
</div>
)
}
// Page shell component for consistent page structure
export const PageShell: React.FC<PageShellProps> = ({
title,
icon: Icon,
eyebrow,
subtitle,
cta,
children,
className = ''
}) => {
return (
<MainLayout>
<div className={`mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12 ${className}`}>
{/* Page Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
>
{eyebrow && (
<div className="flex items-center justify-center gap-2 mb-4">
{Icon && <Icon className="w-5 h-5 text-primary-600" />}
<span className="text-sm font-medium text-primary-600 uppercase tracking-wider">
{eyebrow}
</span>
</div>
)}
<h1 className="text-4xl md:text-6xl font-bold text-gray-900 dark:text-white mb-4">
{title}
</h1>
{subtitle && (
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto mb-8">
{subtitle}
</p>
)}
{cta && (
<div className="flex justify-center">
{cta}
</div>
)}
</motion.div>
{/* Page Content */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
{children}
</motion.div>
</div>
</MainLayout>
)
}
// Section header component
interface SectionHeaderProps {
eyebrow?: string
title: string
subtitle?: string
className?: string
}
export const SectionHeader: React.FC<SectionHeaderProps> = ({
eyebrow,
title,
subtitle,
className = ''
}) => {
return (
<div className={`text-center mb-12 ${className}`}>
{eyebrow && (
<div className="text-sm font-medium text-primary-600 uppercase tracking-wider mb-2">
{eyebrow}
</div>
)}
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{title}
</h2>
{subtitle && (
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
{subtitle}
</p>
)}
</div>
)
}
// Card component for consistent styling
interface CardProps {
children: ReactNode
className?: string
hover?: boolean
padding?: 'sm' | 'md' | 'lg'
}
export const Card: React.FC<CardProps> = ({
children,
className = '',
hover = false,
padding = 'md'
}) => {
const paddingClasses = {
sm: 'p-4',
md: 'p-6',
lg: 'p-8'
}
return (
<div
className={`
bg-white/70 dark:bg-white/5
backdrop-blur-sm
border border-white/30 dark:border-white/10
rounded-xl shadow-lg
${hover ? 'hover:shadow-xl transition-shadow duration-300' : ''}
${paddingClasses[padding]}
${className}
`}
>
{children}
</div>
)
import React, { ReactNode, useState } from 'react'
import { motion } from 'framer-motion'
import { LucideIcon } from 'lucide-react'
import { Navigation } from '../components/Navigation'
import { Footer } from '../components/Footer'
interface MainLayoutProps {
children: ReactNode
className?: string
darkMode?: boolean
setDarkMode?: (value: boolean) => void
}
interface PageShellProps {
title: string
icon?: LucideIcon
eyebrow?: string
subtitle?: string
cta?: ReactNode
children: ReactNode
className?: string
}
// Main layout wrapper with Navigation and Footer
export const MainLayout: React.FC<MainLayoutProps> = ({
children,
className = '',
darkMode = false,
setDarkMode = () => {}
}) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return (
<div className={`min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 dark:from-gray-900 dark:via-gray-800 dark:to-purple-900 ${className}`}>
<header className="sticky top-0 z-40 bg-white/80 backdrop-blur dark:bg-black/80">
<Navigation
darkMode={darkMode}
setDarkMode={setDarkMode}
mobileMenuOpen={mobileMenuOpen}
setMobileMenuOpen={setMobileMenuOpen}
/>
</header>
<main id="content">
{children}
</main>
<Footer />
</div>
)
}
// Page shell component for consistent page structure
export const PageShell: React.FC<PageShellProps> = ({
title,
icon: Icon,
eyebrow,
subtitle,
cta,
children,
className = ''
}) => {
return (
<MainLayout>
<div className={`mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12 ${className}`}>
{/* Page Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
>
{eyebrow && (
<div className="flex items-center justify-center gap-2 mb-4">
{Icon && <Icon className="w-5 h-5 text-primary-600" />}
<span className="text-sm font-medium text-primary-600 uppercase tracking-wider">
{eyebrow}
</span>
</div>
)}
<h1 className="text-4xl md:text-6xl font-bold text-gray-900 dark:text-white mb-4">
{title}
</h1>
{subtitle && (
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto mb-8">
{subtitle}
</p>
)}
{cta && (
<div className="flex justify-center">
{cta}
</div>
)}
</motion.div>
{/* Page Content */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
{children}
</motion.div>
</div>
</MainLayout>
)
}
// Section header component
interface SectionHeaderProps {
eyebrow?: string
title: string
subtitle?: string
className?: string
}
export const SectionHeader: React.FC<SectionHeaderProps> = ({
eyebrow,
title,
subtitle,
className = ''
}) => {
return (
<div className={`text-center mb-12 ${className}`}>
{eyebrow && (
<div className="text-sm font-medium text-primary-600 uppercase tracking-wider mb-2">
{eyebrow}
</div>
)}
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{title}
</h2>
{subtitle && (
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
{subtitle}
</p>
)}
</div>
)
}
// Card component for consistent styling
interface CardProps {
children: ReactNode
className?: string
hover?: boolean
padding?: 'sm' | 'md' | 'lg'
}
export const Card: React.FC<CardProps> = ({
children,
className = '',
hover = false,
padding = 'md'
}) => {
const paddingClasses = {
sm: 'p-4',
md: 'p-6',
lg: 'p-8'
}
return (
<div
className={`
bg-white/70 dark:bg-white/5
backdrop-blur-sm
border border-white/30 dark:border-white/10
rounded-xl shadow-lg
${hover ? 'hover:shadow-xl transition-shadow duration-300' : ''}
${paddingClasses[padding]}
${className}
`}
>
{children}
</div>
)
}
+13 -13
View File
@@ -1,14 +1,14 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { I18nextProvider } from 'react-i18next'
import i18n from './i18n/config'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<I18nextProvider i18n={i18n}>
<App />
</I18nextProvider>
</React.StrictMode>,
import React from 'react'
import ReactDOM from 'react-dom/client'
import { I18nextProvider } from 'react-i18next'
import i18n from './i18n/config'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<I18nextProvider i18n={i18n}>
<App />
</I18nextProvider>
</React.StrictMode>,
)
+368 -368
View File
@@ -1,369 +1,369 @@
import React, { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import { Heart, CreditCard, Shield, Lock, CheckCircle2 } from 'lucide-react'
import { PageShell, Card } from '@/layouts/MainLayout'
import { useDonationImpact } from '@/hooks/useDonationImpact'
import { useFormValidation } from '@/hooks/useDonationImpact'
import { formatCurrency } from '@/utils/helpers'
import { trackEvent, trackPageView } from '@/utils/analytics'
interface DonatePageProps {
className?: string
}
export const DonatePage: React.FC<DonatePageProps> = ({ className }) => {
const [selectedAmount, setSelectedAmount] = useState<number>(50)
const [customAmount, setCustomAmount] = useState<string>('')
const [isCustom, setIsCustom] = useState<boolean>(false)
const finalAmount = isCustom && customAmount ? parseInt(customAmount) || 0 : selectedAmount
const impact = useDonationImpact(finalAmount)
useEffect(() => {
trackPageView('/donate', 'Donate - Support Students in Need')
}, [])
const donationTiers = [
{ amount: 25, label: "Supplies for one student" },
{ amount: 50, label: "Fill a backpack with essentials" },
{ amount: 75, label: "Shoes + warm coat" },
{ amount: 100, label: "Complete school outfit & supplies" },
]
const handleAmountSelect = (amount: number): void => {
setSelectedAmount(amount)
setIsCustom(false)
setCustomAmount('')
trackEvent('donation_amount_selected', { amount, type: 'preset' })
}
const handleCustomAmount = (value: string): void => {
setCustomAmount(value)
setIsCustom(true)
if (parseInt(value)) {
trackEvent('donation_amount_selected', { amount: parseInt(value), type: 'custom' })
}
}
return (
<PageShell
title="Make a Difference Today"
icon={Heart}
eyebrow="Every dollar counts"
subtitle="Your donation directly supports students with school supplies, clothing, and emergency assistance."
className={className}
>
<div className="grid gap-8 lg:grid-cols-3">
<div className="lg:col-span-2 space-y-8">
{/* Donation Tiers */}
<Card>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
Choose Your Impact Level
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
{donationTiers.map((tier) => (
<button
key={tier.amount}
onClick={() => handleAmountSelect(tier.amount)}
className={`p-4 rounded-lg border-2 transition-all duration-200 text-left ${
selectedAmount === tier.amount && !isCustom
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-primary-300'
}`}
>
<div className="font-semibold text-lg text-gray-900 dark:text-white">
{formatCurrency(tier.amount)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-300">
{tier.label}
</div>
</button>
))}
</div>
<div className="border-t pt-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 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-gray-500">
$
</span>
<input
type="number"
placeholder="0"
value={customAmount}
onChange={(e) => handleCustomAmount(e.target.value)}
className="w-full pl-8 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
</div>
</Card>
{/* Donation Form */}
<DonationForm amount={finalAmount} />
</div>
<div className="space-y-6">
{/* Impact Calculator */}
<ImpactCalculator impact={impact} amount={finalAmount} />
{/* Trust Badges */}
<TrustBadges />
</div>
</div>
</PageShell>
)
}
// Impact Calculator Component
interface ImpactCalculatorProps {
impact: ReturnType<typeof useDonationImpact>
amount: number
}
const ImpactCalculator: React.FC<ImpactCalculatorProps> = ({ impact, amount }) => {
if (amount <= 0) return null
return (
<Card>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Heart className="w-5 h-5 text-primary-600" />
Your Impact
</h3>
<div className="text-2xl font-bold text-primary-600 mb-4">
{formatCurrency(amount)}
</div>
<div className="space-y-3">
{impact.students > 0 && (
<ImpactItem
count={impact.students}
label={`student${impact.students > 1 ? 's' : ''} with supplies`}
/>
)}
{impact.backpacks > 0 && (
<ImpactItem
count={impact.backpacks}
label={`backpack kit${impact.backpacks > 1 ? 's' : ''}`}
/>
)}
{impact.clothing > 0 && (
<ImpactItem
count={impact.clothing}
label={`clothing item${impact.clothing > 1 ? 's' : ''}`}
/>
)}
{impact.emergency > 0 && (
<ImpactItem
count={impact.emergency}
label={`emergency response${impact.emergency > 1 ? 's' : ''}`}
/>
)}
</div>
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Annual Impact:</strong> {impact.annual.totalImpact}
</div>
</div>
</Card>
)
}
const ImpactItem: React.FC<{ count: number; label: string }> = ({ count, label }) => (
<div className="flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-green-500" />
<span className="text-sm">
<strong>{count}</strong> {label}
</span>
</div>
)
// Donation Form Component
interface DonationFormProps {
amount: number
}
const DonationForm: React.FC<DonationFormProps> = ({ amount }) => {
const [paymentMethod, setPaymentMethod] = useState<'card' | 'paypal'>('card')
const formValidation = useFormValidation(
{
email: '',
firstName: '',
lastName: '',
phone: ''
},
{
email: (value) => {
if (!value) return 'Email is required'
if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format'
return null
},
firstName: (value) => !value ? 'First name is required' : null,
lastName: (value) => !value ? 'Last name is required' : null,
phone: (value) => {
if (!value) return null // Optional
if (!/^\+?[\d\s\-\(\)]{10,}$/.test(value)) return 'Invalid phone format'
return null
}
}
)
const handleSubmit = (e: React.FormEvent): void => {
e.preventDefault()
if (formValidation.validate()) {
trackEvent('donation_form_submitted', {
amount,
payment_method: paymentMethod,
form_valid: true
})
// Process donation
console.log('Processing donation:', { amount, paymentMethod, ...formValidation.values })
} else {
trackEvent('donation_form_error', {
amount,
errors: Object.keys(formValidation.errors)
})
}
}
return (
<Card>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
Complete Your Donation
</h3>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Contact Information */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
First Name *
</label>
<input
type="text"
value={formValidation.values.firstName}
onChange={(e) => formValidation.setValue('firstName', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800"
/>
{formValidation.errors.firstName && (
<p className="text-red-500 text-sm mt-1">{formValidation.errors.firstName}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Last Name *
</label>
<input
type="text"
value={formValidation.values.lastName}
onChange={(e) => formValidation.setValue('lastName', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800"
/>
{formValidation.errors.lastName && (
<p className="text-red-500 text-sm mt-1">{formValidation.errors.lastName}</p>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email Address *
</label>
<input
type="email"
value={formValidation.values.email}
onChange={(e) => formValidation.setValue('email', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800"
/>
{formValidation.errors.email && (
<p className="text-red-500 text-sm mt-1">{formValidation.errors.email}</p>
)}
</div>
{/* Payment Method */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Payment Method
</label>
<div className="grid grid-cols-2 gap-4">
<button
type="button"
onClick={() => setPaymentMethod('card')}
className={`p-3 rounded-lg border-2 flex items-center justify-center gap-2 transition-colors ${
paymentMethod === 'card'
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200 dark:border-gray-700'
}`}
>
<CreditCard className="w-5 h-5" />
Credit Card
</button>
<button
type="button"
onClick={() => setPaymentMethod('paypal')}
className={`p-3 rounded-lg border-2 flex items-center justify-center gap-2 transition-colors ${
paymentMethod === 'paypal'
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200 dark:border-gray-700'
}`}
>
PayPal
</button>
</div>
</div>
{/* Submit Button */}
<motion.button
type="submit"
disabled={amount <= 0}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="w-full btn-primary py-4 text-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
Donate {formatCurrency(amount)}
</motion.button>
</form>
</Card>
)
}
// Trust Badges Component
const TrustBadges: React.FC = () => (
<Card>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Secure & Trusted
</h3>
<div className="space-y-4">
<div className="flex items-center gap-3">
<Shield className="w-5 h-5 text-green-500" />
<span className="text-sm">SSL Encrypted Payments</span>
</div>
<div className="flex items-center gap-3">
<Lock className="w-5 h-5 text-green-500" />
<span className="text-sm">501(c)3 Tax Deductible</span>
</div>
<div className="flex items-center gap-3">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<span className="text-sm">100% Goes to Students</span>
</div>
</div>
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
<p className="text-xs text-gray-500">
EIN: 12-3456789 • All donations are tax-deductible to the fullest extent allowed by law.
</p>
</div>
</Card>
import React, { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import { Heart, CreditCard, Shield, Lock, CheckCircle2 } from 'lucide-react'
import { PageShell, Card } from '@/layouts/MainLayout'
import { useDonationImpact } from '@/hooks/useDonationImpact'
import { useFormValidation } from '@/hooks/useDonationImpact'
import { formatCurrency } from '@/utils/helpers'
import { trackEvent, trackPageView } from '@/utils/analytics'
interface DonatePageProps {
className?: string
}
export const DonatePage: React.FC<DonatePageProps> = ({ className }) => {
const [selectedAmount, setSelectedAmount] = useState<number>(50)
const [customAmount, setCustomAmount] = useState<string>('')
const [isCustom, setIsCustom] = useState<boolean>(false)
const finalAmount = isCustom && customAmount ? parseInt(customAmount) || 0 : selectedAmount
const impact = useDonationImpact(finalAmount)
useEffect(() => {
trackPageView('/donate', 'Donate - Support Students in Need')
}, [])
const donationTiers = [
{ amount: 25, label: "Supplies for one student" },
{ amount: 50, label: "Fill a backpack with essentials" },
{ amount: 75, label: "Shoes + warm coat" },
{ amount: 100, label: "Complete school outfit & supplies" },
]
const handleAmountSelect = (amount: number): void => {
setSelectedAmount(amount)
setIsCustom(false)
setCustomAmount('')
trackEvent('donation_amount_selected', { amount, type: 'preset' })
}
const handleCustomAmount = (value: string): void => {
setCustomAmount(value)
setIsCustom(true)
if (parseInt(value)) {
trackEvent('donation_amount_selected', { amount: parseInt(value), type: 'custom' })
}
}
return (
<PageShell
title="Make a Difference Today"
icon={Heart}
eyebrow="Every dollar counts"
subtitle="Your donation directly supports students with school supplies, clothing, and emergency assistance."
className={className}
>
<div className="grid gap-8 lg:grid-cols-3">
<div className="lg:col-span-2 space-y-8">
{/* Donation Tiers */}
<Card>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
Choose Your Impact Level
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
{donationTiers.map((tier) => (
<button
key={tier.amount}
onClick={() => handleAmountSelect(tier.amount)}
className={`p-4 rounded-lg border-2 transition-all duration-200 text-left ${
selectedAmount === tier.amount && !isCustom
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-primary-300'
}`}
>
<div className="font-semibold text-lg text-gray-900 dark:text-white">
{formatCurrency(tier.amount)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-300">
{tier.label}
</div>
</button>
))}
</div>
<div className="border-t pt-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 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-gray-500">
$
</span>
<input
type="number"
placeholder="0"
value={customAmount}
onChange={(e) => handleCustomAmount(e.target.value)}
className="w-full pl-8 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
</div>
</Card>
{/* Donation Form */}
<DonationForm amount={finalAmount} />
</div>
<div className="space-y-6">
{/* Impact Calculator */}
<ImpactCalculator impact={impact} amount={finalAmount} />
{/* Trust Badges */}
<TrustBadges />
</div>
</div>
</PageShell>
)
}
// Impact Calculator Component
interface ImpactCalculatorProps {
impact: ReturnType<typeof useDonationImpact>
amount: number
}
const ImpactCalculator: React.FC<ImpactCalculatorProps> = ({ impact, amount }) => {
if (amount <= 0) return null
return (
<Card>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Heart className="w-5 h-5 text-primary-600" />
Your Impact
</h3>
<div className="text-2xl font-bold text-primary-600 mb-4">
{formatCurrency(amount)}
</div>
<div className="space-y-3">
{impact.students > 0 && (
<ImpactItem
count={impact.students}
label={`student${impact.students > 1 ? 's' : ''} with supplies`}
/>
)}
{impact.backpacks > 0 && (
<ImpactItem
count={impact.backpacks}
label={`backpack kit${impact.backpacks > 1 ? 's' : ''}`}
/>
)}
{impact.clothing > 0 && (
<ImpactItem
count={impact.clothing}
label={`clothing item${impact.clothing > 1 ? 's' : ''}`}
/>
)}
{impact.emergency > 0 && (
<ImpactItem
count={impact.emergency}
label={`emergency response${impact.emergency > 1 ? 's' : ''}`}
/>
)}
</div>
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Annual Impact:</strong> {impact.annual.totalImpact}
</div>
</div>
</Card>
)
}
const ImpactItem: React.FC<{ count: number; label: string }> = ({ count, label }) => (
<div className="flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-green-500" />
<span className="text-sm">
<strong>{count}</strong> {label}
</span>
</div>
)
// Donation Form Component
interface DonationFormProps {
amount: number
}
const DonationForm: React.FC<DonationFormProps> = ({ amount }) => {
const [paymentMethod, setPaymentMethod] = useState<'card' | 'paypal'>('card')
const formValidation = useFormValidation(
{
email: '',
firstName: '',
lastName: '',
phone: ''
},
{
email: (value) => {
if (!value) return 'Email is required'
if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format'
return null
},
firstName: (value) => !value ? 'First name is required' : null,
lastName: (value) => !value ? 'Last name is required' : null,
phone: (value) => {
if (!value) return null // Optional
if (!/^\+?[\d\s\-\(\)]{10,}$/.test(value)) return 'Invalid phone format'
return null
}
}
)
const handleSubmit = (e: React.FormEvent): void => {
e.preventDefault()
if (formValidation.validate()) {
trackEvent('donation_form_submitted', {
amount,
payment_method: paymentMethod,
form_valid: true
})
// Process donation
console.log('Processing donation:', { amount, paymentMethod, ...formValidation.values })
} else {
trackEvent('donation_form_error', {
amount,
errors: Object.keys(formValidation.errors)
})
}
}
return (
<Card>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
Complete Your Donation
</h3>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Contact Information */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
First Name *
</label>
<input
type="text"
value={formValidation.values.firstName}
onChange={(e) => formValidation.setValue('firstName', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800"
/>
{formValidation.errors.firstName && (
<p className="text-red-500 text-sm mt-1">{formValidation.errors.firstName}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Last Name *
</label>
<input
type="text"
value={formValidation.values.lastName}
onChange={(e) => formValidation.setValue('lastName', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800"
/>
{formValidation.errors.lastName && (
<p className="text-red-500 text-sm mt-1">{formValidation.errors.lastName}</p>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email Address *
</label>
<input
type="email"
value={formValidation.values.email}
onChange={(e) => formValidation.setValue('email', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800"
/>
{formValidation.errors.email && (
<p className="text-red-500 text-sm mt-1">{formValidation.errors.email}</p>
)}
</div>
{/* Payment Method */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Payment Method
</label>
<div className="grid grid-cols-2 gap-4">
<button
type="button"
onClick={() => setPaymentMethod('card')}
className={`p-3 rounded-lg border-2 flex items-center justify-center gap-2 transition-colors ${
paymentMethod === 'card'
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200 dark:border-gray-700'
}`}
>
<CreditCard className="w-5 h-5" />
Credit Card
</button>
<button
type="button"
onClick={() => setPaymentMethod('paypal')}
className={`p-3 rounded-lg border-2 flex items-center justify-center gap-2 transition-colors ${
paymentMethod === 'paypal'
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200 dark:border-gray-700'
}`}
>
PayPal
</button>
</div>
</div>
{/* Submit Button */}
<motion.button
type="submit"
disabled={amount <= 0}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="w-full btn-primary py-4 text-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
Donate {formatCurrency(amount)}
</motion.button>
</form>
</Card>
)
}
// Trust Badges Component
const TrustBadges: React.FC = () => (
<Card>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Secure & Trusted
</h3>
<div className="space-y-4">
<div className="flex items-center gap-3">
<Shield className="w-5 h-5 text-green-500" />
<span className="text-sm">SSL Encrypted Payments</span>
</div>
<div className="flex items-center gap-3">
<Lock className="w-5 h-5 text-green-500" />
<span className="text-sm">501(c)3 Tax Deductible</span>
</div>
<div className="flex items-center gap-3">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<span className="text-sm">100% Goes to Students</span>
</div>
</div>
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
<p className="text-xs text-gray-500">
EIN: 12-3456789 • All donations are tax-deductible to the fullest extent allowed by law.
</p>
</div>
</Card>
)
+203 -203
View File
@@ -1,204 +1,204 @@
import React from 'react'
import { motion } from 'framer-motion'
import { Heart, Globe, ArrowRight, CheckCircle2 } from 'lucide-react'
import { MainLayout, SectionHeader, Card } from '@/layouts/MainLayout'
import { trackEvent, trackPageView } from '@/utils/analytics'
export const HomePage: React.FC = () => {
React.useEffect(() => {
trackPageView('/', 'Home - Miracles In Motion')
}, [])
const handleDonateClick = (): void => {
trackEvent('cta_clicked', { button: 'hero_donate', location: 'homepage' })
window.location.hash = '/donate'
}
const handleVolunteerClick = (): void => {
trackEvent('cta_clicked', { button: 'hero_volunteer', location: 'homepage' })
window.location.hash = '/volunteer'
}
return (
<MainLayout>
{/* Hero Section */}
<section className="relative overflow-hidden">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-20">
<div className="text-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<div className="flex items-center justify-center gap-2 mb-6">
<Heart className="w-8 h-8 text-primary-600" />
<span className="text-lg font-medium text-primary-600 uppercase tracking-wider">
501(c)3 Non-Profit Organization
</span>
</div>
<h1 className="text-5xl md:text-7xl font-bold text-gray-900 dark:text-white mb-6">
Miracles in <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-pink-600">Motion</span>
</h1>
<p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto mb-12 leading-relaxed">
Empowering students with essential supplies, clothing, and support to succeed in school and life.
Every child deserves the tools they need to learn and grow.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<button
onClick={handleDonateClick}
className="btn-primary text-lg px-8 py-4 group"
>
Donate Now
<Heart className="w-5 h-5 ml-2 group-hover:scale-110 transition-transform" />
</button>
<button
onClick={handleVolunteerClick}
className="btn-secondary text-lg px-8 py-4 group"
>
Volunteer Today
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
</button>
</div>
</motion.div>
</div>
</div>
{/* Background Elements */}
<div className="absolute inset-0 -z-10">
<div className="absolute top-1/4 left-1/4 w-64 h-64 bg-primary-200/30 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-pink-200/30 rounded-full blur-3xl" />
</div>
</section>
{/* Impact Stats */}
<section className="py-16 bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionHeader
eyebrow="Our Impact"
title="Making a Real Difference"
subtitle="Transparent, measurable outcomes powered by community partnerships"
/>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6">
<ImpactStat label="Students Helped" value="4,200+" />
<ImpactStat label="Schools Partnered" value="38" />
<ImpactStat label="Avg Response Time" value="24 hrs" />
<ImpactStat label="Counties Served" value="6" />
</div>
</div>
</section>
{/* What We Do */}
<section className="py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionHeader
eyebrow="What We Do"
title="Supporting Student Success"
subtitle="Comprehensive support to remove barriers and create opportunities"
/>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<ServiceCard
icon={<CheckCircle2 className="w-8 h-8 text-primary-600" />}
title="School Supplies"
description="Backpacks, notebooks, pencils, and all the essentials students need to succeed in the classroom."
/>
<ServiceCard
icon={<Heart className="w-8 h-8 text-primary-600" />}
title="Clothing & Shoes"
description="Weather-appropriate clothing and sturdy shoes so students can attend school with confidence."
/>
<ServiceCard
icon={<Globe className="w-8 h-8 text-primary-600" />}
title="Emergency Support"
description="Rapid response assistance for urgent needs including food, transportation, and crisis support."
/>
</div>
</div>
</section>
{/* Call to Action */}
<section className="py-20 bg-gradient-to-r from-primary-600 to-pink-600">
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
Ready to Make a Difference?
</h2>
<p className="text-xl text-white/90 mb-8">
Join our community of supporters helping students succeed. Every contribution creates ripples of positive change.
</p>
<button
onClick={handleDonateClick}
className="bg-white text-primary-600 hover:bg-gray-50 font-semibold px-8 py-4 rounded-lg transition-colors"
>
Start Supporting Students Today
</button>
</motion.div>
</div>
</section>
</MainLayout>
)
}
// Impact Stat Component
interface ImpactStatProps {
label: string
value: string
}
const ImpactStat: React.FC<ImpactStatProps> = ({ label, value }) => (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
className="text-center"
>
<Card className="h-full">
<div className="text-3xl md:text-4xl font-bold text-primary-600 mb-2">
{value}
</div>
<div className="text-gray-600 dark:text-gray-300 font-medium">
{label}
</div>
</Card>
</motion.div>
)
// Service Card Component
interface ServiceCardProps {
icon: React.ReactNode
title: string
description: string
}
const ServiceCard: React.FC<ServiceCardProps> = ({ icon, title, description }) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
whileHover={{ y: -5 }}
transition={{ duration: 0.3 }}
>
<Card className="h-full text-center hover:shadow-xl transition-shadow duration-300">
<div className="flex justify-center mb-4">
{icon}
</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
{title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{description}
</p>
</Card>
</motion.div>
import React from 'react'
import { motion } from 'framer-motion'
import { Heart, Globe, ArrowRight, CheckCircle2 } from 'lucide-react'
import { MainLayout, SectionHeader, Card } from '@/layouts/MainLayout'
import { trackEvent, trackPageView } from '@/utils/analytics'
export const HomePage: React.FC = () => {
React.useEffect(() => {
trackPageView('/', 'Home - Miracles In Motion')
}, [])
const handleDonateClick = (): void => {
trackEvent('cta_clicked', { button: 'hero_donate', location: 'homepage' })
window.location.hash = '/donate'
}
const handleVolunteerClick = (): void => {
trackEvent('cta_clicked', { button: 'hero_volunteer', location: 'homepage' })
window.location.hash = '/volunteer'
}
return (
<MainLayout>
{/* Hero Section */}
<section className="relative overflow-hidden">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-20">
<div className="text-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<div className="flex items-center justify-center gap-2 mb-6">
<Heart className="w-8 h-8 text-primary-600" />
<span className="text-lg font-medium text-primary-600 uppercase tracking-wider">
501(c)3 Non-Profit Organization
</span>
</div>
<h1 className="text-5xl md:text-7xl font-bold text-gray-900 dark:text-white mb-6">
Miracles in <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-pink-600">Motion</span>
</h1>
<p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto mb-12 leading-relaxed">
Empowering students with essential supplies, clothing, and support to succeed in school and life.
Every child deserves the tools they need to learn and grow.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<button
onClick={handleDonateClick}
className="btn-primary text-lg px-8 py-4 group"
>
Donate Now
<Heart className="w-5 h-5 ml-2 group-hover:scale-110 transition-transform" />
</button>
<button
onClick={handleVolunteerClick}
className="btn-secondary text-lg px-8 py-4 group"
>
Volunteer Today
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
</button>
</div>
</motion.div>
</div>
</div>
{/* Background Elements */}
<div className="absolute inset-0 -z-10">
<div className="absolute top-1/4 left-1/4 w-64 h-64 bg-primary-200/30 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-pink-200/30 rounded-full blur-3xl" />
</div>
</section>
{/* Impact Stats */}
<section className="py-16 bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionHeader
eyebrow="Our Impact"
title="Making a Real Difference"
subtitle="Transparent, measurable outcomes powered by community partnerships"
/>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6">
<ImpactStat label="Students Helped" value="4,200+" />
<ImpactStat label="Schools Partnered" value="38" />
<ImpactStat label="Avg Response Time" value="24 hrs" />
<ImpactStat label="Counties Served" value="6" />
</div>
</div>
</section>
{/* What We Do */}
<section className="py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<SectionHeader
eyebrow="What We Do"
title="Supporting Student Success"
subtitle="Comprehensive support to remove barriers and create opportunities"
/>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<ServiceCard
icon={<CheckCircle2 className="w-8 h-8 text-primary-600" />}
title="School Supplies"
description="Backpacks, notebooks, pencils, and all the essentials students need to succeed in the classroom."
/>
<ServiceCard
icon={<Heart className="w-8 h-8 text-primary-600" />}
title="Clothing & Shoes"
description="Weather-appropriate clothing and sturdy shoes so students can attend school with confidence."
/>
<ServiceCard
icon={<Globe className="w-8 h-8 text-primary-600" />}
title="Emergency Support"
description="Rapid response assistance for urgent needs including food, transportation, and crisis support."
/>
</div>
</div>
</section>
{/* Call to Action */}
<section className="py-20 bg-gradient-to-r from-primary-600 to-pink-600">
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
Ready to Make a Difference?
</h2>
<p className="text-xl text-white/90 mb-8">
Join our community of supporters helping students succeed. Every contribution creates ripples of positive change.
</p>
<button
onClick={handleDonateClick}
className="bg-white text-primary-600 hover:bg-gray-50 font-semibold px-8 py-4 rounded-lg transition-colors"
>
Start Supporting Students Today
</button>
</motion.div>
</div>
</section>
</MainLayout>
)
}
// Impact Stat Component
interface ImpactStatProps {
label: string
value: string
}
const ImpactStat: React.FC<ImpactStatProps> = ({ label, value }) => (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
className="text-center"
>
<Card className="h-full">
<div className="text-3xl md:text-4xl font-bold text-primary-600 mb-2">
{value}
</div>
<div className="text-gray-600 dark:text-gray-300 font-medium">
{label}
</div>
</Card>
</motion.div>
)
// Service Card Component
interface ServiceCardProps {
icon: React.ReactNode
title: string
description: string
}
const ServiceCard: React.FC<ServiceCardProps> = ({ icon, title, description }) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
whileHover={{ y: -5 }}
transition={{ duration: 0.3 }}
>
<Card className="h-full text-center hover:shadow-xl transition-shadow duration-300">
<div className="flex justify-center mb-4">
{icon}
</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
{title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{description}
</p>
</Card>
</motion.div>
)
+479 -479
View File
@@ -1,480 +1,480 @@
// Phase 3B: Browser-Compatible Real-Time Processing System
import type { StudentRequest, MatchResult, AIUpdate } from '../ai/types'
// Browser-compatible AI processor
class BrowserAIProcessor {
async processRequest(request: StudentRequest): Promise<MatchResult[]> {
// Mock implementation for browser
console.log('Processing request in browser:', request.id)
await new Promise(resolve => setTimeout(resolve, 1000))
return [{
resourceId: 'browser-resource-1',
resourceName: 'Browser Mock Resource',
resourceType: 'supplies',
confidenceScore: 0.75,
estimatedImpact: 7.5,
logisticalComplexity: 2.0,
estimatedCost: 45,
fulfillmentTimeline: '2-4 days',
reasoningFactors: ['Browser-based processing', 'Mock data for demo'],
riskFactors: ['Demo mode - not real matching']
}]
}
}
interface RealTimeConfig {
enableWebSockets: boolean
batchSize: number
processingInterval: number
maxConcurrentRequests: number
enablePredictiveLoading: boolean
cacheTimeout: number
enableOfflineMode: boolean
}
interface ProcessingMetrics {
totalProcessed: number
averageProcessingTime: number
successRate: number
errorCount: number
queueLength: number
activeProcessors: number
throughputPerMinute: number
lastProcessedAt: Date
}
interface DataSyncStatus {
salesforceSync: boolean
databaseSync: boolean
cacheSync: boolean
aiModelSync: boolean
lastSyncAt: Date
pendingSyncCount: number
}
class RealTimeProcessor {
private ai: BrowserAIProcessor
private salesforce?: any // Mock for browser demo
private config: RealTimeConfig
private processingQueue: StudentRequest[] = []
private activeProcessors = new Map<string, Promise<MatchResult[]>>()
private metrics: ProcessingMetrics
private syncStatus: DataSyncStatus
private subscribers: ((update: AIUpdate) => void)[] = []
private websocket?: WebSocket
private processTimer?: number
private isProcessing = false
constructor(config: RealTimeConfig, salesforceConfig?: any) {
this.config = config
this.ai = new BrowserAIProcessor()
if (salesforceConfig) {
this.salesforce = { mock: true } // Mock for browser demo
}
this.metrics = {
totalProcessed: 0,
averageProcessingTime: 0,
successRate: 0,
errorCount: 0,
queueLength: 0,
activeProcessors: 0,
throughputPerMinute: 0,
lastProcessedAt: new Date()
}
this.syncStatus = {
salesforceSync: false,
databaseSync: false,
cacheSync: false,
aiModelSync: false,
lastSyncAt: new Date(),
pendingSyncCount: 0
}
this.initialize()
}
private async initialize(): Promise<void> {
try {
// Initializing Real-Time Processing System
// Initialize AI engine
// AI Engine ready
// Initialize Salesforce connection
if (this.salesforce) {
const connected = await this.salesforce.authenticate()
this.syncStatus.salesforceSync = connected
console.log(connected ? '✅ Salesforce connected' : '⚠️ Salesforce connection failed')
}
// Setup WebSocket for real-time updates
if (this.config.enableWebSockets) {
this.setupWebSocket()
}
// Start processing timer
this.startProcessingTimer()
console.log('🎯 Real-Time Processing System Online')
} catch (error) {
// Failed to initialize real-time processor - error handled
}
}
private setupWebSocket(): void {
try {
const wsUrl = process.env.NODE_ENV === 'production'
? 'wss://miracles-in-motion.org/websocket'
: 'ws://localhost:8080/websocket'
this.websocket = new WebSocket(wsUrl)
this.websocket.onopen = () => {
// WebSocket connected for real-time updates
this.broadcastUpdate({
type: 'model-updated',
message: 'Real-time processing system online',
timestamp: new Date()
})
}
this.websocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
this.handleWebSocketMessage(data)
} catch (error) {
console.error('Error parsing WebSocket message:', error)
}
}
this.websocket.onclose = () => {
console.log('🔌 WebSocket disconnected, attempting reconnection...')
setTimeout(() => this.setupWebSocket(), 5000)
}
this.websocket.onerror = (error) => {
console.error('WebSocket error:', error)
}
} catch (error) {
console.error('Failed to setup WebSocket:', error)
}
}
private handleWebSocketMessage(data: any): void {
switch (data.type) {
case 'new-request':
this.addToQueue(data.request)
break
case 'priority-update':
this.updateRequestPriority(data.requestId, data.priority)
break
case 'system-status':
this.handleSystemStatusUpdate(data)
break
}
}
private startProcessingTimer(): void {
this.processTimer = window.setInterval(() => {
if (!this.isProcessing && this.processingQueue.length > 0) {
this.processNextBatch()
}
this.updateMetrics()
}, this.config.processingInterval)
}
// Public API Methods
public addToQueue(request: StudentRequest): void {
this.processingQueue.push(request)
this.metrics.queueLength = this.processingQueue.length
this.broadcastUpdate({
type: 'request-processed',
requestId: request.id,
studentName: request.studentName,
status: 'queued',
message: `Request added to processing queue (${this.metrics.queueLength} pending)`,
timestamp: new Date()
})
// Trigger immediate processing if queue was empty
if (this.processingQueue.length === 1 && !this.isProcessing) {
setTimeout(() => this.processNextBatch(), 100)
}
}
public async processNextBatch(): Promise<void> {
if (this.isProcessing || this.processingQueue.length === 0) {
return
}
this.isProcessing = true
const batchSize = Math.min(this.config.batchSize, this.processingQueue.length)
const batch = this.processingQueue.splice(0, batchSize)
console.log(`📊 Processing batch of ${batch.length} requests...`)
const processingPromises = batch.map(request => this.processSingleRequest(request))
try {
const results = await Promise.allSettled(processingPromises)
let successCount = 0
let errorCount = 0
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successCount++
this.handleProcessingSuccess(batch[index], result.value)
} else {
errorCount++
this.handleProcessingError(batch[index], result.reason)
}
})
// Update metrics
this.metrics.totalProcessed += batch.length
this.metrics.errorCount += errorCount
this.metrics.successRate = (this.metrics.totalProcessed - this.metrics.errorCount) / this.metrics.totalProcessed
this.metrics.lastProcessedAt = new Date()
console.log(`✅ Batch complete: ${successCount} success, ${errorCount} errors`)
} catch (error) {
console.error('Batch processing error:', error)
} finally {
this.isProcessing = false
this.metrics.queueLength = this.processingQueue.length
}
}
private async processSingleRequest(request: StudentRequest): Promise<MatchResult[]> {
const startTime = Date.now()
try {
// Process with AI
const matches = await this.ai.processRequest(request)
// Create Salesforce case if connected
if (this.salesforce && matches.length > 0) {
const caseId = await this.salesforce.createAssistanceCase(request)
if (caseId) {
await this.salesforce.updateCaseWithMatching(caseId, matches[0])
}
}
const processingTime = Date.now() - startTime
this.updateAverageProcessingTime(processingTime)
return matches
} catch (error) {
console.error(`Error processing request ${request.id}:`, error)
throw error
}
}
private handleProcessingSuccess(request: StudentRequest, matches: MatchResult[]): void {
this.broadcastUpdate({
type: 'request-processed',
requestId: request.id,
studentName: request.studentName,
status: 'completed',
recommendations: matches,
message: `Found ${matches.length} potential matches`,
timestamp: new Date()
})
// Auto-approve high confidence matches
const highConfidenceMatches = matches.filter(match => match.confidenceScore >= 0.85)
if (highConfidenceMatches.length > 0) {
this.broadcastUpdate({
type: 'auto-approval',
requestId: request.id,
studentName: request.studentName,
message: `Auto-approved ${highConfidenceMatches.length} high-confidence matches`,
timestamp: new Date()
})
}
}
private handleProcessingError(request: StudentRequest, error: any): void {
console.error(`Processing failed for ${request.id}:`, error)
this.broadcastUpdate({
type: 'alert',
requestId: request.id,
studentName: request.studentName,
message: `Processing failed: ${error.message || 'Unknown error'}`,
timestamp: new Date()
})
// Re-queue with lower priority if retryable
if (this.isRetryableError(error)) {
setTimeout(() => {
this.processingQueue.push({ ...request, urgency: 'low' })
}, 5000)
}
}
private isRetryableError(error: any): boolean {
// Define retryable error conditions
return error.code === 'NETWORK_ERROR' ||
error.code === 'RATE_LIMIT' ||
error.message?.includes('timeout')
}
private updateAverageProcessingTime(newTime: number): void {
const totalProcessed = this.metrics.totalProcessed
const currentAverage = this.metrics.averageProcessingTime
this.metrics.averageProcessingTime = ((currentAverage * totalProcessed) + newTime) / (totalProcessed + 1)
}
private updateMetrics(): void {
const now = Date.now()
// Calculate throughput (simplified)
this.metrics.throughputPerMinute = this.metrics.totalProcessed > 0 ?
Math.round(this.metrics.totalProcessed / ((now - this.metrics.lastProcessedAt.getTime()) / 60000)) : 0
this.metrics.activeProcessors = this.activeProcessors.size
}
private updateRequestPriority(requestId: string, newPriority: string): void {
const requestIndex = this.processingQueue.findIndex(req => req.id === requestId)
if (requestIndex !== -1) {
this.processingQueue[requestIndex].urgency = newPriority as any
// Re-sort queue by priority
this.processingQueue.sort((a, b) => {
const priorityOrder = { emergency: 4, high: 3, medium: 2, low: 1 }
return priorityOrder[b.urgency] - priorityOrder[a.urgency]
})
}
}
private handleSystemStatusUpdate(data: any): void {
// Update sync status based on external system updates
if (data.salesforce !== undefined) {
this.syncStatus.salesforceSync = data.salesforce
}
if (data.database !== undefined) {
this.syncStatus.databaseSync = data.database
}
}
// Subscription methods for real-time updates
public subscribe(callback: (update: AIUpdate) => void): () => void {
this.subscribers.push(callback)
// Return unsubscribe function
return () => {
const index = this.subscribers.indexOf(callback)
if (index > -1) {
this.subscribers.splice(index, 1)
}
}
}
private broadcastUpdate(update: AIUpdate): void {
this.subscribers.forEach(callback => {
try {
callback(update)
} catch (error) {
console.error('Error in subscriber callback:', error)
}
})
// Send to WebSocket if connected
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send(JSON.stringify(update))
}
}
// Status and metrics getters
public getMetrics(): ProcessingMetrics {
return { ...this.metrics }
}
public getSyncStatus(): DataSyncStatus {
return { ...this.syncStatus }
}
public getQueueStatus(): { length: number; processing: boolean; nextEstimatedTime?: number } {
return {
length: this.processingQueue.length,
processing: this.isProcessing,
nextEstimatedTime: this.processingQueue.length > 0 ?
this.metrics.averageProcessingTime * Math.ceil(this.processingQueue.length / this.config.batchSize) : undefined
}
}
// Lifecycle management
public pause(): void {
if (this.processTimer) {
clearInterval(this.processTimer)
this.processTimer = undefined
}
console.log('⏸️ Real-time processing paused')
}
public resume(): void {
if (!this.processTimer) {
this.startProcessingTimer()
console.log('▶️ Real-time processing resumed')
}
}
public async shutdown(): Promise<void> {
console.log('🛑 Shutting down real-time processor...')
this.pause()
// Wait for active processors to complete
if (this.activeProcessors.size > 0) {
console.log(`⏳ Waiting for ${this.activeProcessors.size} active processors...`)
await Promise.all(this.activeProcessors.values())
}
// Close WebSocket
if (this.websocket) {
this.websocket.close()
}
console.log('✅ Real-time processor shutdown complete')
}
}
// Factory function for easy initialization
export const createRealTimeProcessor = (config: Partial<RealTimeConfig> = {}): RealTimeProcessor => {
const defaultConfig: RealTimeConfig = {
enableWebSockets: true,
batchSize: 5,
processingInterval: 2000,
maxConcurrentRequests: 10,
enablePredictiveLoading: true,
cacheTimeout: 300000, // 5 minutes
enableOfflineMode: true
}
const finalConfig = { ...defaultConfig, ...config }
// Salesforce config from environment variables
const salesforceConfig = process.env.NODE_ENV === 'production' ? {
instanceUrl: process.env.REACT_APP_SALESFORCE_URL || '',
clientId: process.env.REACT_APP_SALESFORCE_CLIENT_ID || '',
clientSecret: process.env.REACT_APP_SALESFORCE_CLIENT_SECRET || '',
username: process.env.REACT_APP_SALESFORCE_USERNAME || '',
password: process.env.REACT_APP_SALESFORCE_PASSWORD || '',
securityToken: process.env.REACT_APP_SALESFORCE_TOKEN || '',
apiVersion: '58.0'
} : undefined
return new RealTimeProcessor(finalConfig, salesforceConfig)
}
export { RealTimeProcessor }
// Phase 3B: Browser-Compatible Real-Time Processing System
import type { StudentRequest, MatchResult, AIUpdate } from '../ai/types'
// Browser-compatible AI processor
class BrowserAIProcessor {
async processRequest(request: StudentRequest): Promise<MatchResult[]> {
// Mock implementation for browser
console.log('Processing request in browser:', request.id)
await new Promise(resolve => setTimeout(resolve, 1000))
return [{
resourceId: 'browser-resource-1',
resourceName: 'Browser Mock Resource',
resourceType: 'supplies',
confidenceScore: 0.75,
estimatedImpact: 7.5,
logisticalComplexity: 2.0,
estimatedCost: 45,
fulfillmentTimeline: '2-4 days',
reasoningFactors: ['Browser-based processing', 'Mock data for demo'],
riskFactors: ['Demo mode - not real matching']
}]
}
}
interface RealTimeConfig {
enableWebSockets: boolean
batchSize: number
processingInterval: number
maxConcurrentRequests: number
enablePredictiveLoading: boolean
cacheTimeout: number
enableOfflineMode: boolean
}
interface ProcessingMetrics {
totalProcessed: number
averageProcessingTime: number
successRate: number
errorCount: number
queueLength: number
activeProcessors: number
throughputPerMinute: number
lastProcessedAt: Date
}
interface DataSyncStatus {
salesforceSync: boolean
databaseSync: boolean
cacheSync: boolean
aiModelSync: boolean
lastSyncAt: Date
pendingSyncCount: number
}
class RealTimeProcessor {
private ai: BrowserAIProcessor
private salesforce?: any // Mock for browser demo
private config: RealTimeConfig
private processingQueue: StudentRequest[] = []
private activeProcessors = new Map<string, Promise<MatchResult[]>>()
private metrics: ProcessingMetrics
private syncStatus: DataSyncStatus
private subscribers: ((update: AIUpdate) => void)[] = []
private websocket?: WebSocket
private processTimer?: number
private isProcessing = false
constructor(config: RealTimeConfig, salesforceConfig?: any) {
this.config = config
this.ai = new BrowserAIProcessor()
if (salesforceConfig) {
this.salesforce = { mock: true } // Mock for browser demo
}
this.metrics = {
totalProcessed: 0,
averageProcessingTime: 0,
successRate: 0,
errorCount: 0,
queueLength: 0,
activeProcessors: 0,
throughputPerMinute: 0,
lastProcessedAt: new Date()
}
this.syncStatus = {
salesforceSync: false,
databaseSync: false,
cacheSync: false,
aiModelSync: false,
lastSyncAt: new Date(),
pendingSyncCount: 0
}
this.initialize()
}
private async initialize(): Promise<void> {
try {
// Initializing Real-Time Processing System
// Initialize AI engine
// AI Engine ready
// Initialize Salesforce connection
if (this.salesforce) {
const connected = await this.salesforce.authenticate()
this.syncStatus.salesforceSync = connected
console.log(connected ? '✅ Salesforce connected' : '⚠️ Salesforce connection failed')
}
// Setup WebSocket for real-time updates
if (this.config.enableWebSockets) {
this.setupWebSocket()
}
// Start processing timer
this.startProcessingTimer()
console.log('🎯 Real-Time Processing System Online')
} catch (error) {
// Failed to initialize real-time processor - error handled
}
}
private setupWebSocket(): void {
try {
const wsUrl = process.env.NODE_ENV === 'production'
? 'wss://miracles-in-motion.org/websocket'
: 'ws://localhost:8080/websocket'
this.websocket = new WebSocket(wsUrl)
this.websocket.onopen = () => {
// WebSocket connected for real-time updates
this.broadcastUpdate({
type: 'model-updated',
message: 'Real-time processing system online',
timestamp: new Date()
})
}
this.websocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
this.handleWebSocketMessage(data)
} catch (error) {
console.error('Error parsing WebSocket message:', error)
}
}
this.websocket.onclose = () => {
console.log('🔌 WebSocket disconnected, attempting reconnection...')
setTimeout(() => this.setupWebSocket(), 5000)
}
this.websocket.onerror = (error) => {
console.error('WebSocket error:', error)
}
} catch (error) {
console.error('Failed to setup WebSocket:', error)
}
}
private handleWebSocketMessage(data: any): void {
switch (data.type) {
case 'new-request':
this.addToQueue(data.request)
break
case 'priority-update':
this.updateRequestPriority(data.requestId, data.priority)
break
case 'system-status':
this.handleSystemStatusUpdate(data)
break
}
}
private startProcessingTimer(): void {
this.processTimer = window.setInterval(() => {
if (!this.isProcessing && this.processingQueue.length > 0) {
this.processNextBatch()
}
this.updateMetrics()
}, this.config.processingInterval)
}
// Public API Methods
public addToQueue(request: StudentRequest): void {
this.processingQueue.push(request)
this.metrics.queueLength = this.processingQueue.length
this.broadcastUpdate({
type: 'request-processed',
requestId: request.id,
studentName: request.studentName,
status: 'queued',
message: `Request added to processing queue (${this.metrics.queueLength} pending)`,
timestamp: new Date()
})
// Trigger immediate processing if queue was empty
if (this.processingQueue.length === 1 && !this.isProcessing) {
setTimeout(() => this.processNextBatch(), 100)
}
}
public async processNextBatch(): Promise<void> {
if (this.isProcessing || this.processingQueue.length === 0) {
return
}
this.isProcessing = true
const batchSize = Math.min(this.config.batchSize, this.processingQueue.length)
const batch = this.processingQueue.splice(0, batchSize)
console.log(`📊 Processing batch of ${batch.length} requests...`)
const processingPromises = batch.map(request => this.processSingleRequest(request))
try {
const results = await Promise.allSettled(processingPromises)
let successCount = 0
let errorCount = 0
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successCount++
this.handleProcessingSuccess(batch[index], result.value)
} else {
errorCount++
this.handleProcessingError(batch[index], result.reason)
}
})
// Update metrics
this.metrics.totalProcessed += batch.length
this.metrics.errorCount += errorCount
this.metrics.successRate = (this.metrics.totalProcessed - this.metrics.errorCount) / this.metrics.totalProcessed
this.metrics.lastProcessedAt = new Date()
console.log(`✅ Batch complete: ${successCount} success, ${errorCount} errors`)
} catch (error) {
console.error('Batch processing error:', error)
} finally {
this.isProcessing = false
this.metrics.queueLength = this.processingQueue.length
}
}
private async processSingleRequest(request: StudentRequest): Promise<MatchResult[]> {
const startTime = Date.now()
try {
// Process with AI
const matches = await this.ai.processRequest(request)
// Create Salesforce case if connected
if (this.salesforce && matches.length > 0) {
const caseId = await this.salesforce.createAssistanceCase(request)
if (caseId) {
await this.salesforce.updateCaseWithMatching(caseId, matches[0])
}
}
const processingTime = Date.now() - startTime
this.updateAverageProcessingTime(processingTime)
return matches
} catch (error) {
console.error(`Error processing request ${request.id}:`, error)
throw error
}
}
private handleProcessingSuccess(request: StudentRequest, matches: MatchResult[]): void {
this.broadcastUpdate({
type: 'request-processed',
requestId: request.id,
studentName: request.studentName,
status: 'completed',
recommendations: matches,
message: `Found ${matches.length} potential matches`,
timestamp: new Date()
})
// Auto-approve high confidence matches
const highConfidenceMatches = matches.filter(match => match.confidenceScore >= 0.85)
if (highConfidenceMatches.length > 0) {
this.broadcastUpdate({
type: 'auto-approval',
requestId: request.id,
studentName: request.studentName,
message: `Auto-approved ${highConfidenceMatches.length} high-confidence matches`,
timestamp: new Date()
})
}
}
private handleProcessingError(request: StudentRequest, error: any): void {
console.error(`Processing failed for ${request.id}:`, error)
this.broadcastUpdate({
type: 'alert',
requestId: request.id,
studentName: request.studentName,
message: `Processing failed: ${error.message || 'Unknown error'}`,
timestamp: new Date()
})
// Re-queue with lower priority if retryable
if (this.isRetryableError(error)) {
setTimeout(() => {
this.processingQueue.push({ ...request, urgency: 'low' })
}, 5000)
}
}
private isRetryableError(error: any): boolean {
// Define retryable error conditions
return error.code === 'NETWORK_ERROR' ||
error.code === 'RATE_LIMIT' ||
error.message?.includes('timeout')
}
private updateAverageProcessingTime(newTime: number): void {
const totalProcessed = this.metrics.totalProcessed
const currentAverage = this.metrics.averageProcessingTime
this.metrics.averageProcessingTime = ((currentAverage * totalProcessed) + newTime) / (totalProcessed + 1)
}
private updateMetrics(): void {
const now = Date.now()
// Calculate throughput (simplified)
this.metrics.throughputPerMinute = this.metrics.totalProcessed > 0 ?
Math.round(this.metrics.totalProcessed / ((now - this.metrics.lastProcessedAt.getTime()) / 60000)) : 0
this.metrics.activeProcessors = this.activeProcessors.size
}
private updateRequestPriority(requestId: string, newPriority: string): void {
const requestIndex = this.processingQueue.findIndex(req => req.id === requestId)
if (requestIndex !== -1) {
this.processingQueue[requestIndex].urgency = newPriority as any
// Re-sort queue by priority
this.processingQueue.sort((a, b) => {
const priorityOrder = { emergency: 4, high: 3, medium: 2, low: 1 }
return priorityOrder[b.urgency] - priorityOrder[a.urgency]
})
}
}
private handleSystemStatusUpdate(data: any): void {
// Update sync status based on external system updates
if (data.salesforce !== undefined) {
this.syncStatus.salesforceSync = data.salesforce
}
if (data.database !== undefined) {
this.syncStatus.databaseSync = data.database
}
}
// Subscription methods for real-time updates
public subscribe(callback: (update: AIUpdate) => void): () => void {
this.subscribers.push(callback)
// Return unsubscribe function
return () => {
const index = this.subscribers.indexOf(callback)
if (index > -1) {
this.subscribers.splice(index, 1)
}
}
}
private broadcastUpdate(update: AIUpdate): void {
this.subscribers.forEach(callback => {
try {
callback(update)
} catch (error) {
console.error('Error in subscriber callback:', error)
}
})
// Send to WebSocket if connected
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send(JSON.stringify(update))
}
}
// Status and metrics getters
public getMetrics(): ProcessingMetrics {
return { ...this.metrics }
}
public getSyncStatus(): DataSyncStatus {
return { ...this.syncStatus }
}
public getQueueStatus(): { length: number; processing: boolean; nextEstimatedTime?: number } {
return {
length: this.processingQueue.length,
processing: this.isProcessing,
nextEstimatedTime: this.processingQueue.length > 0 ?
this.metrics.averageProcessingTime * Math.ceil(this.processingQueue.length / this.config.batchSize) : undefined
}
}
// Lifecycle management
public pause(): void {
if (this.processTimer) {
clearInterval(this.processTimer)
this.processTimer = undefined
}
console.log('⏸️ Real-time processing paused')
}
public resume(): void {
if (!this.processTimer) {
this.startProcessingTimer()
console.log('▶️ Real-time processing resumed')
}
}
public async shutdown(): Promise<void> {
console.log('🛑 Shutting down real-time processor...')
this.pause()
// Wait for active processors to complete
if (this.activeProcessors.size > 0) {
console.log(`⏳ Waiting for ${this.activeProcessors.size} active processors...`)
await Promise.all(this.activeProcessors.values())
}
// Close WebSocket
if (this.websocket) {
this.websocket.close()
}
console.log('✅ Real-time processor shutdown complete')
}
}
// Factory function for easy initialization
export const createRealTimeProcessor = (config: Partial<RealTimeConfig> = {}): RealTimeProcessor => {
const defaultConfig: RealTimeConfig = {
enableWebSockets: true,
batchSize: 5,
processingInterval: 2000,
maxConcurrentRequests: 10,
enablePredictiveLoading: true,
cacheTimeout: 300000, // 5 minutes
enableOfflineMode: true
}
const finalConfig = { ...defaultConfig, ...config }
// Salesforce config from environment variables
const salesforceConfig = process.env.NODE_ENV === 'production' ? {
instanceUrl: process.env.REACT_APP_SALESFORCE_URL || '',
clientId: process.env.REACT_APP_SALESFORCE_CLIENT_ID || '',
clientSecret: process.env.REACT_APP_SALESFORCE_CLIENT_SECRET || '',
username: process.env.REACT_APP_SALESFORCE_USERNAME || '',
password: process.env.REACT_APP_SALESFORCE_PASSWORD || '',
securityToken: process.env.REACT_APP_SALESFORCE_TOKEN || '',
apiVersion: '58.0'
} : undefined
return new RealTimeProcessor(finalConfig, salesforceConfig)
}
export { RealTimeProcessor }
export type { RealTimeConfig, ProcessingMetrics, DataSyncStatus }
+77 -77
View File
@@ -1,78 +1,78 @@
import '@testing-library/jest-dom'
import { vi } from 'vitest'
// Mock IntersectionObserver
class MockIntersectionObserver {
observe = vi.fn()
disconnect = vi.fn()
unobserve = vi.fn()
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver,
})
Object.defineProperty(global, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver,
})
// Mock ResizeObserver
class MockResizeObserver {
observe = vi.fn()
disconnect = vi.fn()
unobserve = vi.fn()
}
window.ResizeObserver = MockResizeObserver
global.ResizeObserver = MockResizeObserver
// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
}
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
})
// Mock window.location
delete (window as any).location
Object.defineProperty(window, 'location', {
writable: true,
value: {
...window.location,
hash: '#/',
pathname: '/',
get search() {
return '';
},
get href() {
return 'http://localhost:3000/';
},
assign: vi.fn(),
replace: vi.fn(),
reload: vi.fn(),
},
import '@testing-library/jest-dom'
import { vi } from 'vitest'
// Mock IntersectionObserver
class MockIntersectionObserver {
observe = vi.fn()
disconnect = vi.fn()
unobserve = vi.fn()
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver,
})
Object.defineProperty(global, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver,
})
// Mock ResizeObserver
class MockResizeObserver {
observe = vi.fn()
disconnect = vi.fn()
unobserve = vi.fn()
}
window.ResizeObserver = MockResizeObserver
global.ResizeObserver = MockResizeObserver
// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
}
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
})
// Mock window.location
delete (window as any).location
Object.defineProperty(window, 'location', {
writable: true,
value: {
...window.location,
hash: '#/',
pathname: '/',
get search() {
return '';
},
get href() {
return 'http://localhost:3000/';
},
assign: vi.fn(),
replace: vi.fn(),
reload: vi.fn(),
},
});
+90 -90
View File
@@ -1,91 +1,91 @@
import { ReactElement } from 'react'
import { render, RenderOptions } from '@testing-library/react'
import { vi } from 'vitest'
// Mock contexts for testing
const MockAuthProvider = ({ children }: { children: React.ReactNode }) => {
return <div data-testid="auth-provider">{children}</div>
}
const MockNotificationProvider = ({ children }: { children: React.ReactNode }) => {
return <div data-testid="notification-provider">{children}</div>
}
const MockLanguageProvider = ({ children }: { children: React.ReactNode }) => {
return <div data-testid="language-provider">{children}</div>
}
const AllProviders = ({ children }: { children: React.ReactNode }) => {
return (
<MockAuthProvider>
<MockNotificationProvider>
<MockLanguageProvider>
{children}
</MockLanguageProvider>
</MockNotificationProvider>
</MockAuthProvider>
)
}
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllProviders, ...options })
// Mock implementations for common hooks
export const mockUseAuth = () => ({
user: null,
login: vi.fn(),
logout: vi.fn(),
isLoading: false
})
export const mockUseNotifications = () => ({
notifications: [],
addNotification: vi.fn(),
markAsRead: vi.fn(),
clearAll: vi.fn(),
unreadCount: 0
})
export const mockUseLanguage = () => ({
currentLanguage: { code: 'en', name: 'English', flag: '🇺🇸' },
changeLanguage: vi.fn(),
t: (key: string) => key
})
// Mock framer-motion components
export const mockMotionComponents = {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
section: ({ children, ...props }: any) => <section {...props}>{children}</section>,
a: ({ children, ...props }: any) => <a {...props}>{children}</a>,
button: ({ children, ...props }: any) => <button {...props}>{children}</button>
}
// Test data generators
export const createMockStudentRequest = (overrides = {}) => ({
id: 'req-123',
studentId: 'student-123',
studentName: 'John Doe',
description: 'Need school supplies',
category: 'school-supplies' as const,
urgency: 'medium' as const,
location: { address: '123 Test St', city: 'Test City', state: 'TS', zip: '12345' },
constraints: { budget: 100, timeline: '1 week' },
submittedAt: new Date(),
...overrides
})
export const createMockUser = (overrides = {}) => ({
id: 'user-123',
email: '[email protected]',
role: 'admin' as const,
name: 'Test User',
lastLogin: new Date(),
permissions: ['read', 'write', 'delete'],
...overrides
})
// Re-export everything from testing library
export * from '@testing-library/react'
import { ReactElement } from 'react'
import { render, RenderOptions } from '@testing-library/react'
import { vi } from 'vitest'
// Mock contexts for testing
const MockAuthProvider = ({ children }: { children: React.ReactNode }) => {
return <div data-testid="auth-provider">{children}</div>
}
const MockNotificationProvider = ({ children }: { children: React.ReactNode }) => {
return <div data-testid="notification-provider">{children}</div>
}
const MockLanguageProvider = ({ children }: { children: React.ReactNode }) => {
return <div data-testid="language-provider">{children}</div>
}
const AllProviders = ({ children }: { children: React.ReactNode }) => {
return (
<MockAuthProvider>
<MockNotificationProvider>
<MockLanguageProvider>
{children}
</MockLanguageProvider>
</MockNotificationProvider>
</MockAuthProvider>
)
}
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllProviders, ...options })
// Mock implementations for common hooks
export const mockUseAuth = () => ({
user: null,
login: vi.fn(),
logout: vi.fn(),
isLoading: false
})
export const mockUseNotifications = () => ({
notifications: [],
addNotification: vi.fn(),
markAsRead: vi.fn(),
clearAll: vi.fn(),
unreadCount: 0
})
export const mockUseLanguage = () => ({
currentLanguage: { code: 'en', name: 'English', flag: '🇺🇸' },
changeLanguage: vi.fn(),
t: (key: string) => key
})
// Mock framer-motion components
export const mockMotionComponents = {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
section: ({ children, ...props }: any) => <section {...props}>{children}</section>,
a: ({ children, ...props }: any) => <a {...props}>{children}</a>,
button: ({ children, ...props }: any) => <button {...props}>{children}</button>
}
// Test data generators
export const createMockStudentRequest = (overrides = {}) => ({
id: 'req-123',
studentId: 'student-123',
studentName: 'John Doe',
description: 'Need school supplies',
category: 'school-supplies' as const,
urgency: 'medium' as const,
location: { address: '123 Test St', city: 'Test City', state: 'TS', zip: '12345' },
constraints: { budget: 100, timeline: '1 week' },
submittedAt: new Date(),
...overrides
})
export const createMockUser = (overrides = {}) => ({
id: 'user-123',
email: '[email protected]',
role: 'admin' as const,
name: 'Test User',
lastLogin: new Date(),
permissions: ['read', 'write', 'delete'],
...overrides
})
// Re-export everything from testing library
export * from '@testing-library/react'
export { customRender as render }
+127 -127
View File
@@ -1,128 +1,128 @@
// Analytics and tracking utilities
interface TrackingEvent {
event: string
properties?: Record<string, any>
userId?: string
}
class Analytics {
private isEnabled: boolean = false
private userId: string | null = null
constructor() {
this.isEnabled = process.env.VITE_ANALYTICS_ENABLED === 'true'
}
// Initialize analytics
init(userId?: string): void {
this.userId = userId || null
if (this.isEnabled) {
// Analytics initialized for user
}
}
// Track page views
trackPageView(page: string, title?: string): void {
if (!this.isEnabled) return
const event: TrackingEvent = {
event: 'page_view',
properties: {
page,
title: title || document.title,
timestamp: new Date().toISOString(),
url: window.location.href
},
userId: this.userId || undefined
}
this.sendEvent(event)
}
// Track custom events
trackEvent(eventName: string, properties?: Record<string, any>): void {
if (!this.isEnabled) return
const event: TrackingEvent = {
event: eventName,
properties: {
...properties,
timestamp: new Date().toISOString(),
page: window.location.pathname
},
userId: this.userId || undefined
}
this.sendEvent(event)
}
// Track donation events
trackDonation(amount: number, method: string): void {
this.trackEvent('donation_completed', {
amount,
method,
impact: this.calculateImpactString(amount)
})
}
// Track volunteer signups
trackVolunteerSignup(volunteerType: string): void {
this.trackEvent('volunteer_signup', {
type: volunteerType
})
}
// Track form submissions
trackFormSubmission(formName: string, success: boolean): void {
this.trackEvent('form_submission', {
form_name: formName,
success
})
}
// Send event to analytics service
private sendEvent(event: TrackingEvent): void {
if (typeof window !== 'undefined' && window.gtag) {
// Google Analytics 4
window.gtag('event', event.event, {
custom_parameters: event.properties,
user_id: event.userId
})
}
// Console logging for development
if (process.env.NODE_ENV === 'development') {
// Analytics event tracked
}
// Could also send to other analytics services here
// this.sendToMixpanel(event)
// this.sendToAmplitude(event)
}
private calculateImpactString(amount: number): string {
const students = Math.floor(amount / 25)
return `${students} students supported`
}
}
// Export singleton instance
export const analytics = new Analytics()
// Track event helper function
export const trackEvent = (eventName: string, properties?: Record<string, any>): void => {
analytics.trackEvent(eventName, properties)
}
// Track page view helper function
export const trackPageView = (page: string, title?: string): void => {
analytics.trackPageView(page, title)
}
// Declare global gtag function for TypeScript
declare global {
interface Window {
gtag: (...args: any[]) => void
}
// Analytics and tracking utilities
interface TrackingEvent {
event: string
properties?: Record<string, any>
userId?: string
}
class Analytics {
private isEnabled: boolean = false
private userId: string | null = null
constructor() {
this.isEnabled = process.env.VITE_ANALYTICS_ENABLED === 'true'
}
// Initialize analytics
init(userId?: string): void {
this.userId = userId || null
if (this.isEnabled) {
// Analytics initialized for user
}
}
// Track page views
trackPageView(page: string, title?: string): void {
if (!this.isEnabled) return
const event: TrackingEvent = {
event: 'page_view',
properties: {
page,
title: title || document.title,
timestamp: new Date().toISOString(),
url: window.location.href
},
userId: this.userId || undefined
}
this.sendEvent(event)
}
// Track custom events
trackEvent(eventName: string, properties?: Record<string, any>): void {
if (!this.isEnabled) return
const event: TrackingEvent = {
event: eventName,
properties: {
...properties,
timestamp: new Date().toISOString(),
page: window.location.pathname
},
userId: this.userId || undefined
}
this.sendEvent(event)
}
// Track donation events
trackDonation(amount: number, method: string): void {
this.trackEvent('donation_completed', {
amount,
method,
impact: this.calculateImpactString(amount)
})
}
// Track volunteer signups
trackVolunteerSignup(volunteerType: string): void {
this.trackEvent('volunteer_signup', {
type: volunteerType
})
}
// Track form submissions
trackFormSubmission(formName: string, success: boolean): void {
this.trackEvent('form_submission', {
form_name: formName,
success
})
}
// Send event to analytics service
private sendEvent(event: TrackingEvent): void {
if (typeof window !== 'undefined' && window.gtag) {
// Google Analytics 4
window.gtag('event', event.event, {
custom_parameters: event.properties,
user_id: event.userId
})
}
// Console logging for development
if (process.env.NODE_ENV === 'development') {
// Analytics event tracked
}
// Could also send to other analytics services here
// this.sendToMixpanel(event)
// this.sendToAmplitude(event)
}
private calculateImpactString(amount: number): string {
const students = Math.floor(amount / 25)
return `${students} students supported`
}
}
// Export singleton instance
export const analytics = new Analytics()
// Track event helper function
export const trackEvent = (eventName: string, properties?: Record<string, any>): void => {
analytics.trackEvent(eventName, properties)
}
// Track page view helper function
export const trackPageView = (page: string, title?: string): void => {
analytics.trackPageView(page, title)
}
// Declare global gtag function for TypeScript
declare global {
interface Window {
gtag: (...args: any[]) => void
}
}
+289 -289
View File
@@ -1,290 +1,290 @@
// Bundle Analysis Utilities
interface BundleStats {
totalSize: number
chunks: ChunkInfo[]
dependencies: DependencyInfo[]
duplicates: string[]
}
interface ChunkInfo {
name: string
size: number
modules: string[]
type: 'entry' | 'vendor' | 'async'
}
interface DependencyInfo {
name: string
version: string
size: number
treeshakeable: boolean
sideEffects: boolean
}
// Analyze bundle performance and suggest optimizations
export class BundleAnalyzer {
private stats: BundleStats | null = null
async analyzeBuild(_statsFile?: string): Promise<BundleStats> {
// In a real implementation, this would parse webpack/vite stats
// For now, we'll simulate bundle analysis
const mockStats: BundleStats = {
totalSize: 245000, // 245KB
chunks: [
{
name: 'main',
size: 85000,
modules: ['src/main.tsx', 'src/App.tsx'],
type: 'entry'
},
{
name: 'vendor',
size: 120000,
modules: ['react', 'react-dom', 'framer-motion'],
type: 'vendor'
},
{
name: 'async-donation',
size: 40000,
modules: ['src/pages/DonatePage'],
type: 'async'
}
],
dependencies: [
{
name: 'react',
version: '18.2.0',
size: 45000,
treeshakeable: false,
sideEffects: false
},
{
name: 'framer-motion',
version: '10.16.4',
size: 35000,
treeshakeable: true,
sideEffects: false
},
{
name: 'lucide-react',
version: '0.294.0',
size: 15000,
treeshakeable: true,
sideEffects: false
}
],
duplicates: []
}
this.stats = mockStats
return mockStats
}
generateOptimizationSuggestions(): OptimizationSuggestion[] {
if (!this.stats) return []
const suggestions: OptimizationSuggestion[] = []
// Check for large chunks
this.stats.chunks.forEach(chunk => {
if (chunk.size > 100000) {
suggestions.push({
type: 'chunk-size',
severity: 'warning',
message: `Large chunk detected: ${chunk.name} (${(chunk.size / 1024).toFixed(1)}KB)`,
recommendation: 'Consider code splitting or lazy loading for this chunk',
impact: 'medium'
})
}
})
// Check for non-treeshakeable dependencies
this.stats.dependencies.forEach(dep => {
if (!dep.treeshakeable && dep.size > 20000) {
suggestions.push({
type: 'treeshaking',
severity: 'info',
message: `Non-treeshakeable dependency: ${dep.name} (${(dep.size / 1024).toFixed(1)}KB)`,
recommendation: 'Look for lighter alternatives or import specific modules',
impact: 'low'
})
}
})
// Check total bundle size
if (this.stats.totalSize > 300000) {
suggestions.push({
type: 'bundle-size',
severity: 'error',
message: `Bundle size is large: ${(this.stats.totalSize / 1024).toFixed(1)}KB`,
recommendation: 'Implement aggressive code splitting and lazy loading',
impact: 'high'
})
}
// Check for duplicate dependencies
if (this.stats.duplicates.length > 0) {
suggestions.push({
type: 'duplicates',
severity: 'warning',
message: `Duplicate dependencies found: ${this.stats.duplicates.join(', ')}`,
recommendation: 'Configure webpack/vite to deduplicate shared dependencies',
impact: 'medium'
})
}
return suggestions
}
generateReport(): BundleReport {
if (!this.stats) throw new Error('No stats available. Run analyzeBuild first.')
const suggestions = this.generateOptimizationSuggestions()
const score = this.calculatePerformanceScore()
return {
stats: this.stats,
suggestions,
score,
timestamp: new Date().toISOString(),
recommendations: this.generateRecommendations(score)
}
}
private calculatePerformanceScore(): number {
if (!this.stats) return 0
let score = 100
// Deduct points for large bundle size
if (this.stats.totalSize > 200000) score -= 20
if (this.stats.totalSize > 300000) score -= 30
// Deduct points for large chunks
this.stats.chunks.forEach(chunk => {
if (chunk.size > 100000) score -= 10
})
// Deduct points for non-treeshakeable deps
const nonTreeshakeable = this.stats.dependencies.filter(dep => !dep.treeshakeable)
score -= nonTreeshakeable.length * 5
// Deduct points for duplicates
score -= this.stats.duplicates.length * 10
return Math.max(0, Math.min(100, score))
}
private generateRecommendations(score: number): string[] {
const recommendations: string[] = []
if (score < 50) {
recommendations.push(
'Implement aggressive code splitting',
'Use dynamic imports for route-based splitting',
'Consider removing heavy dependencies',
'Implement preloading for critical resources'
)
} else if (score < 70) {
recommendations.push(
'Optimize large chunks with lazy loading',
'Review dependency usage for tree-shaking opportunities',
'Consider using CDN for large libraries'
)
} else if (score < 85) {
recommendations.push(
'Fine-tune code splitting boundaries',
'Optimize asset loading strategies'
)
} else {
recommendations.push(
'Your bundle is well optimized!',
'Consider monitoring performance over time'
)
}
return recommendations
}
}
interface OptimizationSuggestion {
type: 'chunk-size' | 'treeshaking' | 'bundle-size' | 'duplicates'
severity: 'info' | 'warning' | 'error'
message: string
recommendation: string
impact: 'low' | 'medium' | 'high'
}
interface BundleReport {
stats: BundleStats
suggestions: OptimizationSuggestion[]
score: number
timestamp: string
recommendations: string[]
}
// Singleton instance
export const bundleAnalyzer = new BundleAnalyzer()
// Helper functions for Vite integration
export function createBundleAnalyzerPlugin() {
return {
name: 'bundle-analyzer',
writeBundle(_options: any, bundle: any) {
if (process.env.ANALYZE_BUNDLE) {
// Bundle analysis performed
let totalSize = 0
const chunks: any[] = []
Object.entries(bundle).forEach(([name, chunk]: [string, any]) => {
if (chunk.type === 'chunk') {
const size = chunk.code.length
totalSize += size
chunks.push({ name, size, modules: chunk.modules || [] })
}
})
console.table(chunks.map(chunk => ({
name: chunk.name,
size: `${(chunk.size / 1024).toFixed(1)}KB`,
modules: chunk.modules.length
})))
// Total bundle size calculated
if (totalSize > 300000) {
console.warn('⚠️ Bundle size is large. Consider code splitting.')
}
}
}
}
}
// Performance monitoring in production
export function setupBundleMonitoring() {
if (typeof window === 'undefined' || process.env.NODE_ENV !== 'production') return
// Monitor bundle loading performance
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByType('resource')
entries.forEach((entry: any) => {
if (entry.name.includes('.js') || entry.name.includes('.css')) {
const loadTime = entry.loadEnd - entry.loadStart
const size = entry.encodedBodySize || entry.transferSize
// Send metrics to analytics
if (window.gtag) {
window.gtag('event', 'bundle_performance', {
resource_name: entry.name.split('/').pop(),
load_time: Math.round(loadTime),
size_kb: Math.round(size / 1024)
})
}
}
})
})
observer.observe({ entryTypes: ['resource'] })
// Bundle Analysis Utilities
interface BundleStats {
totalSize: number
chunks: ChunkInfo[]
dependencies: DependencyInfo[]
duplicates: string[]
}
interface ChunkInfo {
name: string
size: number
modules: string[]
type: 'entry' | 'vendor' | 'async'
}
interface DependencyInfo {
name: string
version: string
size: number
treeshakeable: boolean
sideEffects: boolean
}
// Analyze bundle performance and suggest optimizations
export class BundleAnalyzer {
private stats: BundleStats | null = null
async analyzeBuild(_statsFile?: string): Promise<BundleStats> {
// In a real implementation, this would parse webpack/vite stats
// For now, we'll simulate bundle analysis
const mockStats: BundleStats = {
totalSize: 245000, // 245KB
chunks: [
{
name: 'main',
size: 85000,
modules: ['src/main.tsx', 'src/App.tsx'],
type: 'entry'
},
{
name: 'vendor',
size: 120000,
modules: ['react', 'react-dom', 'framer-motion'],
type: 'vendor'
},
{
name: 'async-donation',
size: 40000,
modules: ['src/pages/DonatePage'],
type: 'async'
}
],
dependencies: [
{
name: 'react',
version: '18.2.0',
size: 45000,
treeshakeable: false,
sideEffects: false
},
{
name: 'framer-motion',
version: '10.16.4',
size: 35000,
treeshakeable: true,
sideEffects: false
},
{
name: 'lucide-react',
version: '0.294.0',
size: 15000,
treeshakeable: true,
sideEffects: false
}
],
duplicates: []
}
this.stats = mockStats
return mockStats
}
generateOptimizationSuggestions(): OptimizationSuggestion[] {
if (!this.stats) return []
const suggestions: OptimizationSuggestion[] = []
// Check for large chunks
this.stats.chunks.forEach(chunk => {
if (chunk.size > 100000) {
suggestions.push({
type: 'chunk-size',
severity: 'warning',
message: `Large chunk detected: ${chunk.name} (${(chunk.size / 1024).toFixed(1)}KB)`,
recommendation: 'Consider code splitting or lazy loading for this chunk',
impact: 'medium'
})
}
})
// Check for non-treeshakeable dependencies
this.stats.dependencies.forEach(dep => {
if (!dep.treeshakeable && dep.size > 20000) {
suggestions.push({
type: 'treeshaking',
severity: 'info',
message: `Non-treeshakeable dependency: ${dep.name} (${(dep.size / 1024).toFixed(1)}KB)`,
recommendation: 'Look for lighter alternatives or import specific modules',
impact: 'low'
})
}
})
// Check total bundle size
if (this.stats.totalSize > 300000) {
suggestions.push({
type: 'bundle-size',
severity: 'error',
message: `Bundle size is large: ${(this.stats.totalSize / 1024).toFixed(1)}KB`,
recommendation: 'Implement aggressive code splitting and lazy loading',
impact: 'high'
})
}
// Check for duplicate dependencies
if (this.stats.duplicates.length > 0) {
suggestions.push({
type: 'duplicates',
severity: 'warning',
message: `Duplicate dependencies found: ${this.stats.duplicates.join(', ')}`,
recommendation: 'Configure webpack/vite to deduplicate shared dependencies',
impact: 'medium'
})
}
return suggestions
}
generateReport(): BundleReport {
if (!this.stats) throw new Error('No stats available. Run analyzeBuild first.')
const suggestions = this.generateOptimizationSuggestions()
const score = this.calculatePerformanceScore()
return {
stats: this.stats,
suggestions,
score,
timestamp: new Date().toISOString(),
recommendations: this.generateRecommendations(score)
}
}
private calculatePerformanceScore(): number {
if (!this.stats) return 0
let score = 100
// Deduct points for large bundle size
if (this.stats.totalSize > 200000) score -= 20
if (this.stats.totalSize > 300000) score -= 30
// Deduct points for large chunks
this.stats.chunks.forEach(chunk => {
if (chunk.size > 100000) score -= 10
})
// Deduct points for non-treeshakeable deps
const nonTreeshakeable = this.stats.dependencies.filter(dep => !dep.treeshakeable)
score -= nonTreeshakeable.length * 5
// Deduct points for duplicates
score -= this.stats.duplicates.length * 10
return Math.max(0, Math.min(100, score))
}
private generateRecommendations(score: number): string[] {
const recommendations: string[] = []
if (score < 50) {
recommendations.push(
'Implement aggressive code splitting',
'Use dynamic imports for route-based splitting',
'Consider removing heavy dependencies',
'Implement preloading for critical resources'
)
} else if (score < 70) {
recommendations.push(
'Optimize large chunks with lazy loading',
'Review dependency usage for tree-shaking opportunities',
'Consider using CDN for large libraries'
)
} else if (score < 85) {
recommendations.push(
'Fine-tune code splitting boundaries',
'Optimize asset loading strategies'
)
} else {
recommendations.push(
'Your bundle is well optimized!',
'Consider monitoring performance over time'
)
}
return recommendations
}
}
interface OptimizationSuggestion {
type: 'chunk-size' | 'treeshaking' | 'bundle-size' | 'duplicates'
severity: 'info' | 'warning' | 'error'
message: string
recommendation: string
impact: 'low' | 'medium' | 'high'
}
interface BundleReport {
stats: BundleStats
suggestions: OptimizationSuggestion[]
score: number
timestamp: string
recommendations: string[]
}
// Singleton instance
export const bundleAnalyzer = new BundleAnalyzer()
// Helper functions for Vite integration
export function createBundleAnalyzerPlugin() {
return {
name: 'bundle-analyzer',
writeBundle(_options: any, bundle: any) {
if (process.env.ANALYZE_BUNDLE) {
// Bundle analysis performed
let totalSize = 0
const chunks: any[] = []
Object.entries(bundle).forEach(([name, chunk]: [string, any]) => {
if (chunk.type === 'chunk') {
const size = chunk.code.length
totalSize += size
chunks.push({ name, size, modules: chunk.modules || [] })
}
})
console.table(chunks.map(chunk => ({
name: chunk.name,
size: `${(chunk.size / 1024).toFixed(1)}KB`,
modules: chunk.modules.length
})))
// Total bundle size calculated
if (totalSize > 300000) {
console.warn('⚠️ Bundle size is large. Consider code splitting.')
}
}
}
}
}
// Performance monitoring in production
export function setupBundleMonitoring() {
if (typeof window === 'undefined' || process.env.NODE_ENV !== 'production') return
// Monitor bundle loading performance
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByType('resource')
entries.forEach((entry: any) => {
if (entry.name.includes('.js') || entry.name.includes('.css')) {
const loadTime = entry.loadEnd - entry.loadStart
const size = entry.encodedBodySize || entry.transferSize
// Send metrics to analytics
if (window.gtag) {
window.gtag('event', 'bundle_performance', {
resource_name: entry.name.split('/').pop(),
load_time: Math.round(loadTime),
size_kb: Math.round(size / 1024)
})
}
}
})
})
observer.observe({ entryTypes: ['resource'] })
}
+121 -121
View File
@@ -1,122 +1,122 @@
// Enhanced impact calculation utilities
export interface ImpactCalculation {
students: number
families: number
backpacks: number
clothing: number
emergency: number
annual: {
students: number
families: number
totalImpact: string
}
}
export const 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`
}
}
}
// Format currency
export const formatCurrency = (amount: number): string => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount)
}
// Format numbers with commas
export const formatNumber = (num: number): string => {
return new Intl.NumberFormat('en-US').format(num)
}
// Validate email
export const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
// Validate phone number
export const validatePhone = (phone: string): boolean => {
const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/
return phoneRegex.test(phone.replace(/\D/g, ''))
}
// Generate unique ID
export const generateId = (): string => {
return Math.random().toString(36).substr(2, 9)
}
// Debounce function
export const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number
): ((...args: Parameters<T>) => void) => {
let timeoutId: NodeJS.Timeout
return (...args: Parameters<T>) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func(...args), delay)
}
}
// Throttle function
export const throttle = <T extends (...args: any[]) => any>(
func: T,
limit: number
): ((...args: Parameters<T>) => void) => {
let inThrottle: boolean
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
// Safe JSON parse
export const safeJsonParse = <T>(str: string, fallback: T): T => {
try {
return JSON.parse(str)
} catch {
return fallback
}
}
// Format date
export const formatDate = (date: Date): string => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date)
}
// Format relative time
export const formatRelativeTime = (date: Date): string => {
const now = new Date()
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
if (diffInSeconds < 60) return 'just now'
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`
return formatDate(date)
// Enhanced impact calculation utilities
export interface ImpactCalculation {
students: number
families: number
backpacks: number
clothing: number
emergency: number
annual: {
students: number
families: number
totalImpact: string
}
}
export const 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`
}
}
}
// Format currency
export const formatCurrency = (amount: number): string => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount)
}
// Format numbers with commas
export const formatNumber = (num: number): string => {
return new Intl.NumberFormat('en-US').format(num)
}
// Validate email
export const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
// Validate phone number
export const validatePhone = (phone: string): boolean => {
const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/
return phoneRegex.test(phone.replace(/\D/g, ''))
}
// Generate unique ID
export const generateId = (): string => {
return Math.random().toString(36).substr(2, 9)
}
// Debounce function
export const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number
): ((...args: Parameters<T>) => void) => {
let timeoutId: NodeJS.Timeout
return (...args: Parameters<T>) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func(...args), delay)
}
}
// Throttle function
export const throttle = <T extends (...args: any[]) => any>(
func: T,
limit: number
): ((...args: Parameters<T>) => void) => {
let inThrottle: boolean
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
// Safe JSON parse
export const safeJsonParse = <T>(str: string, fallback: T): T => {
try {
return JSON.parse(str)
} catch {
return fallback
}
}
// Format date
export const formatDate = (date: Date): string => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date)
}
// Format relative time
export const formatRelativeTime = (date: Date): string => {
const now = new Date()
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
if (diffInSeconds < 60) return 'just now'
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`
return formatDate(date)
}