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
+225
View File
@@ -0,0 +1,225 @@
import { useEffect, useState, useCallback } from 'react'
interface PerformanceMetrics {
FCP: number | null // First Contentful Paint
LCP: number | null // Largest Contentful Paint
FID: number | null // First Input Delay
CLS: number | null // Cumulative Layout Shift
TTFB: number | null // Time to First Byte
}
interface WebVitals {
metrics: PerformanceMetrics
isLoading: boolean
}
// Performance monitoring hook
export function usePerformance(): WebVitals {
const [metrics, setMetrics] = useState<PerformanceMetrics>({
FCP: null,
LCP: null,
FID: null,
CLS: null,
TTFB: null
})
const [isLoading, setIsLoading] = useState(true)
const updateMetric = useCallback((name: keyof PerformanceMetrics, value: number) => {
setMetrics(prev => ({ ...prev, [name]: value }))
}, [])
useEffect(() => {
// Check if Web Vitals API is available
if (typeof window === 'undefined') return
// Measure TTFB from Navigation Timing API
const measureTTFB = () => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
if (navigation) {
const ttfb = navigation.responseStart - navigation.requestStart
updateMetric('TTFB', ttfb)
}
}
// Measure FCP using PerformanceObserver
const measureFCP = () => {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByName('first-contentful-paint')
if (entries.length > 0) {
updateMetric('FCP', entries[0].startTime)
}
})
observer.observe({ entryTypes: ['paint'] })
return () => observer.disconnect()
} catch (error) {
console.warn('FCP measurement not supported')
return () => {}
}
}
// Measure LCP using PerformanceObserver
const measureLCP = () => {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
if (entries.length > 0) {
const lastEntry = entries[entries.length - 1]
updateMetric('LCP', lastEntry.startTime)
}
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
return () => observer.disconnect()
} catch (error) {
console.warn('LCP measurement not supported')
return () => {}
}
}
// Measure FID using PerformanceObserver
const measureFID = () => {
try {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
entries.forEach((entry: any) => {
if (entry.name === 'first-input') {
const fid = entry.processingStart - entry.startTime
updateMetric('FID', fid)
}
})
})
observer.observe({ entryTypes: ['first-input'] })
return () => observer.disconnect()
} catch (error) {
console.warn('FID measurement not supported')
return () => {}
}
}
// Measure CLS using PerformanceObserver
const measureCLS = () => {
try {
let clsValue = 0
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
entries.forEach((entry: any) => {
if (!entry.hadRecentInput) {
clsValue += entry.value
updateMetric('CLS', clsValue)
}
})
})
observer.observe({ entryTypes: ['layout-shift'] })
return () => observer.disconnect()
} catch (error) {
console.warn('CLS measurement not supported')
return () => {}
}
}
// Initialize measurements
measureTTFB()
const cleanupFCP = measureFCP()
const cleanupLCP = measureLCP()
const cleanupFID = measureFID()
const cleanupCLS = measureCLS()
// Set loading to false after initial measurements
const timeout = setTimeout(() => setIsLoading(false), 2000)
return () => {
clearTimeout(timeout)
cleanupFCP()
cleanupLCP()
cleanupFID()
cleanupCLS()
}
}, [])
return { metrics, isLoading }
}
// Hook for monitoring bundle size and loading performance
export function useBundlePerformance() {
const [bundleMetrics, setBundleMetrics] = useState({
totalSize: 0,
jsSize: 0,
cssSize: 0,
imageSize: 0,
loadTime: 0
})
useEffect(() => {
if (typeof window === 'undefined') return
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByType('resource')
let jsSize = 0
let cssSize = 0
let imageSize = 0
let totalSize = 0
entries.forEach((entry: any) => {
const size = entry.encodedBodySize || entry.transferSize || 0
totalSize += size
if (entry.name.includes('.js')) jsSize += size
else if (entry.name.includes('.css')) cssSize += size
else if (entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) imageSize += size
})
setBundleMetrics({
totalSize: Math.round(totalSize / 1024), // KB
jsSize: Math.round(jsSize / 1024),
cssSize: Math.round(cssSize / 1024),
imageSize: Math.round(imageSize / 1024),
loadTime: performance.now()
})
})
observer.observe({ entryTypes: ['resource'] })
return () => observer.disconnect()
}, [])
return bundleMetrics
}
// Analytics tracking for performance metrics
export function trackPerformanceMetrics(metrics: PerformanceMetrics) {
if (typeof window === 'undefined' || !(window as any).gtag) return
const { FCP, LCP, FID, CLS, TTFB } = metrics
// Send to Google Analytics
if (FCP) (window as any).gtag('event', 'timing_complete', {
name: 'FCP',
value: Math.round(FCP)
})
if (LCP) (window as any).gtag('event', 'timing_complete', {
name: 'LCP',
value: Math.round(LCP)
})
if (FID) (window as any).gtag('event', 'timing_complete', {
name: 'FID',
value: Math.round(FID)
})
if (CLS) (window as any).gtag('event', 'timing_complete', {
name: 'CLS',
value: Math.round(CLS * 1000) // Convert to ms
})
if (TTFB) (window as any).gtag('event', 'timing_complete', {
name: 'TTFB',
value: Math.round(TTFB)
})
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.group('Performance Metrics:')
console.table(metrics)
console.groupEnd()
}
}