feat: Implement browser-compatible processing pipeline for student requests
- Added SimpleBrowserProcessor class to handle student requests in a queue. - Introduced methods for adding requests, notifying subscribers, and processing the queue. - Mock AI matching logic implemented to simulate resource matching based on request categories and urgency. - Added generateInsights method to analyze request trends and urgencies. feat: Create authentication context for user management - Developed AuthContext to manage user authentication state. - Implemented login and logout functionalities with mock user data. - Added loading state management during authentication processes. feat: Add notification context for managing user notifications - Created NotificationContext to handle notifications throughout the application. - Implemented methods for adding, marking as read, and removing notifications. feat: Introduce common hooks for utility functions - Developed useHashRoute for hash-based routing. - Created useLocalStorage for managing local storage with TypeScript support. - Implemented useMediaQuery for responsive design handling. - Added useDebounce for debouncing values in state. feat: Implement donation impact calculation hook - Created useDonationImpact hook to calculate the impact of donations. - Added useFormValidation hook for form state management and validation. feat: Design main layout and page structure components - Developed MainLayout and PageShell components for consistent layout structure. - Created SectionHeader and Card components for reusable UI elements. feat: Build HomePage with impact statistics and service offerings - Implemented HomePage component with hero section, impact stats, and service cards. - Integrated tracking for donation and volunteer actions. feat: Add analytics utilities for tracking events - Created analytics utility for tracking page views and custom events. - Implemented donation and volunteer tracking functionalities. feat: Enhance helper functions for various utilities - Developed utility functions for impact calculation, formatting, validation, and debouncing.
This commit is contained in:
3011
package-lock.json
generated
3011
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -32,23 +32,18 @@
|
||||
"homepage": "https://miraclesinmotion.org",
|
||||
"dependencies": {
|
||||
"@tensorflow/tfjs": "^4.22.0",
|
||||
"bull": "^4.16.5",
|
||||
"compromise": "^14.14.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^10.16.16",
|
||||
"ioredis": "^5.8.0",
|
||||
"lucide-react": "^0.290.0",
|
||||
"ml-matrix": "^6.12.1",
|
||||
"natural": "^8.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"redis": "^5.8.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"uuid": "^13.0.0",
|
||||
"ws": "^8.18.3"
|
||||
"react-helmet-async": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
@@ -60,8 +55,11 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.4",
|
||||
"gh-pages": "^6.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"terser": "^5.44.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0"
|
||||
"vite": "^7.1.9",
|
||||
"vite-bundle-analyzer": "^1.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
267
src/AppNew.tsx
Normal file
267
src/AppNew.tsx
Normal file
@@ -0,0 +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: 12-3456789 • 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@miraclesinmotion.org</p>
|
||||
<p>(555) 123-4567</p>
|
||||
<p>123 Community Way<br />Hometown, ST 12345</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-700 mt-8 pt-8 text-center text-gray-400">
|
||||
<p>© 2025 Miracles in Motion. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
231
src/ai/BrowserProcessor.ts
Normal file
231
src/ai/BrowserProcessor.ts
Normal file
@@ -0,0 +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
|
||||
export const browserProcessor = new SimpleBrowserProcessor()
|
||||
@@ -1,7 +1,10 @@
|
||||
// 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 = {
|
||||
private static modelUrls: Record<string, string> = {
|
||||
'text-vectorization': '/models/text-vectorizer.json',
|
||||
'matching-engine': '/models/matcher.json',
|
||||
'priority-classifier': '/models/priority.json'
|
||||
@@ -55,15 +58,37 @@ export class OptimizedStudentAssistanceAI {
|
||||
|
||||
private async aiBasedMatching(request: StudentRequest, textModel: any, matchingModel: any): Promise<MatchResult[]> {
|
||||
// AI-powered matching logic
|
||||
console.log('🤖 Using AI-powered matching')
|
||||
// ... implementation
|
||||
return []
|
||||
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)')
|
||||
// ... implementation
|
||||
return []
|
||||
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']
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,26 @@
|
||||
// Phase 3: Real-time Processing Pipeline for AI Assistance Matching
|
||||
import type {
|
||||
StudentRequest,
|
||||
MatchResult,
|
||||
// Phase 3: Browser-Compatible Processing Pipeline for AI Assistance Matching
|
||||
import {
|
||||
StudentRequest,
|
||||
MatchResult,
|
||||
AIInsight,
|
||||
AIUpdate,
|
||||
AIInsight,
|
||||
ProcessingPipelineConfig
|
||||
ProcessingPipelineConfig
|
||||
} from './types'
|
||||
import { aiEngine } from './StudentAssistanceAI'
|
||||
|
||||
// Mock Queue Implementation (in production, use Redis + Bull)
|
||||
class MockQueue<T> {
|
||||
private jobs: Array<{ id: string; data: T; priority: number }> = []
|
||||
// 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
|
||||
priority: options.priority,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
this.jobs.push(job)
|
||||
@@ -131,14 +134,14 @@ class ReviewManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Main Processing Pipeline
|
||||
// Main Processing Pipeline - Simplified browser-compatible version
|
||||
export class RealTimeProcessingPipeline {
|
||||
private queue: MockQueue<StudentRequest>
|
||||
private queue: BrowserQueue<StudentRequest>
|
||||
private notificationService: NotificationService
|
||||
private config: ProcessingPipelineConfig
|
||||
|
||||
constructor(config: Partial<ProcessingPipelineConfig> = {}) {
|
||||
this.queue = new MockQueue<StudentRequest>()
|
||||
this.queue = new BrowserQueue<StudentRequest>()
|
||||
this.notificationService = new NotificationService()
|
||||
this.config = {
|
||||
autoApprovalThreshold: 0.85,
|
||||
@@ -169,14 +172,14 @@ export class RealTimeProcessingPipeline {
|
||||
}
|
||||
|
||||
private setupQueueProcessors(): void {
|
||||
this.queue.process('analyze-request', 5, async (job) => {
|
||||
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}`)
|
||||
|
||||
// AI analysis and matching
|
||||
const matches = await aiEngine.processRequest(request)
|
||||
// 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) {
|
||||
@@ -397,8 +400,35 @@ export class RealTimeProcessingPipeline {
|
||||
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[]> {
|
||||
return await aiEngine.generateInsights(requests)
|
||||
// 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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Phase 3: AI-Powered Student Assistance Matching Engine
|
||||
// Phase 3: AI-Powered Student Assistance Matching Engine (Browser-Compatible)
|
||||
import * as tf from '@tensorflow/tfjs'
|
||||
import type {
|
||||
StudentRequest,
|
||||
@@ -10,10 +10,10 @@ import type {
|
||||
ResourceRequirement
|
||||
} from './types'
|
||||
|
||||
// Text Vectorization for NLP Analysis
|
||||
class TextVectorizer {
|
||||
// Simple browser-compatible text analysis
|
||||
class BrowserTextAnalyzer {
|
||||
private vocabulary: Map<string, number> = new Map()
|
||||
private vectorSize: number = 100
|
||||
private vectorSize: number = 50 // Reduced for browser performance
|
||||
|
||||
constructor() {
|
||||
this.initializeVocabulary()
|
||||
@@ -110,7 +110,7 @@ class TextVectorizer {
|
||||
|
||||
// Core AI Matching Engine
|
||||
export class StudentAssistanceAI {
|
||||
private vectorizer: TextVectorizer
|
||||
private vectorizer: BrowserTextAnalyzer
|
||||
private matchingModel: tf.LayersModel | null = null
|
||||
private impactModel: tf.LayersModel | null = null
|
||||
private isInitialized = false
|
||||
@@ -132,7 +132,7 @@ export class StudentAssistanceAI {
|
||||
]
|
||||
|
||||
constructor() {
|
||||
this.vectorizer = new TextVectorizer()
|
||||
this.vectorizer = new BrowserTextAnalyzer()
|
||||
this.initializeModels()
|
||||
}
|
||||
|
||||
|
||||
103
src/contexts/AuthContext.tsx
Normal file
103
src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +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> = {
|
||||
'admin@miraclesinmotion.org': {
|
||||
id: '1',
|
||||
email: 'admin@miraclesinmotion.org',
|
||||
role: 'admin',
|
||||
name: 'Admin User',
|
||||
lastLogin: new Date(),
|
||||
permissions: ['all']
|
||||
},
|
||||
'volunteer@miraclesinmotion.org': {
|
||||
id: '2',
|
||||
email: 'volunteer@miraclesinmotion.org',
|
||||
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
|
||||
}
|
||||
85
src/contexts/NotificationContext.tsx
Normal file
85
src/contexts/NotificationContext.tsx
Normal file
@@ -0,0 +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
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
// Phase 3B: Salesforce Nonprofit Cloud CRM Integration
|
||||
// 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
|
||||
|
||||
84
src/hooks/useCommon.ts
Normal file
84
src/hooks/useCommon.ts
Normal file
@@ -0,0 +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
|
||||
}
|
||||
108
src/hooks/useDonationImpact.ts
Normal file
108
src/hooks/useDonationImpact.ts
Normal file
@@ -0,0 +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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/* @tailwind directives removed because Tailwind CSS is not being processed.
|
||||
If you are using Tailwind, make sure to process this file with Tailwind CLI or PostCSS. */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
|
||||
155
src/layouts/MainLayout.tsx
Normal file
155
src/layouts/MainLayout.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React, { ReactNode } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { LucideIcon } from 'lucide-react'
|
||||
|
||||
interface MainLayoutProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
interface PageShellProps {
|
||||
title: string
|
||||
icon?: LucideIcon
|
||||
eyebrow?: string
|
||||
subtitle?: string
|
||||
cta?: ReactNode
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
// Main layout wrapper
|
||||
export const MainLayout: React.FC<MainLayoutProps> = ({ children, className = '' }) => {
|
||||
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}`}>
|
||||
{children}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +1,369 @@
|
||||
// Example: Modular Page Structure
|
||||
import React from 'react'
|
||||
import { SEOHead } from '@/components/SEO'
|
||||
import { PageShell } from '@/components/Layout'
|
||||
import { DonationForm } from './DonationForm'
|
||||
import { ImpactCalculator } from './ImpactCalculator'
|
||||
import { DonationTiers } from './DonationTiers'
|
||||
import { Heart } from 'lucide-react'
|
||||
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 (
|
||||
<>
|
||||
<SEOHead
|
||||
title="Donate - Support Students in Need"
|
||||
description="Your donation directly supports students with school supplies, clothing, and emergency assistance."
|
||||
/>
|
||||
<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>
|
||||
|
||||
<PageShell
|
||||
title="Make a Difference Today"
|
||||
icon={Heart}
|
||||
eyebrow="Every dollar counts"
|
||||
className={className}
|
||||
>
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
<DonationTiers />
|
||||
<DonationForm />
|
||||
<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 className="space-y-6">
|
||||
<ImpactCalculator />
|
||||
{/* Add trust badges, testimonials, etc. */}
|
||||
<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>
|
||||
</PageShell>
|
||||
</>
|
||||
|
||||
<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>
|
||||
)
|
||||
204
src/pages/HomePage/index.tsx
Normal file
204
src/pages/HomePage/index.tsx
Normal file
@@ -0,0 +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>
|
||||
)
|
||||
@@ -1,8 +1,28 @@
|
||||
// Phase 3B: Real-Time Data Processing and Live Deployment System
|
||||
import { StudentAssistanceAI } from '../ai/StudentAssistanceAI'
|
||||
import { SalesforceConnector } from '../crm/SalesforceConnector'
|
||||
// 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
|
||||
@@ -34,8 +54,8 @@ interface DataSyncStatus {
|
||||
}
|
||||
|
||||
class RealTimeProcessor {
|
||||
private ai: StudentAssistanceAI
|
||||
private salesforce?: SalesforceConnector
|
||||
private ai: BrowserAIProcessor
|
||||
private salesforce?: any // Mock for browser demo
|
||||
private config: RealTimeConfig
|
||||
private processingQueue: StudentRequest[] = []
|
||||
private activeProcessors = new Map<string, Promise<MatchResult[]>>()
|
||||
@@ -48,10 +68,10 @@ class RealTimeProcessor {
|
||||
|
||||
constructor(config: RealTimeConfig, salesforceConfig?: any) {
|
||||
this.config = config
|
||||
this.ai = new StudentAssistanceAI()
|
||||
this.ai = new BrowserAIProcessor()
|
||||
|
||||
if (salesforceConfig) {
|
||||
this.salesforce = new SalesforceConnector(salesforceConfig)
|
||||
this.salesforce = { mock: true } // Mock for browser demo
|
||||
}
|
||||
|
||||
this.metrics = {
|
||||
|
||||
128
src/utils/analytics.ts
Normal file
128
src/utils/analytics.ts
Normal file
@@ -0,0 +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) {
|
||||
console.log('🔍 Analytics initialized', { userId })
|
||||
}
|
||||
}
|
||||
|
||||
// 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') {
|
||||
console.log('📊 Analytics Event:', event)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
122
src/utils/helpers.ts
Normal file
122
src/utils/helpers.ts
Normal file
@@ -0,0 +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)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
// Optimized Vite Configuration for Production
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
@@ -20,11 +20,30 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
// Core React libraries
|
||||
vendor: ['react', 'react-dom'],
|
||||
motion: ['framer-motion'],
|
||||
icons: ['lucide-react'],
|
||||
|
||||
// UI and animations
|
||||
ui: ['framer-motion', 'lucide-react'],
|
||||
|
||||
// AI libraries (browser-compatible)
|
||||
ai: ['@tensorflow/tfjs'],
|
||||
|
||||
// Utilities
|
||||
utils: ['date-fns', 'react-helmet-async']
|
||||
},
|
||||
chunkFileNames: 'js/[name]-[hash].js'
|
||||
}
|
||||
},
|
||||
// Enable compression
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
},
|
||||
},
|
||||
// Optimize chunks
|
||||
chunkSizeWarningLimit: 500,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user