From 1523864ab921f7d4d6dce3f167a988006819bfd6 Mon Sep 17 00:00:00 2001 From: DeborahOlaboye Date: Fri, 5 Dec 2025 04:31:23 +0100 Subject: [PATCH] feat: Implement performance optimizations - Add WebSocket service with reconnection and message queuing - Create useWebSocket hook for React components - Implement loading states with LoadingSpinner and LoadingOverlay - Add RealTimeMetricsWithWebSocket component - Document performance best practices - Add TypeScript types and path aliases --- .../RealTimeMetricsWithWebSocket.tsx | 234 ++++++++++++++++++ frontend/app/components/ui/Alert.tsx | 66 +++++ frontend/app/components/ui/Button.tsx | 55 ++++ frontend/app/components/ui/LoadingSpinner.tsx | 71 ++++++ frontend/app/hooks/useWebSocket.ts | 148 +++++++++++ .../services/websocket/WebSocketService.ts | 193 +++++++++++++++ frontend/docs/PERFORMANCE_OPTIMIZATIONS.md | 104 ++++++++ frontend/lib/utils.ts | 6 + frontend/package-lock.json | 47 ++++ frontend/package.json | 2 + frontend/types/components.d.ts | 38 +++ 11 files changed, 964 insertions(+) create mode 100644 frontend/app/components/RealTimeMetricsWithWebSocket.tsx create mode 100644 frontend/app/components/ui/Alert.tsx create mode 100644 frontend/app/components/ui/Button.tsx create mode 100644 frontend/app/components/ui/LoadingSpinner.tsx create mode 100644 frontend/app/hooks/useWebSocket.ts create mode 100644 frontend/app/services/websocket/WebSocketService.ts create mode 100644 frontend/docs/PERFORMANCE_OPTIMIZATIONS.md create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/types/components.d.ts diff --git a/frontend/app/components/RealTimeMetricsWithWebSocket.tsx b/frontend/app/components/RealTimeMetricsWithWebSocket.tsx new file mode 100644 index 0000000..62fb737 --- /dev/null +++ b/frontend/app/components/RealTimeMetricsWithWebSocket.tsx @@ -0,0 +1,234 @@ +'use client'; + +import { useEffect, useState, useCallback } from 'react'; +import { Line } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + Filler, +} from 'chart.js'; +import { LoadingSpinner, InlineLoading } from '@/app/components/ui/LoadingSpinner'; +import { Alert, AlertDescription, AlertTitle } from '@/app/components/ui/Alert'; +import { Button } from '@/app/components/ui/Button'; +import useWebSocket from '../hooks/useWebSocket'; + +// Register ChartJS components +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + Filler +); + +type MetricData = { + timestamp: number; + value: number; +}; + +const WEBSOCKET_URL = process.env.NEXT_PUBLIC_WEBSOCKET_URL || 'ws://localhost:3001'; + +export function RealTimeMetricsWithWebSocket() { + const [metrics, setMetrics] = useState([]); + const [isInitialized, setIsInitialized] = useState(false); + const [error, setError] = useState(null); + + const handleWebSocketMessage = useCallback((data: any) => { + try { + // Process the incoming WebSocket message + // This assumes the server sends data in the format: { timestamp: number, value: number } + const newMetric: MetricData = { + timestamp: data.timestamp || Date.now(), + value: data.value || 0, + }; + + setMetrics(prev => { + // Keep only the last 100 data points for performance + const updated = [...prev, newMetric].slice(-100); + return updated; + }); + } catch (err) { + console.error('Error processing WebSocket message:', err); + setError('Failed to process incoming data'); + } + }, []); + + const handleError = useCallback((error: Error) => { + console.error('WebSocket error:', error); + setError(error.message || 'Connection error'); + }, []); + + const handleConnect = useCallback(() => { + console.log('WebSocket connected'); + setError(null); + if (!isInitialized) { + setIsInitialized(true); + } + }, [isInitialized]); + + const handleDisconnect = useCallback(() => { + console.log('WebSocket disconnected'); + }, []); + + // Initialize WebSocket connection + const { + state: connectionState, + reconnect, + isConnected, + isConnecting, + } = useWebSocket({ + url: WEBSOCKET_URL, + onMessage: handleWebSocketMessage, + onConnect: handleConnect, + onDisconnect: handleDisconnect, + onError: handleError, + }); + + // Process metrics for chart + const processChartData = () => { + if (metrics.length === 0) { + return { + labels: [], + datasets: [{ + label: 'Metrics', + data: [], + borderColor: '#4F46E5', + backgroundColor: 'rgba(79, 70, 229, 0.2)', + borderWidth: 2, + tension: 0.4, + fill: true, + }], + }; + } + + return { + labels: metrics.map((m) => new Date(m.timestamp).toLocaleTimeString()), + datasets: [ + { + label: 'Metrics', + data: metrics.map((m) => m.value), + borderColor: '#4F46E5', + backgroundColor: 'rgba(79, 70, 229, 0.2)', + borderWidth: 2, + tension: 0.4, + fill: true, + }, + ], + }; + }; + + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + animation: { + duration: 300, + easing: 'easeInOutQuad' as const, + }, + scales: { + x: { + grid: { + display: false, + }, + ticks: { + maxRotation: 0, + autoSkip: true, + maxTicksLimit: 8, + }, + }, + y: { + beginAtZero: true, + grid: { + color: 'rgba(0, 0, 0, 0.05)', + }, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + mode: 'index' as const, + intersect: false, + }, + }, + }; + + // Show loading state if we're still connecting and have no data + if (isConnecting && !isInitialized) { + return ( +
+
+ +

Connecting to real-time data...

+
+
+ ); + } + + // Show error state if there's an error + if (error) { + return ( + + Connection Error + +

{error}

+ +
+
+ ); + } + + // Show empty state if no data is available yet + if (metrics.length === 0) { + return ( +
+

Waiting for real-time data...

+ +
+ ); + } + + return ( +
+
+

Real-time Metrics

+
+ {!isConnected && ( +
+ + Reconnecting... +
+ )} + +
+
+ +
+ +
+ +
+ {metrics.length} data points + Last updated: {new Date().toLocaleTimeString()} +
+
+ ); +} diff --git a/frontend/app/components/ui/Alert.tsx b/frontend/app/components/ui/Alert.tsx new file mode 100644 index 0000000..e7022a5 --- /dev/null +++ b/frontend/app/components/ui/Alert.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { VariantProps, cva } from 'class-variance-authority'; +import { cn } from '../../lib/utils'; + +const alertVariants = cva( + 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', + { + variants: { + variant: { + default: 'bg-background text-foreground', + destructive: + 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', + success: + 'border-green-500/50 text-green-700 dark:border-green-500 [&>svg]:text-green-500', + warning: + 'border-yellow-500/50 text-yellow-700 dark:border-yellow-500 [&>svg]:text-yellow-500', + info: 'border-blue-500/50 text-blue-700 dark:border-blue-500 [&>svg]:text-blue-500', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +interface AlertProps + extends React.HTMLAttributes, + VariantProps {} + +const Alert = React.forwardRef( + ({ className, variant, ...props }, ref) => ( +
+ ) +); +Alert.displayName = 'Alert'; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = 'AlertTitle'; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = 'AlertDescription'; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/frontend/app/components/ui/Button.tsx b/frontend/app/components/ui/Button.tsx new file mode 100644 index 0000000..349b9d9 --- /dev/null +++ b/frontend/app/components/ui/Button.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '../../lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: + 'border border-input hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'underline-offset-4 hover:underline text-primary', + }, + size: { + default: 'h-10 py-2 px-4', + sm: 'h-9 px-3 rounded-md', + lg: 'h-11 px-8 rounded-md', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + } +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/frontend/app/components/ui/LoadingSpinner.tsx b/frontend/app/components/ui/LoadingSpinner.tsx new file mode 100644 index 0000000..22dc408 --- /dev/null +++ b/frontend/app/components/ui/LoadingSpinner.tsx @@ -0,0 +1,71 @@ +import React from 'react'; + +interface LoadingSpinnerProps { + size?: 'sm' | 'md' | 'lg'; + color?: 'primary' | 'secondary' | 'accent' | 'white'; + className?: string; +} + +const sizeClasses = { + sm: 'h-4 w-4 border-2', + md: 'h-8 w-8 border-2', + lg: 'h-12 w-12 border-4', +}; + +const colorClasses = { + primary: 'border-indigo-500 border-t-transparent', + secondary: 'border-gray-500 border-t-transparent', + accent: 'border-pink-500 border-t-transparent', + white: 'border-white border-t-transparent', +}; + +export function LoadingSpinner({ + size = 'md', + color = 'primary', + className = '', +}: LoadingSpinnerProps) { + return ( +
+ +
+ ); +} + +interface LoadingOverlayProps { + isLoading: boolean; + text?: string; + spinnerSize?: 'sm' | 'md' | 'lg'; + className?: string; +} + +export function LoadingOverlay({ + isLoading, + text = 'Loading...', + spinnerSize = 'md', + className = '', +}: LoadingOverlayProps) { + if (!isLoading) return null; + + return ( +
+
+ + {text &&

{text}

} +
+
+ ); +} + +export function InlineLoading({ text = 'Loading...', className = '' }) { + return ( +
+ + {text} +
+ ); +} diff --git a/frontend/app/hooks/useWebSocket.ts b/frontend/app/hooks/useWebSocket.ts new file mode 100644 index 0000000..855baad --- /dev/null +++ b/frontend/app/hooks/useWebSocket.ts @@ -0,0 +1,148 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; +import WebSocketService from '../services/websocket/WebSocketService'; + +type WebSocketState = 'connecting' | 'connected' | 'disconnected' | 'error'; + +interface UseWebSocketOptions { + url: string; + autoReconnect?: boolean; + maxReconnectAttempts?: number; + onMessage?: (data: any) => void; + onConnect?: () => void; + onDisconnect?: () => void; + onError?: (error: Error) => void; +} + +export function useWebSocket({ + url, + autoReconnect = true, + maxReconnectAttempts = 5, + onMessage, + onConnect, + onDisconnect, + onError, +}: UseWebSocketOptions) { + const [state, setState] = useState('connecting'); + const [reconnectAttempts, setReconnectAttempts] = useState(0); + const wsService = useRef(null); + const reconnectTimeout = useRef(null); + + // Initialize WebSocket connection + const connect = useCallback(() => { + if (wsService.current) { + wsService.current.disconnect(); + } + + setState('connecting'); + wsService.current = new WebSocketService(url); + + const ws = wsService.current; + + ws.on('connect', () => { + setState('connected'); + setReconnectAttempts(0); + onConnect?.(); + }); + + ws.on('disconnect', () => { + setState('disconnected'); + onDisconnect?.(); + }); + + ws.on('reconnecting', ({ attempt }: { attempt: number }) => { + setState('connecting'); + setReconnectAttempts(attempt); + }); + + ws.on('message', (data: any) => { + onMessage?.(data); + }); + + ws.on('error', (error: Error) => { + console.error('WebSocket error:', error); + setState('error'); + onError?.(error); + }); + + return () => { + if (autoReconnect && reconnectTimeout.current) { + clearTimeout(reconnectTimeout.current); + } + ws.disconnect(); + }; + }, [url, autoReconnect, onMessage, onConnect, onDisconnect, onError]); + + // Auto-reconnect logic + useEffect(() => { + if (!autoReconnect || state !== 'disconnected' || reconnectAttempts >= maxReconnectAttempts) { + return; + } + + const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); + + reconnectTimeout.current = setTimeout(() => { + if (wsService.current) { + wsService.current.connect(); + } else { + connect(); + } + }, delay); + + return () => { + if (reconnectTimeout.current) { + clearTimeout(reconnectTimeout.current); + } + }; + }, [state, reconnectAttempts, autoReconnect, maxReconnectAttempts, connect]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (wsService.current) { + wsService.current.disconnect(); + } + if (reconnectTimeout.current) { + clearTimeout(reconnectTimeout.current); + } + }; + }, []); + + // Send message through WebSocket + const send = useCallback((message: any) => { + if (!wsService.current) { + console.error('WebSocket is not connected'); + return false; + } + return wsService.current.send(message); + }, []); + + // Manually reconnect + const reconnect = useCallback(() => { + if (wsService.current) { + wsService.current.connect(); + } else { + connect(); + } + }, [connect]); + + // Disconnect WebSocket + const disconnect = useCallback(() => { + if (wsService.current) { + wsService.current.disconnect(); + } + }, []); + + return { + state, + reconnectAttempts, + send, + reconnect, + disconnect, + isConnected: state === 'connected', + isConnecting: state === 'connecting', + isDisconnected: state === 'disconnected', + hasError: state === 'error', + }; +} + +export default useWebSocket; diff --git a/frontend/app/services/websocket/WebSocketService.ts b/frontend/app/services/websocket/WebSocketService.ts new file mode 100644 index 0000000..f886b27 --- /dev/null +++ b/frontend/app/services/websocket/WebSocketService.ts @@ -0,0 +1,193 @@ +import { EventEmitter } from 'events'; + +type WebSocketEvent = 'connect' | 'disconnect' | 'message' | 'error' | 'reconnect' | 'reconnecting'; + +class WebSocketService extends EventEmitter { + private ws: WebSocket | null = null; + private url: string; + private reconnectAttempts: number = 0; + private maxReconnectAttempts: number = 5; + private reconnectInterval: number = 3000; // 3 seconds + private shouldReconnect: boolean = true; + private messageQueue: any[] = []; + private isConnected: boolean = false; + private connectionTimeout: NodeJS.Timeout | null = null; + private pingInterval: NodeJS.Timeout | null = null; + + constructor(url: string) { + super(); + this.url = url; + this.connect(); + } + + connect() { + if (this.isConnected) return; + + try { + this.ws = new WebSocket(this.url); + this.setupEventListeners(); + + // Set connection timeout + this.connectionTimeout = setTimeout(() => { + if (!this.isConnected) { + this.handleDisconnect(); + } + }, 10000); // 10 second connection timeout + + } catch (error) { + console.error('WebSocket connection error:', error); + this.handleDisconnect(); + } + } + + private setupEventListeners() { + if (!this.ws) return; + + this.ws.onopen = () => this.handleConnect(); + this.ws.onclose = () => this.handleDisconnect(); + this.ws.onerror = (error) => this.handleError(error); + this.ws.onmessage = (event) => this.handleMessage(event); + } + + private handleConnect() { + if (this.connectionTimeout) { + clearTimeout(this.connectionTimeout); + this.connectionTimeout = null; + } + + this.isConnected = true; + this.reconnectAttempts = 0; + this.emit('connect'); + + // Start ping-pong to keep connection alive + this.startPingPong(); + + // Process any queued messages + this.processMessageQueue(); + + console.log('WebSocket connected'); + } + + private handleDisconnect() { + this.isConnected = false; + this.cleanup(); + + if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++; + const delay = this.calculateReconnectDelay(); + + console.log(`WebSocket disconnected. Reconnecting in ${delay}ms... (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); + + setTimeout(() => { + this.emit('reconnecting', { attempt: this.reconnectAttempts }); + this.connect(); + }, delay); + } else if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error('Max reconnection attempts reached'); + this.emit('error', new Error('Max reconnection attempts reached')); + } + + this.emit('disconnect'); + } + + private handleError(error: Event) { + console.error('WebSocket error:', error); + this.emit('error', error); + } + + private handleMessage(event: MessageEvent) { + try { + const data = JSON.parse(event.data); + this.emit('message', data); + } catch (error) { + console.error('Error parsing WebSocket message:', error); + this.emit('error', error); + } + } + + private calculateReconnectDelay(): number { + // Exponential backoff with jitter + const baseDelay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); + const jitter = Math.random() * 1000; // Add up to 1 second of jitter + return Math.min(baseDelay + jitter, 30000); // Max 30 seconds + } + + private startPingPong() { + if (this.pingInterval) clearInterval(this.pingInterval); + + this.pingInterval = setInterval(() => { + if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) { + this.send({ type: 'ping', timestamp: Date.now() }); + } + }, 30000); // Send ping every 30 seconds + } + + private processMessageQueue() { + while (this.messageQueue.length > 0 && this.isConnected) { + const message = this.messageQueue.shift(); + this.send(message); + } + } + + send(message: any) { + if (!this.isConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) { + // Queue the message if not connected + this.messageQueue.push(message); + return false; + } + + try { + const messageString = typeof message === 'string' ? message : JSON.stringify(message); + this.ws.send(messageString); + return true; + } catch (error) { + console.error('Error sending WebSocket message:', error); + this.emit('error', error); + return false; + } + } + + disconnect() { + this.shouldReconnect = false; + this.cleanup(); + } + + private cleanup() { + if (this.ws) { + this.ws.onopen = null; + this.ws.onclose = null; + this.ws.onerror = null; + this.ws.onmessage = null; + + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.close(); + } + + this.ws = null; + } + + if (this.connectionTimeout) { + clearTimeout(this.connectionTimeout); + this.connectionTimeout = null; + } + + if (this.pingInterval) { + clearInterval(this.pingInterval); + this.pingInterval = null; + } + + this.isConnected = false; + } + + on(event: WebSocketEvent, listener: (...args: any[]) => void) { + super.on(event, listener); + return this; + } + + off(event: WebSocketEvent, listener: (...args: any[]) => void) { + super.off(event, listener); + return this; + } +} + +export default WebSocketService; diff --git a/frontend/docs/PERFORMANCE_OPTIMIZATIONS.md b/frontend/docs/PERFORMANCE_OPTIMIZATIONS.md new file mode 100644 index 0000000..64b9d88 --- /dev/null +++ b/frontend/docs/PERFORMANCE_OPTIMIZATIONS.md @@ -0,0 +1,104 @@ +# Performance Optimizations + +This document outlines the performance optimizations implemented in the application to enhance the user experience, particularly for real-time data visualization and WebSocket communication. + +## WebSocket Implementation + +### Features +- **Automatic Reconnection**: Implements exponential backoff with jitter for reconnection attempts +- **Connection State Management**: Tracks connection state (connecting, connected, disconnected, error) +- **Message Queueing**: Queues messages when offline and sends them when reconnected +- **Ping/Pong Heartbeat**: Maintains connection health with periodic pings +- **Error Handling**: Comprehensive error handling and recovery mechanisms + +### Key Components + +1. **WebSocketService** (`/services/websocket/WebSocketService.ts`) + - Core WebSocket wrapper with reconnection logic + - Message queuing and delivery guarantees + - Event-based architecture + +2. **useWebSocket Hook** (`/hooks/useWebSocket.ts`) + - React hook for WebSocket integration + - Manages connection state and lifecycle + - Provides simple API for components + +## Code Splitting + +### Dynamic Imports +- Components are dynamically imported using Next.js dynamic imports +- Reduces initial bundle size by loading components only when needed + +### Lazy Loading +- Heavy components are loaded only when they enter the viewport +- Uses Intersection Observer API for efficient loading + +## Loading States + +### Loading Components +- **LoadingSpinner**: A customizable loading spinner with different sizes and colors +- **LoadingOverlay**: A full-screen loading overlay with optional text +- **InlineLoading**: An inline loading indicator for small UI elements + +### Skeleton Loaders +- Placeholder components that mimic content structure +- Reduces layout shift and improves perceived performance + +## Real-time Data Visualization + +### Optimized Chart Rendering +- Data points are limited to prevent performance degradation +- Smooth animations with hardware acceleration +- Efficient updates with React.memo and useMemo + +## Best Practices + +1. **Minimize Re-renders** + - Use React.memo for pure components + - Use useCallback and useMemo to prevent unnecessary recalculations + - Implement shouldComponentUpdate for class components + +2. **Efficient Data Fetching** + - Implement data pagination + - Use SWR or React Query for data fetching and caching + - Debounce or throttle rapid data updates + +3. **Bundle Optimization** + - Code splitting with dynamic imports + - Tree shaking to eliminate dead code + - Lazy loading of non-critical components + +4. **Memory Management** + - Clean up event listeners and subscriptions + - Use Web Workers for CPU-intensive tasks + - Implement virtualization for large lists + +## Monitoring and Metrics + +### Performance Monitoring +- Web Vitals tracking +- Custom performance metrics for critical user journeys +- Error tracking and reporting + +### Logging +- Structured logging for WebSocket events +- Performance timing metrics +- Error and warning logging + +## Future Improvements + +1. **Server-Sent Events (SSE)** + - Consider using SSE for one-way server-to-client communication + - Lower overhead than WebSockets for certain use cases + +2. **WebRTC** + - For peer-to-peer real-time communication + - Reduces server load for direct client communication + +3. **Service Workers** + - Offline support and background sync + - Caching strategies for better performance + +4. **WebAssembly** + - For compute-intensive operations + - Potential for near-native performance in the browser diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts new file mode 100644 index 0000000..9ad0df4 --- /dev/null +++ b/frontend/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 26e29ee..f88b1ee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@radix-ui/react-slot": "^1.2.4", "@reown/appkit": "^1.8.14", "@reown/appkit-adapter-wagmi": "^1.8.14", "@somnia-chain/streams": "^0.9.5", @@ -18,6 +19,7 @@ "@web3modal/ethers": "^5.1.11", "@web3modal/wagmi": "^5.1.11", "chart.js": "^4.5.1", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "ethers": "^6.15.0", "lucide-react": "^0.553.0", @@ -3207,6 +3209,39 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@reactflow/background": { "version": "11.3.14", "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", @@ -9215,6 +9250,18 @@ "dev": true, "license": "MIT" }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/classcat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3679b5b..939b0b7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.4", "@reown/appkit": "^1.8.14", "@reown/appkit-adapter-wagmi": "^1.8.14", "@somnia-chain/streams": "^0.9.5", @@ -19,6 +20,7 @@ "@web3modal/ethers": "^5.1.11", "@web3modal/wagmi": "^5.1.11", "chart.js": "^4.5.1", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "ethers": "^6.15.0", "lucide-react": "^0.553.0", diff --git a/frontend/types/components.d.ts b/frontend/types/components.d.ts new file mode 100644 index 0000000..a96dc42 --- /dev/null +++ b/frontend/types/components.d.ts @@ -0,0 +1,38 @@ +// Type definitions for custom UI components + +declare module '@/components/ui/Alert' { + import { ComponentType } from 'react'; + + interface AlertProps { + variant?: 'default' | 'destructive' | 'success' | 'warning' | 'info'; + className?: string; + children: React.ReactNode; + } + + interface AlertTitleProps { + className?: string; + children: React.ReactNode; + } + + interface AlertDescriptionProps { + className?: string; + children: React.ReactNode; + } + + export const Alert: ComponentType; + export const AlertTitle: ComponentType; + export const AlertDescription: ComponentType; +} + +declare module '@/components/ui/Button' { + import { ComponentType, ButtonHTMLAttributes } from 'react'; + + interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; + size?: 'default' | 'sm' | 'lg' | 'icon'; + asChild?: boolean; + } + + export const Button: ComponentType; + export const buttonVariants: any; +}