di usage in website

This commit is contained in:
2026-01-06 19:36:03 +01:00
parent 589b55a87e
commit e589c30bf8
191 changed files with 6367 additions and 4253 deletions

View File

@@ -1,8 +1,7 @@
'use client';
import { ReactNode } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useServices } from '@/lib/services/ServiceProvider';
import { useCapability } from '@/hooks/useCapability';
type CapabilityGateProps = {
capabilityKey: string;
@@ -17,26 +16,17 @@ export function CapabilityGate({
fallback = null,
comingSoon = null,
}: CapabilityGateProps) {
const { policyService } = useServices();
const { isLoading, isError, capabilityState } = useCapability(capabilityKey);
const { data, isLoading, isError } = useQuery({
queryKey: ['policySnapshot'],
queryFn: () => policyService.getSnapshot(),
staleTime: 60_000,
gcTime: 5 * 60_000,
});
if (isLoading || isError || !data) {
if (isLoading || isError || !capabilityState) {
return <>{fallback}</>;
}
const state = policyService.getCapabilityState(data, capabilityKey);
if (state === 'enabled') {
if (capabilityState === 'enabled') {
return <>{children}</>;
}
if (state === 'coming_soon') {
if (capabilityState === 'coming_soon') {
return <>{comingSoon ?? fallback}</>;
}

View File

@@ -1,39 +0,0 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import Button from '@/components/ui/Button';
interface EmptyStateProps {
icon: LucideIcon;
title: string;
description?: string;
action?: {
label: string;
onClick: () => void;
};
className?: string;
}
export const EmptyState = ({
icon: Icon,
title,
description,
action,
className = ''
}: EmptyStateProps) => (
<div className={`text-center py-12 ${className}`}>
<div className="max-w-md mx-auto">
<div className="flex h-16 w-16 mx-auto items-center justify-center rounded-2xl bg-iron-gray/60 border border-charcoal-outline/50 mb-6">
<Icon className="w-8 h-8 text-gray-500" />
</div>
<h3 className="text-xl font-semibold text-white mb-3">{title}</h3>
{description && (
<p className="text-gray-400 mb-8">{description}</p>
)}
{action && (
<Button variant="primary" onClick={action.onClick} className="mx-auto">
{action.label}
</Button>
)}
</div>
</div>
);

View File

@@ -1,15 +0,0 @@
import React from 'react';
interface LoadingStateProps {
message?: string;
className?: string;
}
export const LoadingState = ({ message = 'Loading...', className = '' }: LoadingStateProps) => (
<div className={`flex items-center justify-center min-h-[200px] ${className}`}>
<div className="flex flex-col items-center gap-4">
<div className="w-10 h-10 border-2 border-primary-blue border-t-transparent rounded-full animate-spin" />
<p className="text-gray-400">{message}</p>
</div>
</div>
);

View File

@@ -1,374 +0,0 @@
'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
import { UseDataFetchingOptions, UseDataFetchingResult } from '../types/state.types';
import { delay, retryWithBackoff } from '@/lib/utils/errorUtils';
/**
* useDataFetching Hook
*
* Unified data fetching hook with built-in state management, error handling,
* retry logic, and caching support.
*
* Features:
* - Automatic loading state management
* - Error classification and handling
* - Built-in retry with exponential backoff
* - Cache and stale time support
* - Refetch capability
* - Success/error callbacks
* - Auto-retry on mount for recoverable errors
*
* Usage Example:
* ```typescript
* const { data, isLoading, error, retry, refetch } = useDataFetching({
* queryKey: ['dashboardOverview'],
* queryFn: () => dashboardService.getDashboardOverview(),
* retryOnMount: true,
* cacheTime: 5 * 60 * 1000,
* onSuccess: (data) => console.log('Loaded:', data),
* onError: (error) => console.error('Error:', error),
* });
* ```
*/
export function useDataFetching<T>(
options: UseDataFetchingOptions<T>
): UseDataFetchingResult<T> {
const {
queryKey,
queryFn,
enabled = true,
retryOnMount = false,
cacheTime = 5 * 60 * 1000, // 5 minutes
staleTime = 1 * 60 * 1000, // 1 minute
maxRetries = 3,
retryDelay = 1000,
onSuccess,
onError,
} = options;
// State management
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isFetching, setIsFetching] = useState<boolean>(false);
const [error, setError] = useState<ApiError | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [isStale, setIsStale] = useState<boolean>(true);
// Refs for caching and retry logic
const cacheRef = useRef<{
data: T | null;
timestamp: number;
isStale: boolean;
} | null>(null);
const retryCountRef = useRef<number>(0);
const isMountedRef = useRef<boolean>(true);
// Check if cache is valid
const isCacheValid = useCallback((): boolean => {
if (!cacheRef.current) return false;
const now = Date.now();
const age = now - cacheRef.current.timestamp;
// Cache is valid if within cacheTime and not stale
return age < cacheTime && !cacheRef.current.isStale;
}, [cacheTime]);
// Update cache
const updateCache = useCallback((newData: T | null, isStale: boolean = false) => {
cacheRef.current = {
data: newData,
timestamp: Date.now(),
isStale,
};
}, []);
// Main fetch function
const fetch = useCallback(async (isRetry: boolean = false): Promise<T | null> => {
if (!enabled) {
return null;
}
// Check cache first
if (!isRetry && isCacheValid() && cacheRef.current && cacheRef.current.data !== null) {
setData(cacheRef.current.data);
setIsLoading(false);
setIsFetching(false);
setError(null);
setLastUpdated(new Date(cacheRef.current.timestamp));
setIsStale(false);
return cacheRef.current.data;
}
setIsFetching(true);
if (!isRetry) {
setIsLoading(true);
}
setError(null);
try {
// Execute the fetch with retry logic
const result = await retryWithBackoff(
async () => {
retryCountRef.current++;
return await queryFn();
},
maxRetries,
retryDelay
);
if (!isMountedRef.current) {
return null;
}
// Success - update state and cache
setData(result);
setLastUpdated(new Date());
setIsStale(false);
updateCache(result, false);
retryCountRef.current = 0; // Reset retry count on success
if (onSuccess) {
onSuccess(result);
}
return result;
} catch (err) {
if (!isMountedRef.current) {
return null;
}
// Convert to ApiError if needed
const apiError = err instanceof ApiError ? err : new ApiError(
err instanceof Error ? err.message : 'An unexpected error occurred',
'UNKNOWN_ERROR',
{
timestamp: new Date().toISOString(),
retryCount: retryCountRef.current,
wasRetry: isRetry,
},
err instanceof Error ? err : undefined
);
setError(apiError);
if (onError) {
onError(apiError);
}
// Mark cache as stale on error
if (cacheRef.current) {
cacheRef.current.isStale = true;
setIsStale(true);
}
throw apiError;
} finally {
setIsLoading(false);
setIsFetching(false);
}
}, [enabled, isCacheValid, queryFn, maxRetries, retryDelay, updateCache, onSuccess, onError]);
// Retry function
const retry = useCallback(async () => {
return await fetch(true);
}, [fetch]);
// Refetch function
const refetch = useCallback(async () => {
// Force bypass cache
cacheRef.current = null;
return await fetch(false);
}, [fetch]);
// Initial fetch and auto-retry on mount
useEffect(() => {
isMountedRef.current = true;
const initialize = async () => {
if (!enabled) return;
// Check if we should auto-retry on mount
const shouldRetryOnMount = retryOnMount && error && error.isRetryable();
if (shouldRetryOnMount) {
try {
await retry();
} catch (err) {
// Error already set by retry
}
} else if (!data && !error) {
// Initial fetch
try {
await fetch(false);
} catch (err) {
// Error already set by fetch
}
}
};
initialize();
return () => {
isMountedRef.current = false;
};
}, [enabled, retryOnMount]); // eslint-disable-line react-hooks/exhaustive-deps
// Effect to check staleness
useEffect(() => {
if (!lastUpdated) return;
const checkStale = () => {
if (!lastUpdated) return;
const now = Date.now();
const age = now - lastUpdated.getTime();
if (age > staleTime) {
setIsStale(true);
if (cacheRef.current) {
cacheRef.current.isStale = true;
}
}
};
const interval = setInterval(checkStale, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, [lastUpdated, staleTime]);
// Effect to update cache staleness
useEffect(() => {
if (isStale && cacheRef.current) {
cacheRef.current.isStale = true;
}
}, [isStale]);
// Clear cache function (useful for manual cache invalidation)
const clearCache = useCallback(() => {
cacheRef.current = null;
setIsStale(true);
}, []);
// Reset function (clears everything)
const reset = useCallback(() => {
setData(null);
setIsLoading(false);
setIsFetching(false);
setError(null);
setLastUpdated(null);
setIsStale(true);
cacheRef.current = null;
retryCountRef.current = 0;
}, []);
return {
data,
isLoading,
isFetching,
error,
retry,
refetch,
lastUpdated,
isStale,
// Additional utility functions (not part of standard interface but useful)
_clearCache: clearCache,
_reset: reset,
} as UseDataFetchingResult<T>;
}
/**
* useDataFetchingWithPagination Hook
*
* Extension of useDataFetching for paginated data
*/
export function useDataFetchingWithPagination<T>(
options: UseDataFetchingOptions<T[]> & {
initialPage?: number;
pageSize?: number;
}
) {
const {
initialPage = 1,
pageSize = 10,
queryFn,
...restOptions
} = options;
const [page, setPage] = useState<number>(initialPage);
const [hasMore, setHasMore] = useState<boolean>(true);
const paginatedQueryFn = useCallback(async () => {
const result = await queryFn();
// Check if there's more data
if (Array.isArray(result)) {
setHasMore(result.length === pageSize);
}
return result;
}, [queryFn, pageSize]);
const result = useDataFetching<T[]>({
...restOptions,
queryFn: paginatedQueryFn,
});
const loadMore = useCallback(async () => {
if (!hasMore) return;
const nextPage = page + 1;
setPage(nextPage);
// This would need to be integrated with the actual API
// For now, we'll just refetch which may not be ideal
await result.refetch();
}, [page, hasMore, result]);
const resetPagination = useCallback(() => {
setPage(initialPage);
setHasMore(true);
if (result._reset) {
result._reset();
}
}, [initialPage, result]);
return {
...result,
page,
hasMore,
loadMore,
resetPagination,
};
}
/**
* useDataFetchingWithRefresh Hook
*
* Extension with automatic refresh capability
*/
export function useDataFetchingWithRefresh<T>(
options: UseDataFetchingOptions<T> & {
refreshInterval?: number; // milliseconds
}
) {
const { refreshInterval, ...restOptions } = options;
const result = useDataFetching<T>(restOptions);
useEffect(() => {
if (!refreshInterval) return;
const interval = setInterval(() => {
if (!result.isLoading && !result.isFetching) {
result.refetch();
}
}, refreshInterval);
return () => clearInterval(interval);
}, [refreshInterval, result]);
return result;
}

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { EmptyStateProps } from '../types/state.types';
import { EmptyStateProps } from './types';
import Button from '@/components/ui/Button';
// Illustration components (simple SVG representations)

View File

@@ -3,7 +3,7 @@
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { AlertTriangle, Wifi, RefreshCw, ArrowLeft, Home, X, Info } from 'lucide-react';
import { ErrorDisplayProps } from '../types/state.types';
import { ErrorDisplayProps } from './types';
import Button from '@/components/ui/Button';
/**
@@ -70,12 +70,6 @@ export function ErrorDisplay({
// Icon based on error type
const ErrorIcon = isConnectivity ? Wifi : AlertTriangle;
// Common button styles
const buttonBase = 'flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors disabled:opacity-50';
const primaryButton = `${buttonBase} bg-red-500 hover:bg-red-600 text-white`;
const secondaryButton = `${buttonBase} bg-iron-gray hover:bg-charcoal-outline text-gray-300 border border-charcoal-outline`;
const ghostButton = `${buttonBase} hover:bg-iron-gray/50 text-gray-300`;
// Render different variants
switch (variant) {
case 'full-screen':
@@ -125,11 +119,11 @@ export function ErrorDisplay({
{/* Action Buttons */}
<div className="flex flex-col gap-2 pt-2">
{isRetryable && onRetry && (
<button
<Button
variant="danger"
onClick={handleRetry}
disabled={isRetrying}
className={primaryButton}
aria-label={isRetrying ? 'Retrying...' : 'Try again'}
className="w-full"
>
{isRetrying ? (
<>
@@ -142,55 +136,46 @@ export function ErrorDisplay({
Try Again
</>
)}
</button>
</Button>
)}
{showNavigation && (
<div className="flex gap-2">
<button
<Button
variant="secondary"
onClick={handleGoBack}
className={`${secondaryButton} flex-1`}
aria-label="Go back to previous page"
className="flex-1"
>
<ArrowLeft className="w-4 h-4" />
Go Back
</button>
</Button>
<button
<Button
variant="secondary"
onClick={handleGoHome}
className={`${secondaryButton} flex-1`}
aria-label="Go to home page"
className="flex-1"
>
<Home className="w-4 h-4" />
Home
</button>
</Button>
</div>
)}
{/* Custom Actions */}
{actions.length > 0 && (
<div className="flex flex-col gap-2 pt-2 border-t border-charcoal-outline/50">
{actions.map((action, index) => {
const variantClasses = {
primary: 'bg-primary-blue hover:bg-blue-600 text-white',
secondary: 'bg-iron-gray hover:bg-charcoal-outline text-gray-300 border border-charcoal-outline',
danger: 'bg-red-600 hover:bg-red-700 text-white',
ghost: 'hover:bg-iron-gray/50 text-gray-300',
}[action.variant || 'secondary'];
return (
<button
key={index}
onClick={action.onClick}
disabled={action.disabled}
className={`${buttonBase} ${variantClasses} ${action.disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
aria-label={action.label}
>
{action.icon && <action.icon className="w-4 h-4" />}
{action.label}
</button>
);
})}
{actions.map((action, index) => (
<Button
key={index}
variant={action.variant || 'secondary'}
onClick={action.onClick}
disabled={action.disabled}
className="w-full"
>
{action.icon && <action.icon className="w-4 h-4" />}
{action.label}
</Button>
))}
</div>
)}
</div>

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { LoadingWrapperProps } from '../types/state.types';
import { LoadingWrapperProps } from './types';
/**
* LoadingWrapper Component

View File

@@ -1,7 +1,7 @@
'use client';
import React, { ReactNode } from 'react';
import { StateContainerProps, StateContainerConfig } from '../types/state.types';
import { StateContainerProps, StateContainerConfig } from './types';
import { LoadingWrapper } from './LoadingWrapper';
import { ErrorDisplay } from './ErrorDisplay';
import { EmptyState } from './EmptyState';
@@ -52,7 +52,7 @@ export function StateContainer<T>({
isEmpty,
}: StateContainerProps<T>) {
// Determine if data is empty
const isDataEmpty = (data: T | null): boolean => {
const isDataEmpty = (data: T | null | undefined): boolean => {
if (data === null || data === undefined) return true;
if (isEmpty) return isEmpty(data);
@@ -156,7 +156,7 @@ export function StateContainer<T>({
);
}
// At this point, data is guaranteed to be non-null
// At this point, data is guaranteed to be non-null and non-undefined
return <>{children(data as T)}</>;
}

View File

@@ -1,59 +0,0 @@
/**
* Basic test file to verify state components are properly exported and typed
*/
import { LoadingWrapper } from '../LoadingWrapper';
import { ErrorDisplay } from '../ErrorDisplay';
import { EmptyState } from '../EmptyState';
import { StateContainer } from '../StateContainer';
import { useDataFetching } from '../../hooks/useDataFetching';
import { ApiError } from '@/lib/api/base/ApiError';
// This file just verifies that all components can be imported and are properly typed
// Full testing would be done in separate test files
describe('State Components - Basic Type Checking', () => {
it('should export all components', () => {
expect(LoadingWrapper).toBeDefined();
expect(ErrorDisplay).toBeDefined();
expect(EmptyState).toBeDefined();
expect(StateContainer).toBeDefined();
expect(useDataFetching).toBeDefined();
});
it('should have proper component signatures', () => {
// LoadingWrapper accepts props
const loadingProps = {
variant: 'spinner' as const,
message: 'Loading...',
size: 'md' as const,
};
expect(loadingProps).toBeDefined();
// ErrorDisplay accepts ApiError
const mockError = new ApiError(
'Test error',
'NETWORK_ERROR',
{ timestamp: new Date().toISOString() }
);
expect(mockError).toBeDefined();
expect(mockError.isRetryable()).toBe(true);
// EmptyState accepts icon and title
const emptyProps = {
icon: require('lucide-react').Activity,
title: 'No data',
};
expect(emptyProps).toBeDefined();
// StateContainer accepts data and state
const stateProps = {
data: null,
isLoading: false,
error: null,
retry: async () => {},
children: (data: any) => <div>{JSON.stringify(data)}</div>,
};
expect(stateProps).toBeDefined();
});
});

View File

@@ -0,0 +1,116 @@
'use client';
import { LucideIcon } from 'lucide-react';
import { ReactNode } from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
// ==================== EMPTY STATE TYPES ====================
export interface EmptyStateAction {
label: string;
onClick: () => void;
icon?: LucideIcon;
variant?: 'primary' | 'secondary' | 'danger' | 'race-performance' | 'race-final';
}
export interface EmptyStateProps {
icon: LucideIcon;
title: string;
description?: string;
action?: EmptyStateAction;
variant?: 'default' | 'minimal' | 'full-page';
className?: string;
illustration?: 'racing' | 'league' | 'team' | 'sponsor' | 'driver';
ariaLabel?: string;
}
// ==================== LOADING STATE TYPES ====================
export interface LoadingCardConfig {
count?: number;
height?: string;
className?: string;
}
export interface LoadingWrapperProps {
variant?: 'spinner' | 'skeleton' | 'full-screen' | 'inline' | 'card';
message?: string;
className?: string;
size?: 'sm' | 'md' | 'lg';
skeletonCount?: number;
cardConfig?: LoadingCardConfig;
ariaLabel?: string;
}
// ==================== ERROR STATE TYPES ====================
export interface ErrorAction {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger' | 'race-performance' | 'race-final';
icon?: LucideIcon;
disabled?: boolean;
}
export interface ErrorDisplayProps {
error: ApiError;
onRetry?: () => void;
variant?: 'full-screen' | 'inline' | 'card' | 'toast';
showRetry?: boolean;
showNavigation?: boolean;
actions?: ErrorAction[];
className?: string;
hideTechnicalDetails?: boolean;
ariaLabel?: string;
}
// ==================== STATE CONTAINER TYPES ====================
export interface StateContainerConfig<T> {
loading?: {
variant?: LoadingWrapperProps['variant'];
message?: string;
size?: LoadingWrapperProps['size'];
skeletonCount?: number;
};
error?: {
variant?: ErrorDisplayProps['variant'];
showRetry?: boolean;
showNavigation?: boolean;
hideTechnicalDetails?: boolean;
actions?: ErrorAction[];
};
empty?: {
icon: LucideIcon;
title: string;
description?: string;
action?: EmptyStateAction;
illustration?: EmptyStateProps['illustration'];
};
customRender?: {
loading?: () => ReactNode;
error?: (error: ApiError) => ReactNode;
empty?: () => ReactNode;
};
}
export interface StateContainerProps<T> {
data: T | null | undefined;
isLoading: boolean;
error: ApiError | null;
retry: () => void;
children: (data: T) => ReactNode;
config?: StateContainerConfig<T>;
className?: string;
showEmpty?: boolean;
isEmpty?: (data: T) => boolean;
}
// ==================== CONVENIENCE PROP TYPES ====================
// For components that only need specific subsets of props
export type MinimalEmptyStateProps = Omit<EmptyStateProps, 'variant'>;
export type MinimalLoadingProps = Pick<LoadingWrapperProps, 'message' | 'className'>;
export type InlineLoadingProps = Pick<LoadingWrapperProps, 'message' | 'size' | 'className'>;
export type SkeletonLoadingProps = Pick<LoadingWrapperProps, 'skeletonCount' | 'className'>;
export type CardLoadingProps = Pick<LoadingWrapperProps, 'cardConfig' | 'className'>;

View File

@@ -1,386 +0,0 @@
/**
* TypeScript Interfaces for State Management Components
*
* Provides comprehensive type definitions for loading, error, and empty states
* across the GridPilot website application.
*/
import { ReactNode } from 'react';
import { LucideIcon } from 'lucide-react';
import { ApiError } from '@/lib/api/base/ApiError';
// ============================================================================
// Core State Interfaces
// ============================================================================
/**
* Basic state for any data fetching operation
*/
export interface PageState<T> {
data: T | null;
isLoading: boolean;
error: ApiError | null;
retry: () => Promise<void>;
}
/**
* Extended state with metadata for advanced use cases
*/
export interface PageStateWithMeta<T> extends PageState<T> {
isFetching: boolean;
refetch: () => Promise<void>;
lastUpdated: Date | null;
isStale: boolean;
}
// ============================================================================
// Hook Interfaces
// ============================================================================
/**
* Options for useDataFetching hook
*/
export interface UseDataFetchingOptions<T> {
/** Unique key for caching and invalidation */
queryKey: string[];
/** Function to fetch data */
queryFn: () => Promise<T>;
/** Enable/disable the query */
enabled?: boolean;
/** Auto-retry on mount for recoverable errors */
retryOnMount?: boolean;
/** Cache time in milliseconds */
cacheTime?: number;
/** Stale time in milliseconds */
staleTime?: number;
/** Maximum retry attempts */
maxRetries?: number;
/** Delay between retries in milliseconds */
retryDelay?: number;
/** Success callback */
onSuccess?: (data: T) => void;
/** Error callback */
onError?: (error: ApiError) => void;
}
/**
* Result from useDataFetching hook
*/
export interface UseDataFetchingResult<T> {
data: T | null;
isLoading: boolean;
isFetching: boolean;
error: ApiError | null;
retry: () => Promise<void>;
refetch: () => Promise<void>;
lastUpdated: Date | null;
isStale: boolean;
// Internal methods (not part of public API but needed for extensions)
_clearCache?: () => void;
_reset?: () => void;
}
// ============================================================================
// LoadingWrapper Component
// ============================================================================
export type LoadingVariant = 'spinner' | 'skeleton' | 'full-screen' | 'inline' | 'card';
export type LoadingSize = 'sm' | 'md' | 'lg';
export interface LoadingWrapperProps {
/** Visual variant of loading state */
variant?: LoadingVariant;
/** Custom message to display */
message?: string;
/** Additional CSS classes */
className?: string;
/** Size of loading indicator */
size?: LoadingSize;
/** For skeleton variant - number of skeleton items to show */
skeletonCount?: number;
/** For card variant - card layout configuration */
cardConfig?: {
height?: number;
count?: number;
className?: string;
};
/** ARIA label for accessibility */
ariaLabel?: string;
}
// ============================================================================
// ErrorDisplay Component
// ============================================================================
export type ErrorVariant = 'full-screen' | 'inline' | 'card' | 'toast';
export interface ErrorAction {
/** Button label */
label: string;
/** Click handler */
onClick: () => void;
/** Visual variant */
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
/** Optional icon */
icon?: LucideIcon;
/** Disabled state */
disabled?: boolean;
}
export interface ErrorDisplayProps {
/** The error to display */
error: ApiError;
/** Retry callback */
onRetry?: () => void;
/** Visual variant */
variant?: ErrorVariant;
/** Show retry button (auto-detected from error.isRetryable()) */
showRetry?: boolean;
/** Show navigation buttons */
showNavigation?: boolean;
/** Additional custom actions */
actions?: ErrorAction[];
/** Additional CSS classes */
className?: string;
/** Hide technical details in production */
hideTechnicalDetails?: boolean;
/** ARIA label for accessibility */
ariaLabel?: string;
}
// ============================================================================
// EmptyState Component
// ============================================================================
export type EmptyVariant = 'default' | 'minimal' | 'full-page';
export type EmptyIllustration = 'racing' | 'league' | 'team' | 'sponsor' | 'driver';
export interface EmptyStateProps {
/** Icon to display */
icon: LucideIcon;
/** Title text */
title: string;
/** Description text */
description?: string;
/** Primary action */
action?: {
label: string;
onClick: () => void;
icon?: LucideIcon;
variant?: 'primary' | 'secondary';
};
/** Visual variant */
variant?: EmptyVariant;
/** Additional CSS classes */
className?: string;
/** Illustration instead of icon */
illustration?: EmptyIllustration;
/** ARIA label for accessibility */
ariaLabel?: string;
}
// ============================================================================
// StateContainer Component
// ============================================================================
export interface StateContainerConfig<T> {
/** Loading state configuration */
loading?: {
variant?: LoadingVariant;
message?: string;
size?: LoadingSize;
skeletonCount?: number;
};
/** Error state configuration */
error?: {
variant?: ErrorVariant;
actions?: ErrorAction[];
showRetry?: boolean;
showNavigation?: boolean;
hideTechnicalDetails?: boolean;
};
/** Empty state configuration */
empty?: {
icon: LucideIcon;
title: string;
description?: string;
action?: {
label: string;
onClick: () => void;
};
};
/** Custom render functions for advanced use cases */
customRender?: {
loading?: () => ReactNode;
error?: (error: ApiError) => ReactNode;
empty?: () => ReactNode;
};
}
export interface StateContainerProps<T> {
/** Current data */
data: T | null;
/** Loading state */
isLoading: boolean;
/** Error state */
error: ApiError | null;
/** Retry function */
retry: () => Promise<void>;
/** Child render function */
children: (data: T) => ReactNode;
/** Configuration for all states */
config?: StateContainerConfig<T>;
/** Additional CSS classes */
className?: string;
/** Whether to show empty state (default: true) */
showEmpty?: boolean;
/** Custom function to determine if data is empty */
isEmpty?: (data: T) => boolean;
}
// ============================================================================
// Retry Configuration
// ============================================================================
export interface RetryConfig {
/** Maximum retry attempts */
maxAttempts?: number;
/** Base delay in milliseconds */
baseDelay?: number;
/** Backoff multiplier */
backoffMultiplier?: number;
/** Auto-retry on mount */
retryOnMount?: boolean;
}
// ============================================================================
// Notification Configuration
// ============================================================================
export interface NotificationConfig {
/** Show toast on success */
showToastOnSuccess?: boolean;
/** Show toast on error */
showToastOnError?: boolean;
/** Custom success message */
successMessage?: string;
/** Custom error message */
errorMessage?: string;
/** Auto-dismiss delay in milliseconds */
autoDismissDelay?: number;
}
// ============================================================================
// Analytics Configuration
// ============================================================================
export interface StateAnalytics {
/** Called when state changes */
onStateChange?: (from: string, to: string, data?: unknown) => void;
/** Called on error */
onError?: (error: ApiError, context: string) => void;
/** Called on retry */
onRetry?: (attempt: number, maxAttempts: number) => void;
}
// ============================================================================
// Performance Metrics
// ============================================================================
export interface PerformanceMetrics {
/** Time to first render in milliseconds */
timeToFirstRender?: number;
/** Time to data load in milliseconds */
timeToDataLoad?: number;
/** Number of retry attempts */
retryCount?: number;
/** Whether cache was hit */
cacheHit?: boolean;
}
// ============================================================================
// Advanced Configuration
// ============================================================================
export interface AdvancedStateConfig<T> extends StateContainerConfig<T> {
retry?: RetryConfig;
notifications?: NotificationConfig;
analytics?: StateAnalytics;
performance?: PerformanceMetrics;
}
// ============================================================================
// Page Template Interfaces
// ============================================================================
/**
* Generic page template props
*/
export interface PageTemplateProps<T> {
data: T | null;
isLoading: boolean;
error: ApiError | null;
retry: () => Promise<void>;
refetch: () => Promise<void>;
title?: string;
description?: string;
children: (data: T) => ReactNode;
config?: StateContainerConfig<T>;
}
/**
* List page template props
*/
export interface ListPageTemplateProps<T> extends PageTemplateProps<T[]> {
emptyConfig?: {
icon: LucideIcon;
title: string;
description?: string;
action?: {
label: string;
onClick: () => void;
};
};
showSkeleton?: boolean;
skeletonCount?: number;
}
/**
* Detail page template props
*/
export interface DetailPageTemplateProps<T> extends PageTemplateProps<T> {
onBack?: () => void;
onRefresh?: () => void;
}
// ============================================================================
// Default Configuration
// ============================================================================
export const DEFAULT_CONFIG = {
loading: {
variant: 'spinner' as LoadingVariant,
message: 'Loading...',
size: 'md' as LoadingSize,
},
error: {
variant: 'full-screen' as ErrorVariant,
showRetry: true,
showNavigation: true,
},
empty: {
title: 'No data available',
description: 'There is nothing to display here',
},
retry: {
maxAttempts: 3,
baseDelay: 1000,
backoffMultiplier: 2,
retryOnMount: true,
},
notifications: {
showToastOnSuccess: false,
showToastOnError: true,
autoDismissDelay: 5000,
},
} as const;