- Set up Tailwind CSS configuration for styling - Create Button component with variants (primary, secondary, outline, ghost, danger) - Create Input component with label, error, and helper text support - Create Card component with composable subcomponents (Header, Title, Description, Content, Footer) - Create LoadingSpinner component with size and fullscreen options - Create ErrorMessage component with retry and dismiss actions - Add comprehensive test suite using Jest and React Testing Library - Configure ESLint and Jest for quality assurance All components follow consistent design patterns from mobile app and include proper TypeScript types and accessibility features. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
import React from 'react';
|
|
|
|
interface ErrorMessageProps {
|
|
message: string;
|
|
onRetry?: () => void;
|
|
onDismiss?: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function ErrorMessage({
|
|
message,
|
|
onRetry,
|
|
onDismiss,
|
|
className = '',
|
|
}: ErrorMessageProps) {
|
|
return (
|
|
<div
|
|
className={`bg-red-50 border border-red-200 rounded-lg p-4 flex items-start justify-between ${className}`}
|
|
role="alert"
|
|
>
|
|
<div className="flex items-start gap-3 flex-1">
|
|
<svg
|
|
className="h-5 w-5 text-error flex-shrink-0 mt-0.5"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
<p className="text-sm text-error flex-1">{message}</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 ml-3">
|
|
{onRetry && (
|
|
<button
|
|
onClick={onRetry}
|
|
className="inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-primary hover:bg-red-100 rounded transition-colors"
|
|
type="button"
|
|
>
|
|
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
Retry
|
|
</button>
|
|
)}
|
|
{onDismiss && (
|
|
<button
|
|
onClick={onDismiss}
|
|
className="p-1 text-textMuted hover:text-textPrimary hover:bg-red-100 rounded transition-colors"
|
|
type="button"
|
|
aria-label="Dismiss"
|
|
>
|
|
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface FullScreenErrorProps {
|
|
title?: string;
|
|
message: string;
|
|
onRetry?: () => void;
|
|
}
|
|
|
|
export function FullScreenError({
|
|
title = 'Something went wrong',
|
|
message,
|
|
onRetry,
|
|
}: FullScreenErrorProps) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen bg-background p-6">
|
|
<div className="text-center max-w-md">
|
|
<svg
|
|
className="h-16 w-16 text-textMuted mx-auto mb-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
/>
|
|
</svg>
|
|
|
|
<h1 className="text-2xl font-semibold text-textPrimary mb-2">
|
|
{title}
|
|
</h1>
|
|
<p className="text-base text-textSecondary mb-6">{message}</p>
|
|
|
|
{onRetry && (
|
|
<button
|
|
onClick={onRetry}
|
|
className="inline-flex items-center gap-2 px-6 py-3 bg-primary text-white font-medium rounded-lg hover:bg-blue-600 transition-colors"
|
|
type="button"
|
|
>
|
|
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
Try Again
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|