// ───────────────────────────────────────────────────────────── // Reusable UI Components // ───────────────────────────────────────────────────────────── import React from 'react'; import { CheckStatus, SimilarityLevel } from './lib/types'; // ── InfoRow ─────────────────────────────────────────────────── interface InfoRowProps { label: string; value: React.ReactNode; icon?: React.ReactNode; } export const InfoRow: React.FC = ({ label, value, icon }) => (
{icon && {icon}} {label} {value}
); // ── SectionCard ─────────────────────────────────────────────── interface SectionCardProps { title: string; icon?: React.ReactNode; children: React.ReactNode; className?: string; accent?: 'blue' | 'green' | 'red' | 'amber' | 'violet'; } const accentMap: Record = { blue: 'border-t-blue-500', green: 'border-t-emerald-500', red: 'border-t-red-500', amber: 'border-t-amber-500', violet: 'border-t-violet-500', }; export const SectionCard: React.FC = ({ title, icon, children, className = '', accent = 'blue', }) => (

{icon && {icon}} {title}

{children}
); // ── StatusBadge ─────────────────────────────────────────────── interface StatusBadgeProps { status: CheckStatus; } const statusStyles: Record = { pending: 'bg-slate-100 text-slate-600', processing: 'bg-blue-100 text-blue-700', completed: 'bg-emerald-100 text-emerald-700', failed: 'bg-red-100 text-red-700', }; const statusDots: Record = { pending: 'bg-slate-400', processing: 'bg-blue-500 animate-pulse', completed: 'bg-emerald-500', failed: 'bg-red-500', }; const statusLabels: Record = { pending: 'Pending', processing: 'Processing', completed: 'Completed', failed: 'Failed', }; export const StatusBadge: React.FC = ({ status }) => ( {statusLabels[status]} ); // ── SimilarityMeter ─────────────────────────────────────────── interface SimilarityMeterProps { percentage: number; level: SimilarityLevel; } const levelColors: Record = { low: 'from-emerald-400 to-emerald-500', medium: 'from-amber-400 to-amber-500', high: 'from-red-400 to-red-500', }; const levelTextColors: Record = { low: 'text-emerald-600', medium: 'text-amber-600', high: 'text-red-600', }; const levelBgColors: Record = { low: 'bg-emerald-50 border-emerald-200', medium: 'bg-amber-50 border-amber-200', high: 'bg-red-50 border-red-200', }; const levelLabels: Record = { low: 'Low Similarity — Likely Original', medium: 'Medium Similarity — Review Recommended', high: 'High Similarity — Action Required', }; export const SimilarityMeter: React.FC = ({ percentage, level, }) => (
{percentage} % {level} risk
{/* Track */}
{/* Threshold markers */}
0% 30% 60% 100%
{levelLabels[level]}
); // ── Avatar ──────────────────────────────────────────────────── interface AvatarProps { name: string; avatarUrl?: string; size?: 'sm' | 'md' | 'lg'; } const sizeMap = { sm: 'w-8 h-8 text-xs', md: 'w-10 h-10 text-sm', lg: 'w-14 h-14 text-lg', }; export const Avatar: React.FC = ({ name, avatarUrl, size = 'md', }) => { const initials = name .split(' ') .map((n) => n[0]) .join('') .slice(0, 2) .toUpperCase(); if (avatarUrl) { return ( {name} ); } return (
{initials}
); }; // ── SkeletonLoader ──────────────────────────────────────────── export const SkeletonLoader: React.FC = () => (
{/* Header skeleton */}
{/* Cards skeleton */} {[1, 2, 3].map((i) => (
{[1, 2, 3].map((j) => (
))}
))}
); // ── ErrorState ──────────────────────────────────────────────── interface ErrorStateProps { message: string; onRetry?: () => void; } export const ErrorState: React.FC = ({ message, onRetry }) => (

Failed to load check

{message}

{onRetry && ( )}
); // ── FileTypeBadge ───────────────────────────────────────────── interface FileTypeBadgeProps { extension: string; } const extColors: Record = { PDF: 'bg-red-100 text-red-700', DOCX: 'bg-blue-100 text-blue-700', DOC: 'bg-blue-100 text-blue-700', TXT: 'bg-slate-100 text-slate-700', ODT: 'bg-green-100 text-green-700', }; export const FileTypeBadge: React.FC = ({ extension }) => ( {extension} );