detail page done
This commit is contained in:
311
src/widgets/detail/index.tsx
Normal file
311
src/widgets/detail/index.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 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<InfoRowProps> = ({ label, value, icon }) => (
|
||||
<div className="flex items-start justify-between gap-4 py-3 border-b border-slate-100 last:border-0">
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-slate-500 min-w-[140px]">
|
||||
{icon && <span className="text-slate-400">{icon}</span>}
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-sm text-slate-800 text-right font-medium">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── SectionCard ───────────────────────────────────────────────
|
||||
|
||||
interface SectionCardProps {
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
accent?: 'blue' | 'green' | 'red' | 'amber' | 'violet';
|
||||
}
|
||||
|
||||
const accentMap: Record<string, string> = {
|
||||
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<SectionCardProps> = ({
|
||||
title,
|
||||
icon,
|
||||
children,
|
||||
className = '',
|
||||
accent = 'blue',
|
||||
}) => (
|
||||
<div
|
||||
className={`bg-white rounded-2xl shadow-sm border border-slate-100 border-t-4 ${accentMap[accent]} overflow-hidden ${className}`}
|
||||
>
|
||||
<div className="px-6 py-4 border-b border-slate-50">
|
||||
<h2 className="text-sm font-semibold text-slate-700 uppercase tracking-widest flex items-center gap-2">
|
||||
{icon && <span>{icon}</span>}
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="px-6 py-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── StatusBadge ───────────────────────────────────────────────
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: CheckStatus;
|
||||
}
|
||||
|
||||
const statusStyles: Record<CheckStatus, string> = {
|
||||
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<CheckStatus, string> = {
|
||||
pending: 'bg-slate-400',
|
||||
processing: 'bg-blue-500 animate-pulse',
|
||||
completed: 'bg-emerald-500',
|
||||
failed: 'bg-red-500',
|
||||
};
|
||||
|
||||
const statusLabels: Record<CheckStatus, string> = {
|
||||
pending: 'Pending',
|
||||
processing: 'Processing',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
};
|
||||
|
||||
export const StatusBadge: React.FC<StatusBadgeProps> = ({ status }) => (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold ${statusStyles[status]}`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${statusDots[status]}`} />
|
||||
{statusLabels[status]}
|
||||
</span>
|
||||
);
|
||||
|
||||
// ── SimilarityMeter ───────────────────────────────────────────
|
||||
|
||||
interface SimilarityMeterProps {
|
||||
percentage: number;
|
||||
level: SimilarityLevel;
|
||||
}
|
||||
|
||||
const levelColors: Record<SimilarityLevel, string> = {
|
||||
low: 'from-emerald-400 to-emerald-500',
|
||||
medium: 'from-amber-400 to-amber-500',
|
||||
high: 'from-red-400 to-red-500',
|
||||
};
|
||||
|
||||
const levelTextColors: Record<SimilarityLevel, string> = {
|
||||
low: 'text-emerald-600',
|
||||
medium: 'text-amber-600',
|
||||
high: 'text-red-600',
|
||||
};
|
||||
|
||||
const levelBgColors: Record<SimilarityLevel, string> = {
|
||||
low: 'bg-emerald-50 border-emerald-200',
|
||||
medium: 'bg-amber-50 border-amber-200',
|
||||
high: 'bg-red-50 border-red-200',
|
||||
};
|
||||
|
||||
const levelLabels: Record<SimilarityLevel, string> = {
|
||||
low: 'Low Similarity — Likely Original',
|
||||
medium: 'Medium Similarity — Review Recommended',
|
||||
high: 'High Similarity — Action Required',
|
||||
};
|
||||
|
||||
export const SimilarityMeter: React.FC<SimilarityMeterProps> = ({
|
||||
percentage,
|
||||
level,
|
||||
}) => (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-end justify-between">
|
||||
<span className="text-4xl font-bold tabular-nums text-slate-800">
|
||||
{percentage}
|
||||
<span className="text-xl text-slate-400 font-medium">%</span>
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs font-semibold uppercase tracking-wide ${levelTextColors[level]}`}
|
||||
>
|
||||
{level} risk
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Track */}
|
||||
<div className="relative h-3 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-y-0 left-0 rounded-full bg-gradient-to-r ${levelColors[level]} transition-all duration-700`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
{/* Threshold markers */}
|
||||
<div className="absolute inset-y-0 left-[30%] w-px bg-slate-300 opacity-60" />
|
||||
<div className="absolute inset-y-0 left-[60%] w-px bg-slate-300 opacity-60" />
|
||||
</div>
|
||||
|
||||
<div className="flex text-[10px] text-slate-400 justify-between font-medium">
|
||||
<span>0%</span>
|
||||
<span>30%</span>
|
||||
<span>60%</span>
|
||||
<span>100%</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex items-center gap-2 p-3 rounded-xl border text-sm ${levelBgColors[level]} ${levelTextColors[level]} font-medium`}
|
||||
>
|
||||
<span>{levelLabels[level]}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── 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<AvatarProps> = ({
|
||||
name,
|
||||
avatarUrl,
|
||||
size = 'md',
|
||||
}) => {
|
||||
const initials = name
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')
|
||||
.slice(0, 2)
|
||||
.toUpperCase();
|
||||
|
||||
if (avatarUrl) {
|
||||
return (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
className={`${sizeMap[size]} rounded-full object-cover ring-2 ring-white`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${sizeMap[size]} rounded-full bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center text-white font-bold ring-2 ring-white`}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── SkeletonLoader ────────────────────────────────────────────
|
||||
|
||||
export const SkeletonLoader: React.FC = () => (
|
||||
<div className="animate-pulse space-y-4">
|
||||
{/* Header skeleton */}
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-100">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-14 h-14 rounded-full bg-slate-200" />
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="h-5 bg-slate-200 rounded w-48" />
|
||||
<div className="h-4 bg-slate-100 rounded w-36" />
|
||||
</div>
|
||||
<div className="h-6 w-24 bg-slate-200 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Cards skeleton */}
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="bg-white rounded-2xl border border-slate-100">
|
||||
<div className="px-6 py-4 border-b border-slate-50">
|
||||
<div className="h-4 bg-slate-200 rounded w-32" />
|
||||
</div>
|
||||
<div className="px-6 py-4 space-y-3">
|
||||
{[1, 2, 3].map((j) => (
|
||||
<div key={j} className="h-4 bg-slate-100 rounded w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── ErrorState ────────────────────────────────────────────────
|
||||
|
||||
interface ErrorStateProps {
|
||||
message: string;
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
export const ErrorState: React.FC<ErrorStateProps> = ({ message, onRetry }) => (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center space-y-4">
|
||||
<div className="w-16 h-16 rounded-full bg-red-50 flex items-center justify-center">
|
||||
<svg
|
||||
className="w-8 h-8 text-red-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-700">Failed to load check</p>
|
||||
<p className="text-sm text-slate-500 mt-1">{message}</p>
|
||||
</div>
|
||||
{onRetry && (
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="px-5 py-2 bg-slate-800 text-white text-sm font-medium rounded-xl hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── FileTypeBadge ─────────────────────────────────────────────
|
||||
|
||||
interface FileTypeBadgeProps {
|
||||
extension: string;
|
||||
}
|
||||
|
||||
const extColors: Record<string, string> = {
|
||||
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<FileTypeBadgeProps> = ({ extension }) => (
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-bold tracking-wide ${extColors[extension] ?? 'bg-slate-100 text-slate-600'}`}
|
||||
>
|
||||
{extension}
|
||||
</span>
|
||||
);
|
||||
Reference in New Issue
Block a user