feat: Add ProgramsSection component for comprehensive student support

- Implemented ProgramsSection with various support programs including School Supplies, Clothing Support, Emergency Assistance, Educational Technology, Mentorship Programs, and Family Support Services.
- Integrated framer-motion for animations and transitions.
- Added a call-to-action button for requesting program support.

test: Create unit tests for HeroSection component

- Developed tests for rendering, accessibility, and functionality of the HeroSection component using Vitest and Testing Library.
- Mocked framer-motion for testing purposes.

refactor: Update sections index file to include ProgramsSection

- Modified index.tsx to export ProgramsSection alongside existing sections.

feat: Implement LazyImage component for optimized image loading

- Created LazyImage component with lazy loading, error handling, and blur placeholder support.
- Utilized framer-motion for loading animations.

feat: Add PerformanceMonitor component for real-time performance metrics

- Developed PerformanceMonitor to display web vitals and bundle performance metrics.
- Included toggle functionality for development mode.

feat: Create usePerformance hook for performance monitoring

- Implemented usePerformance hook to track web vitals such as FCP, LCP, FID, CLS, and TTFB.
- Added useBundlePerformance hook for monitoring bundle size and loading performance.

test: Set up testing utilities and mocks for components

- Established testing utilities for rendering components with context providers.
- Mocked common hooks and framer-motion components for consistent testing.

feat: Introduce bundleAnalyzer utility for analyzing bundle performance

- Created BundleAnalyzer class to analyze bundle size, suggest optimizations, and generate reports.
- Implemented helper functions for Vite integration and performance monitoring.

chore: Configure Vitest for testing environment and coverage

- Set up Vitest configuration with global variables, jsdom environment, and coverage thresholds.
This commit is contained in:
defiQUG
2025-10-05 09:42:05 -07:00
parent 93bcf4d560
commit 0b81bcb4f5
20 changed files with 7229 additions and 28 deletions

View File

@@ -0,0 +1,139 @@
import { useState, useRef, useEffect, ImgHTMLAttributes } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
interface LazyImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'loading'> {
src: string
alt: string
placeholder?: string
blurDataURL?: string
priority?: boolean
sizes?: string
quality?: number
onLoadComplete?: () => void
}
export function LazyImage({
src,
alt,
placeholder = '/placeholder.svg',
blurDataURL,
priority = false,
className = '',
onLoadComplete,
...props
}: LazyImageProps) {
const [isLoaded, setIsLoaded] = useState(false)
const [isInView, setIsInView] = useState(priority)
const [error, setError] = useState(false)
const imgRef = useRef<HTMLImageElement>(null)
const [imageSrc, setImageSrc] = useState(priority ? src : placeholder)
// Intersection Observer for lazy loading
useEffect(() => {
if (priority) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true)
setImageSrc(src)
observer.disconnect()
}
},
{ rootMargin: '50px' }
)
const currentImg = imgRef.current
if (currentImg) {
observer.observe(currentImg)
}
return () => {
if (currentImg) observer.unobserve(currentImg)
}
}, [src, priority])
// Handle image load
const handleLoad = () => {
setIsLoaded(true)
onLoadComplete?.()
}
// Handle image error
const handleError = () => {
setError(true)
setImageSrc(placeholder)
}
// Generate optimized src with quality and format
const getOptimizedSrc = (originalSrc: string, quality = 85) => {
// Check if it's already optimized or external
if (originalSrc.includes('?') || originalSrc.startsWith('http')) {
return originalSrc
}
// Add quality parameter for supported formats
if (originalSrc.includes('.jpg') || originalSrc.includes('.jpeg')) {
return `${originalSrc}?quality=${quality}&format=webp`
}
return originalSrc
}
const optimizedSrc = getOptimizedSrc(imageSrc)
return (
<div className={`relative overflow-hidden ${className}`}>
<AnimatePresence>
{blurDataURL && !isLoaded && (
<motion.div
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="absolute inset-0 z-10"
style={{
backgroundImage: `url(${blurDataURL})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
filter: 'blur(10px)',
transform: 'scale(1.1)'
}}
/>
)}
</AnimatePresence>
<motion.img
ref={imgRef}
src={optimizedSrc}
alt={alt}
loading={priority ? 'eager' : 'lazy'}
onLoad={handleLoad}
onError={handleError}
initial={{ opacity: 0 }}
animate={{ opacity: isLoaded ? 1 : 0 }}
transition={{ duration: 0.3 }}
className={`w-full h-full object-cover ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
style={props.style}
/>
{/* Loading skeleton */}
{!isLoaded && !error && (
<div className="absolute inset-0 bg-gray-200 animate-pulse flex items-center justify-center">
<div className="w-8 h-8 bg-gray-300 rounded-full animate-pulse" />
</div>
)}
{/* Error state */}
{error && (
<div className="absolute inset-0 bg-gray-100 flex items-center justify-center text-gray-400">
<div className="text-center">
<div className="w-12 h-12 mx-auto mb-2 bg-gray-300 rounded" />
<p className="text-sm">Failed to load image</p>
</div>
</div>
)}
</div>
)
}
export default LazyImage

View File

@@ -0,0 +1,211 @@
import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import { usePerformance, useBundlePerformance } from '@/hooks/usePerformance'
import { Activity, Zap, Globe, Image, Code } from 'lucide-react'
interface PerformanceMonitorProps {
showDetailed?: boolean
className?: string
}
export function PerformanceMonitor({
showDetailed = false,
className = ''
}: PerformanceMonitorProps) {
const { metrics, isLoading } = usePerformance()
const bundleMetrics = useBundlePerformance()
const [isVisible, setIsVisible] = useState(false)
// Toggle visibility in development mode
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.ctrlKey && e.shiftKey && e.key === 'P') {
setIsVisible(!isVisible)
}
}
if (process.env.NODE_ENV === 'development') {
window.addEventListener('keydown', handleKeyPress)
return () => window.removeEventListener('keydown', handleKeyPress)
}
}, [isVisible])
// Don't render in production unless explicitly requested
if (process.env.NODE_ENV === 'production' && !showDetailed) return null
if (!isVisible && process.env.NODE_ENV === 'development') return null
const getScoreColor = (value: number | null, thresholds: [number, number]) => {
if (value === null) return 'text-gray-400'
if (value <= thresholds[0]) return 'text-green-500'
if (value <= thresholds[1]) return 'text-yellow-500'
return 'text-red-500'
}
const formatMetric = (value: number | null, suffix = 'ms') => {
return value ? `${Math.round(value)}${suffix}` : 'N/A'
}
const webVitalsData = [
{
label: 'FCP',
description: 'First Contentful Paint',
value: metrics.FCP,
threshold: [1800, 3000] as [number, number],
icon: Activity,
good: '< 1.8s',
poor: '> 3.0s'
},
{
label: 'LCP',
description: 'Largest Contentful Paint',
value: metrics.LCP,
threshold: [2500, 4000] as [number, number],
icon: Globe,
good: '< 2.5s',
poor: '> 4.0s'
},
{
label: 'FID',
description: 'First Input Delay',
value: metrics.FID,
threshold: [100, 300] as [number, number],
icon: Zap,
good: '< 100ms',
poor: '> 300ms'
},
{
label: 'CLS',
description: 'Cumulative Layout Shift',
value: metrics.CLS,
threshold: [0.1, 0.25] as [number, number],
icon: Activity,
good: '< 0.1',
poor: '> 0.25',
suffix: ''
},
{
label: 'TTFB',
description: 'Time to First Byte',
value: metrics.TTFB,
threshold: [800, 1800] as [number, number],
icon: Globe,
good: '< 800ms',
poor: '> 1.8s'
}
]
return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className={`fixed top-4 right-4 z-50 bg-white/95 backdrop-blur-sm border border-gray-200 rounded-lg shadow-lg p-4 max-w-sm ${className}`}
>
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-sm text-gray-900 flex items-center gap-2">
<Activity className="w-4 h-4" />
Performance Monitor
</h3>
<button
onClick={() => setIsVisible(false)}
className="text-gray-400 hover:text-gray-600 text-sm"
>
×
</button>
</div>
{isLoading ? (
<div className="flex items-center gap-2 text-sm text-gray-500">
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
Measuring performance...
</div>
) : (
<div className="space-y-3">
{/* Web Vitals */}
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2">Core Web Vitals</h4>
<div className="grid grid-cols-3 gap-2">
{webVitalsData.slice(0, 3).map((metric) => {
const IconComponent = metric.icon
const colorClass = getScoreColor(metric.value, metric.threshold)
return (
<div key={metric.label} className="text-center">
<div className="flex items-center justify-center mb-1">
<IconComponent className={`w-3 h-3 ${colorClass}`} />
</div>
<div className={`text-xs font-mono ${colorClass}`}>
{formatMetric(metric.value, metric.suffix || 'ms')}
</div>
<div className="text-xs text-gray-500">{metric.label}</div>
</div>
)
})}
</div>
</div>
{/* Additional Metrics */}
{showDetailed && (
<>
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2">Additional Metrics</h4>
<div className="grid grid-cols-2 gap-2">
{webVitalsData.slice(3).map((metric) => {
const colorClass = getScoreColor(metric.value, metric.threshold)
return (
<div key={metric.label} className="text-center">
<div className={`text-xs font-mono ${colorClass}`}>
{formatMetric(metric.value, metric.suffix || 'ms')}
</div>
<div className="text-xs text-gray-500">{metric.label}</div>
</div>
)
})}
</div>
</div>
{/* Bundle Metrics */}
<div>
<h4 className="text-xs font-medium text-gray-700 mb-2">Bundle Size</h4>
<div className="space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1">
<Code className="w-3 h-3 text-blue-500" />
JavaScript
</span>
<span className="font-mono">{bundleMetrics.jsSize}KB</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1">
<div className="w-3 h-3 bg-green-500 rounded-sm" />
CSS
</span>
<span className="font-mono">{bundleMetrics.cssSize}KB</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1">
<Image className="w-3 h-3 text-purple-500" />
Images
</span>
<span className="font-mono">{bundleMetrics.imageSize}KB</span>
</div>
<div className="border-t pt-1 flex items-center justify-between text-xs font-medium">
<span>Total</span>
<span className="font-mono">{bundleMetrics.totalSize}KB</span>
</div>
</div>
</div>
</>
)}
{/* Tips */}
<div className="text-xs text-gray-500 border-t pt-2">
Press <kbd className="px-1 py-0.5 bg-gray-100 rounded text-xs">Ctrl+Shift+P</kbd> to toggle
</div>
</div>
)}
</motion.div>
)
}
export default PerformanceMonitor