Files
plagiat/src/widgets/detail/index.tsx
nabijonovdavronbek619@gmail.com 80343c9ca8 landing page added
2026-04-01 01:22:31 +05:00

312 lines
9.8 KiB
TypeScript

// ─────────────────────────────────────────────────────────────
// 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-35">
{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={`w-full 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-linear-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-linear-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>
);