feat: implement authentication context and user session management; enhance donation impact calculator and UI interactions

This commit is contained in:
defiQUG
2025-10-04 22:55:41 -07:00
parent ff7233bfed
commit 914c4180b5

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'
import React, { useState, useEffect, useRef, useContext, createContext } from 'react'
import { motion, useMotionValue, useSpring, useTransform, useScroll } from 'framer-motion'
import {
ArrowRight,
@@ -51,11 +51,178 @@ import {
* donation processing, volunteer management, and impact tracking.
*/
/* ===================== Authentication Context ===================== */
const AuthContext = createContext<AuthContextType | null>(null)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null)
const [isLoading, setIsLoading] = useState(false)
const login = async (email: string, password: string): Promise<boolean> => {
setIsLoading(true)
try {
// Simulate authentication - in production, this would call your API
await new Promise(resolve => setTimeout(resolve, 1000))
// Simple demo validation (in production, validate against secure backend)
if (password.length < 3) {
return false
}
// Mock user data based on email domain
const mockUser: AuthUser = {
id: Math.random().toString(36),
email,
role: email.includes('admin') ? 'admin' : email.includes('volunteer') ? 'volunteer' : 'resource',
name: email.split('@')[0].replace('.', ' ').replace(/\b\w/g, l => l.toUpperCase()),
lastLogin: new Date(),
permissions: email.includes('admin') ? ['read', 'write', 'delete', 'manage'] : ['read', 'write']
}
setUser(mockUser)
localStorage.setItem('mim_user', JSON.stringify(mockUser))
return true
} catch (error) {
console.error('Login failed:', error)
return false
} finally {
setIsLoading(false)
}
}
const logout = () => {
setUser(null)
localStorage.removeItem('mim_user')
}
// Restore user session on app load
useEffect(() => {
const savedUser = localStorage.getItem('mim_user')
if (savedUser) {
try {
setUser(JSON.parse(savedUser))
} catch (error) {
localStorage.removeItem('mim_user')
}
}
}, [])
return (
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
)
}
function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
/* ===================== Enhanced Impact Calculator ===================== */
function calculateDonationImpact(amount: number): ImpactCalculation {
const students = Math.floor(amount / 25) // $25 per student for basic supplies
const families = Math.floor(amount / 50) // $50 per family for comprehensive support
const backpacks = Math.floor(amount / 30) // $30 for complete backpack kit
const clothing = Math.floor(amount / 45) // $45 for clothing items
const emergency = Math.floor(amount / 75) // $75 for emergency assistance
return {
students,
families,
backpacks,
clothing,
emergency,
annual: {
students: Math.floor((amount * 12) / 25),
families: Math.floor((amount * 12) / 50),
totalImpact: `${Math.floor((amount * 12) / 25)} students supported annually`
}
}
}
/* ===================== Analytics Tracking ===================== */
function trackEvent(eventName: string, properties: Record<string, any> = {}) {
// In production, integrate with Google Analytics, Mixpanel, or similar
if (typeof window !== 'undefined' && (window as any).gtag) {
(window as any).gtag('event', eventName, properties)
}
console.log(`Analytics: ${eventName}`, properties)
}
/* ===================== SEO Meta Tags Component ===================== */
function SEOHead({ title, description, image }: { title?: string, description?: string, image?: string }) {
useEffect(() => {
// Update document title
if (title) {
document.title = `${title} | Miracles in Motion`
}
// Update meta description
const metaDescription = document.querySelector('meta[name="description"]')
if (description && metaDescription) {
metaDescription.setAttribute('content', description)
}
// Update Open Graph tags
const updateOGTag = (property: string, content: string) => {
let tag = document.querySelector(`meta[property="${property}"]`)
if (!tag) {
tag = document.createElement('meta')
tag.setAttribute('property', property)
document.head.appendChild(tag)
}
tag.setAttribute('content', content)
}
updateOGTag('og:title', title || 'Miracles in Motion - Equipping Students for Success')
updateOGTag('og:description', description || 'Nonprofit providing students with school supplies, clothing, and emergency assistance to thrive in their education.')
updateOGTag('og:image', image || '/og-image.jpg')
updateOGTag('og:type', 'website')
}, [title, description, image])
return null
}
/* ===================== Types ===================== */
interface IconProps {
className?: string
}
interface AuthUser {
id: string
email: string
role: 'admin' | 'volunteer' | 'resource'
name: string
lastLogin: Date
permissions: string[]
}
interface AuthContextType {
user: AuthUser | null
login: (email: string, password: string) => Promise<boolean>
logout: () => void
isLoading: boolean
}
interface ImpactCalculation {
students: number
families: number
backpacks: number
clothing: number
emergency: number
annual: {
students: number
families: number
totalImpact: string
}
}
interface TiltCardProps {
icon: React.ComponentType<IconProps>
title: string
@@ -1331,6 +1498,7 @@ function DonatePage() {
const [selectedAmount, setSelectedAmount] = useState(50)
const [customAmount, setCustomAmount] = useState('')
const [isRecurring, setIsRecurring] = useState(false)
const [donorInfo, setDonorInfo] = useState({ email: '', name: '', anonymous: false })
const suggestedAmounts = [
{ amount: 25, impact: "School supplies for 1 student", popular: false },
@@ -1339,40 +1507,98 @@ function DonatePage() {
{ amount: 250, impact: "Emergency fund for 5 families", popular: false }
]
const getImpactText = (amount: number) => {
if (amount >= 250) return `Emergency support for ${Math.floor(amount / 50)} families`
if (amount >= 100) return `School clothing for ${Math.floor(amount / 50)} students`
if (amount >= 50) return `Complete support for ${Math.floor(amount / 50)} student${Math.floor(amount / 50) > 1 ? 's' : ''}`
if (amount >= 25) return `School supplies for ${Math.floor(amount / 25)} student${Math.floor(amount / 25) > 1 ? 's' : ''}`
return "Every dollar helps a student in need"
const finalAmount = customAmount ? parseInt(customAmount) || 0 : selectedAmount
const impactData = calculateDonationImpact(finalAmount)
// Enhanced impact text with real calculations
const getDetailedImpactText = (amount: number) => {
const impact = calculateDonationImpact(amount)
const impactItems = []
if (impact.students > 0) impactItems.push(`${impact.students} student${impact.students > 1 ? 's' : ''} with supplies`)
if (impact.backpacks > 0) impactItems.push(`${impact.backpacks} backpack kit${impact.backpacks > 1 ? 's' : ''}`)
if (impact.clothing > 0) impactItems.push(`${impact.clothing} clothing item${impact.clothing > 1 ? 's' : ''}`)
if (impact.emergency > 0) impactItems.push(`${impact.emergency} emergency response${impact.emergency > 1 ? 's' : ''}`)
return impactItems.length > 0 ? impactItems.join(', ') : "Every dollar helps a student in need"
}
const finalAmount = customAmount ? parseInt(customAmount) || 0 : selectedAmount
// Track donation interactions
useEffect(() => {
trackEvent('donation_page_view', { amount: finalAmount, recurring: isRecurring })
}, [])
const handleDonationSubmit = () => {
trackEvent('donation_initiated', {
amount: finalAmount,
recurring: isRecurring,
anonymous: donorInfo.anonymous
})
// This would integrate with Stripe or another payment processor
alert(`Processing ${isRecurring ? 'monthly ' : ''}donation of $${finalAmount}`)
}
return (
<PageShell title="Donate" icon={Heart} eyebrow="Give with confidence" cta={<a href="#/legal" className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" aria-label="View donation policies">Policies</a>}>
<>
<SEOHead
title="Donate Now - Help Students in Need"
description="Make a secure donation to Miracles in Motion. Your gift provides school supplies, clothing, and emergency assistance directly to students who need it most."
/>
<PageShell title="Donate" icon={Heart} eyebrow="Give with confidence" cta={<a href="#/legal" className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" aria-label="View donation policies">Policies</a>}>
<div className="grid gap-8 md:grid-cols-3">
<div className="md:col-span-2 space-y-8">
{/* Impact Calculator */}
{/* Enhanced Impact Calculator */}
<motion.div
className="card bg-gradient-to-br from-primary-50 to-secondary-50 dark:from-primary-900/20 dark:to-secondary-900/20 border-primary-200/50 dark:border-primary-800/50"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
whileHover={{ scale: 1.01, y: -2 }}
>
<div className="flex items-start gap-4">
<div className="p-3 bg-primary-600 text-white rounded-xl">
<div className="flex items-start gap-4 mb-4">
<motion.div
className="p-3 bg-primary-600 text-white rounded-xl"
whileHover={{ scale: 1.1, rotateY: 180 }}
style={{ transformStyle: 'preserve-3d' }}
>
<Heart className="h-6 w-6" />
</div>
</motion.div>
<div className="flex-1">
<h3 className="font-semibold text-lg mb-2">Your Impact: ${finalAmount}</h3>
<p className="text-primary-700 dark:text-primary-300 font-medium">
{getImpactText(finalAmount)}
<p className="text-primary-700 dark:text-primary-300 font-medium mb-3">
{getDetailedImpactText(finalAmount)}
</p>
{isRecurring && (
<p className="text-sm text-primary-600 dark:text-primary-400 mt-1">
That's {getImpactText(finalAmount * 12).replace(/\d+/, String(Math.floor((finalAmount * 12) / 25)))} annually!
</p>
{/* Real-time impact breakdown */}
{finalAmount > 0 && (
<div className="grid gap-2 text-sm">
<div className="flex items-center justify-between p-2 bg-white/50 dark:bg-gray-800/50 rounded">
<span className="flex items-center gap-2">
<Backpack className="h-4 w-4" /> Students Supported
</span>
<span className="font-semibold">{impactData.students}</span>
</div>
<div className="flex items-center justify-between p-2 bg-white/50 dark:bg-gray-800/50 rounded">
<span className="flex items-center gap-2">
<Package className="h-4 w-4" /> Backpack Kits
</span>
<span className="font-semibold">{impactData.backpacks}</span>
</div>
<div className="flex items-center justify-between p-2 bg-white/50 dark:bg-gray-800/50 rounded">
<span className="flex items-center gap-2">
<Shirt className="h-4 w-4" /> Clothing Items
</span>
<span className="font-semibold">{impactData.clothing}</span>
</div>
</div>
)}
{isRecurring && finalAmount > 0 && (
<div className="mt-3 p-3 bg-secondary-50 dark:bg-secondary-900/20 rounded-lg">
<p className="text-sm text-secondary-700 dark:text-secondary-300 font-medium">
🎉 Annual Impact: {impactData.annual.totalImpact}
</p>
</div>
)}
</div>
</div>
@@ -1385,20 +1611,22 @@ function DonatePage() {
<p className="text-sm text-neutral-600 dark:text-neutral-400">Every dollar directly supports students in need</p>
</div>
{/* Suggested Amounts */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 mb-6">
{/* Suggested Amounts - Mobile Optimized */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4 mb-6">
{suggestedAmounts.map((tier) => (
<motion.button
key={tier.amount}
onClick={() => {
setSelectedAmount(tier.amount)
setCustomAmount('')
trackEvent('donation_amount_selected', { amount: tier.amount, method: 'suggested' })
}}
className={`relative p-4 rounded-xl border-2 transition-all focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
selectedAmount === tier.amount && !customAmount
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-neutral-200 dark:border-neutral-700 hover:border-primary-300'
}`}
style={{ minHeight: '88px', minWidth: '120px' }} // Enhanced touch targets for mobile
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
aria-label={`Donate $${tier.amount} - ${tier.impact}`}
@@ -1443,53 +1671,103 @@ function DonatePage() {
</div>
</div>
{/* Recurring Option */}
<div className="mb-6 p-4 bg-secondary-50 dark:bg-secondary-900/20 rounded-xl">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={isRecurring}
onChange={(e) => setIsRecurring(e.target.checked)}
className="mt-1 h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<div className="flex-1">
<div className="font-medium">Make this a monthly donation</div>
<div className="text-sm text-neutral-600 dark:text-neutral-400">
Recurring donations help us plan better and have more impact
</div>
{isRecurring && finalAmount > 0 && (
<div className="text-sm text-secondary-600 dark:text-secondary-400 mt-1 font-medium">
Annual Impact: {getImpactText(finalAmount * 12)}
{/* Recurring Option & Donor Info */}
<div className="space-y-4 mb-6">
<div className="p-4 bg-secondary-50 dark:bg-secondary-900/20 rounded-xl">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={isRecurring}
onChange={(e) => {
setIsRecurring(e.target.checked)
trackEvent('donation_recurring_toggle', { recurring: e.target.checked, amount: finalAmount })
}}
className="mt-1 h-5 w-5 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" // Enhanced for mobile
style={{ minHeight: '20px', minWidth: '20px' }}
/>
<div className="flex-1">
<div className="font-medium">Make this a monthly donation</div>
<div className="text-sm text-neutral-600 dark:text-neutral-400">
Recurring donations help us plan better and have more impact
</div>
)}
{isRecurring && finalAmount > 0 && (
<div className="text-sm text-secondary-600 dark:text-secondary-400 mt-2 p-2 bg-white/50 dark:bg-gray-800/50 rounded font-medium">
🎯 Annual Impact: {impactData.annual.totalImpact}
</div>
)}
</div>
</label>
</div>
{/* Donor Information */}
<div className="grid gap-4 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-xl">
<h4 className="font-medium text-gray-900 dark:text-gray-100">Donor Information (Optional)</h4>
<div className="grid gap-3 sm:grid-cols-2">
<input
type="text"
placeholder="Your Name"
value={donorInfo.name}
onChange={(e) => setDonorInfo({ ...donorInfo, name: e.target.value })}
className="input focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
style={{ minHeight: '44px' }}
/>
<input
type="email"
placeholder="Email (for receipt)"
value={donorInfo.email}
onChange={(e) => setDonorInfo({ ...donorInfo, email: e.target.value })}
className="input focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
style={{ minHeight: '44px' }}
/>
</div>
</label>
<label className="flex items-center gap-3 text-sm">
<input
type="checkbox"
checked={donorInfo.anonymous}
onChange={(e) => setDonorInfo({ ...donorInfo, anonymous: e.target.checked })}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span>Make my donation anonymous</span>
</label>
</div>
</div>
{/* Donation Buttons */}
{/* Enhanced Donation Buttons */}
<div className="space-y-3">
<motion.button
disabled={finalAmount <= 0}
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
style={{ minHeight: '56px' }} // Enhanced mobile touch target
whileHover={finalAmount > 0 ? { scale: 1.02 } : {}}
whileTap={finalAmount > 0 ? { scale: 0.98 } : {}}
onClick={() => {
// This would integrate with Stripe or another payment processor
alert(`Processing ${isRecurring ? 'monthly ' : ''}donation of $${finalAmount}`)
}}
onClick={handleDonationSubmit}
aria-label={`Donate $${finalAmount} ${isRecurring ? 'monthly' : 'one-time'} via credit card`}
>
<Heart className="mr-2 h-5 w-5" />
Donate ${finalAmount} {isRecurring ? 'Monthly' : 'Securely'}
</motion.button>
<div className="flex gap-3">
<button className="flex-1 btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" aria-label="Donate via PayPal">
PayPal
</button>
<button className="flex-1 btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" aria-label="Donate via Venmo">
Venmo
</button>
<div className="grid gap-3 sm:grid-cols-2">
<motion.button
className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
style={{ minHeight: '48px' }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => trackEvent('donation_method_selected', { method: 'paypal', amount: finalAmount })}
aria-label="Donate via PayPal"
>
💳 PayPal
</motion.button>
<motion.button
className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
style={{ minHeight: '48px' }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => trackEvent('donation_method_selected', { method: 'venmo', amount: finalAmount })}
aria-label="Donate via Venmo"
>
📱 Venmo
</motion.button>
</div>
</div>
@@ -1628,6 +1906,7 @@ function DonatePage() {
</div>
</div>
</PageShell>
</>
)
}
@@ -2392,8 +2671,168 @@ function PortalsPage() {
)
}
/* ===================== Authentication Components ===================== */
function LoginForm({ requiredRole }: { requiredRole?: 'admin' | 'volunteer' | 'resource' }) {
const { login, isLoading } = useAuth()
const [formData, setFormData] = useState({ email: '', password: '' })
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
const success = await login(formData.email, formData.password)
if (!success) {
setError('Invalid credentials. Please try again.')
}
}
const getRoleHint = () => {
switch (requiredRole) {
case 'admin': return 'Use an email containing "admin" to access admin features'
case 'volunteer': return 'Use an email containing "volunteer" for volunteer access'
case 'resource': return 'Use any other email for resource center access'
default: return 'Enter your credentials to access the portal'
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-secondary-50 dark:from-gray-900 dark:to-gray-800 px-4">
<motion.div
className="w-full max-w-md"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="text-center mb-8">
<motion.div
className="inline-flex items-center justify-center w-16 h-16 bg-primary-600 text-white rounded-full mb-4"
whileHover={{ scale: 1.05, rotateY: 180 }}
style={{ transformStyle: 'preserve-3d' }}
>
<Lock className="w-8 h-8" />
</motion.div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{requiredRole ? `${requiredRole.charAt(0).toUpperCase() + requiredRole.slice(1)} Portal` : 'Portal Access'}
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-2">
Sign in to access your dashboard
</p>
</div>
<div className="card">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium mb-2">
Email Address
</label>
<input
id="email"
type="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="input w-full focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="your.email@example.com"
style={{ minHeight: '44px' }} // Mobile touch optimization
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium mb-2">
Password
</label>
<input
id="password"
type="password"
required
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
className="input w-full focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Enter your password"
style={{ minHeight: '44px' }} // Mobile touch optimization
/>
</div>
{error && (
<motion.div
className="text-red-600 dark:text-red-400 text-sm p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
>
{error}
</motion.div>
)}
<div className="text-xs text-gray-500 dark:text-gray-400 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
<strong>Demo Access:</strong> {getRoleHint()}
</div>
<motion.button
type="submit"
disabled={isLoading}
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
style={{ minHeight: '44px' }} // Mobile touch optimization
whileHover={{ scale: isLoading ? 1 : 1.02 }}
whileTap={{ scale: isLoading ? 1 : 0.98 }}
>
{isLoading ? (
<><Clock className="w-4 h-4 mr-2 animate-spin" /> Signing In...</>
) : (
<>Sign In</>
)}
</motion.button>
</form>
<div className="mt-6 text-center">
<a href="#/" className="text-sm text-primary-600 dark:text-primary-400 hover:underline">
Back to Main Site
</a>
</div>
</div>
</motion.div>
</div>
)
}
function PortalWrapper({ children, requiredRole }: { children: React.ReactNode, requiredRole?: 'admin' | 'volunteer' | 'resource' }) {
const { user } = useAuth()
if (!user) {
return <LoginForm requiredRole={requiredRole} />
}
if (requiredRole && user.role !== requiredRole) {
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="card max-w-md w-full text-center">
<AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-bold mb-2">Access Denied</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
You don't have permission to access the {requiredRole} portal.
</p>
<div className="space-y-2">
<a href="#/" className="btn-primary">
Return to Main Site
</a>
<button
onClick={() => useAuth().logout()}
className="btn-secondary w-full"
>
Sign Out
</button>
</div>
</div>
</div>
)
}
return <>{children}</>
}
// Admin Portal Dashboard
function AdminPortalPage() {
const { user, logout } = useAuth()
const [stats] = useState({
pendingRequests: 23,
activeVolunteers: 47,
@@ -2402,8 +2841,23 @@ function AdminPortalPage() {
monthlySpent: 8250
})
useEffect(() => {
trackEvent('admin_portal_view', { user_id: user?.id, user_role: user?.role })
}, [])
return (
<PageShell title="Administration Dashboard" icon={Settings} eyebrow="Full system access">
<PortalWrapper requiredRole="admin">
<SEOHead title="Admin Dashboard" description="Administrative portal for Miracles in Motion staff and administrators." />
<PageShell
title="Administration Dashboard"
icon={Settings}
eyebrow={`Welcome back, ${user?.name}`}
cta={
<button onClick={logout} className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
Sign Out
</button>
}
>
<div className="space-y-8">
{/* Quick Stats */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
@@ -2514,13 +2968,31 @@ function AdminPortalPage() {
</div>
</div>
</PageShell>
</PortalWrapper>
)
}
// Volunteer Portal Dashboard
// Volunteer Portal Dashboard
function VolunteerPortalPage() {
const { user, logout } = useAuth()
useEffect(() => {
trackEvent('volunteer_portal_view', { user_id: user?.id, user_role: user?.role })
}, [])
return (
<PageShell title="Volunteer Dashboard" icon={UserCheck} eyebrow="Your assignments and schedule">
<PortalWrapper requiredRole="volunteer">
<SEOHead title="Volunteer Dashboard" description="Volunteer portal for Miracles in Motion volunteers to manage assignments and schedules." />
<PageShell
title="Volunteer Dashboard"
icon={UserCheck}
eyebrow={`Hello, ${user?.name}`}
cta={
<button onClick={logout} className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
Sign Out
</button>
}
>
<div className="space-y-8">
{/* Today's Tasks */}
<div className="card bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
@@ -2620,14 +3092,32 @@ function VolunteerPortalPage() {
</div>
</div>
</PageShell>
</PortalWrapper>
)
}
// Resource Center Portal Dashboard
function ResourcePortalPage() {
const { user, logout } = useAuth()
useEffect(() => {
trackEvent('resource_portal_view', { user_id: user?.id, user_role: user?.role })
}, [])
return (
<PageShell title="Resource Center Portal" icon={School} eyebrow="Submit and track assistance requests">
<div className="space-y-8">
<PortalWrapper requiredRole="resource">
<SEOHead title="Resource Portal" description="Resource center portal for submitting and tracking student assistance requests." />
<PageShell
title="Resource Center Portal"
icon={School}
eyebrow={`Welcome, ${user?.name}`}
cta={
<button onClick={logout} className="btn-secondary focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
Sign Out
</button>
}
>
<div className="space-y-8">
{/* Quick Submit */}
<div className="card bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
<div className="flex items-center gap-4 mb-6">
@@ -2740,6 +3230,7 @@ function ResourcePortalPage() {
</div>
</div>
</PageShell>
</PortalWrapper>
)
}