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,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'>;