This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-06 15:43:51 +05:00
parent 89c5552c4e
commit 27b1510842
23 changed files with 1871 additions and 26 deletions

View File

@@ -0,0 +1,145 @@
'use client';
import React from 'react';
import {
LayoutDashboard,
FileSearch,
BrainCircuit,
CreditCard,
User,
Home,
X,
} from 'lucide-react';
import { Link } from '@/shared/config/i18n/navigation';
import type { CabinetSection } from '../lib/types';
// ─── Nav items ─────────────────────────────────────────────────────────────────
type NavItemDef =
| { id: CabinetSection; label: string; icon: React.ElementType; href?: never }
| { id: 'home'; label: string; icon: React.ElementType; href: string };
const NAV_ITEMS: NavItemDef[] = [
{ id: 'home', label: 'Bosh sahifa', icon: Home, href: '/' },
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'plagiat', label: 'Plagiat', icon: FileSearch },
{ id: 'si', label: 'SI detektor', icon: BrainCircuit },
{ id: 'payments', label: "To'lovlar tarixi", icon: CreditCard },
{ id: 'profile', label: 'Profil', icon: User },
];
// ─── Props ─────────────────────────────────────────────────────────────────────
interface SidebarProps {
active: CabinetSection;
onNavigate: (section: CabinetSection) => void;
isOpen: boolean;
onClose: () => void;
userName: string;
}
// ─── Component ─────────────────────────────────────────────────────────────────
export const Sidebar: React.FC<SidebarProps> = ({
active,
onNavigate,
isOpen,
onClose,
userName,
}) => (
<>
{/* Mobile backdrop */}
{isOpen && (
<div
className="fixed inset-0 z-30 bg-black/30 backdrop-blur-sm lg:hidden"
onClick={onClose}
/>
)}
<aside
className={`
fixed top-0 left-0 z-40 h-full w-60 bg-white border-r border-slate-100
flex flex-col shadow-xl
transition-transform duration-300 ease-out
lg:sticky lg:top-0 lg:h-screen lg:translate-x-0 lg:shadow-none lg:z-auto
${isOpen ? 'translate-x-0' : '-translate-x-full'}
`}
>
{/* Brand */}
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-lg bg-blue-500 flex items-center justify-center">
<span className="text-white text-xs font-bold">P</span>
</div>
<span className="font-semibold text-slate-800 text-sm">Plagat</span>
</div>
<button
onClick={onClose}
className="lg:hidden p-1.5 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors"
aria-label="Yopish"
>
<X size={15} />
</button>
</div>
{/* User pill */}
<div className="px-3 pt-4 pb-2">
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-slate-50 border border-slate-100">
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center shrink-0">
<span className="text-blue-600 text-xs font-semibold">
{userName.charAt(0).toUpperCase()}
</span>
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-slate-800 truncate">
{userName}
</p>
<p className="text-[11px] text-slate-400">Shaxsiy kabinet</p>
</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-2 space-y-0.5 overflow-y-auto">
{NAV_ITEMS.map((item) => {
const Icon = item.icon;
if (item.id === 'home') {
return (
<Link
key="home"
href={item.href}
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium text-slate-500 hover:text-slate-800 hover:bg-slate-50 transition-all duration-150"
>
<Icon size={17} />
<span>{item.label}</span>
</Link>
);
}
const isActive = item.id === active;
return (
<button
key={item.id}
onClick={() => onNavigate(item.id as CabinetSection)}
className={`
w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium
transition-all duration-150
${
isActive
? 'bg-blue-50 text-blue-600'
: 'text-slate-500 hover:text-slate-800 hover:bg-slate-50'
}
`}
>
<Icon size={17} />
<span>{item.label}</span>
{isActive && (
<span className="ml-auto w-1.5 h-1.5 rounded-full bg-blue-500 shrink-0" />
)}
</button>
);
})}
</nav>
</aside>
</>
);

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { FileSearch, BrainCircuit, ArrowRight } from 'lucide-react';
import type { CabinetSection } from '../../lib/types';
interface CtaCardsProps {
onNavigate: (section: CabinetSection) => void;
}
export const CtaCards: React.FC<CtaCardsProps> = ({ onNavigate }) => (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Plagiat */}
<button
onClick={() => onNavigate('plagiat')}
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-blue-500 to-blue-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
>
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
<FileSearch size={72} className="text-white" />
</div>
<FileSearch size={26} className="text-white mb-4" />
<h3 className="text-white font-semibold text-base mb-1">
Plagiat tekshiruvi
</h3>
<p className="text-blue-100 text-sm mb-4 leading-relaxed">
Hujjatingizni originallik uchun tekshiring
</p>
<span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors">
Yuborish <ArrowRight size={12} />
</span>
</button>
{/* SI */}
<button
onClick={() => onNavigate('si')}
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-violet-500 to-violet-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
>
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
<BrainCircuit size={72} className="text-white" />
</div>
<BrainCircuit size={26} className="text-white mb-4" />
<h3 className="text-white font-semibold text-base mb-1">SI detektor</h3>
<p className="text-violet-100 text-sm mb-4 leading-relaxed">
Matnni sun&apos;iy intellekt uchun tekshiring
</p>
<span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors">
Yuborish <ArrowRight size={12} />
</span>
</button>
</div>
);

View File

@@ -0,0 +1,128 @@
import React from 'react';
import {
MODULE_CATEGORIES,
MODULE_STATS,
TAG_STYLES,
type ModuleTag,
} from '../../lib/modules';
// ─── Module stats mini-cards ───────────────────────────────────────────────────
const ModuleStats: React.FC = () => (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
{ value: MODULE_STATS.total, label: 'Jami modullar' },
{ value: MODULE_STATS.freeInternet, label: 'Bepul internet manbalari' },
{ value: MODULE_STATS.aiModules, label: 'AI tahlil modullari' },
{ value: MODULE_STATS.categories, label: 'Kategoriya' },
].map(({ value, label }) => (
<div
key={label}
className="bg-slate-50 border border-slate-100 rounded-xl px-4 py-3"
>
<p className="text-2xl font-bold text-slate-900 tabular-nums leading-none">
{value}
</p>
<p className="text-xs text-slate-500 mt-1">{label}</p>
</div>
))}
</div>
);
// ─── Tag badge ─────────────────────────────────────────────────────────────────
const Tag: React.FC<{ tag: ModuleTag }> = ({ tag }) => (
<span
className={`inline-block text-[10px] font-semibold px-2 py-0.5 rounded-full ${TAG_STYLES[tag]}`}
>
{tag}
</span>
);
// ─── Category divider ──────────────────────────────────────────────────────────
const CategoryHeader: React.FC<{ label: string }> = ({ label }) => (
<div className="flex items-center gap-3 my-5">
<div className="flex-1 h-px bg-slate-100" />
<span className="text-[11px] font-semibold tracking-widest text-slate-400 uppercase whitespace-nowrap">
{label}
</span>
<div className="flex-1 h-px bg-slate-100" />
</div>
);
// ─── Single module card ────────────────────────────────────────────────────────
interface ModuleCardProps {
name: string;
desc: string;
tags: ModuleTag[];
index: number;
}
const ModuleCard: React.FC<ModuleCardProps> = ({ name, desc, tags, index }) => (
<div className="bg-white border border-slate-100 rounded-xl p-4 flex items-start gap-3 hover:border-slate-200 hover:shadow-sm transition-all duration-150">
{/* Index number */}
<span className="w-6 h-6 rounded-full bg-slate-100 flex items-center justify-center text-[10px] font-semibold text-slate-500 shrink-0 mt-0.5">
{index}
</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-slate-800 leading-snug mb-1 truncate">
{name}
</p>
<p className="text-xs text-slate-500 leading-relaxed mb-2.5">{desc}</p>
<div className="flex flex-wrap gap-1.5">
{tags.map((tag) => (
<Tag key={tag} tag={tag} />
))}
</div>
</div>
</div>
);
// ─── Modules section ───────────────────────────────────────────────────────────
export const ModulesSection: React.FC = () => {
// running counter across all categories
let counter = 0;
return (
<div>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-semibold text-slate-800">
Tekshiruv modullari
</h3>
<p className="text-xs text-slate-400 mt-0.5">
Plagiat aniqlashda foydalaniladigan barcha manbalar
</p>
</div>
<span className="text-xs text-slate-400 bg-slate-100 px-2.5 py-1 rounded-lg font-medium">
{MODULE_STATS.total} ta modul
</span>
</div>
<ModuleStats />
{MODULE_CATEGORIES.map((cat) => (
<div key={cat.label}>
<CategoryHeader label={cat.label} />
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3">
{cat.modules.map((mod) => {
counter += 1;
return (
<ModuleCard
key={mod.name}
name={mod.name}
desc={mod.desc}
tags={mod.tags}
index={counter}
/>
);
})}
</div>
</div>
))}
</div>
);
};

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { TrendingUp, Calendar, Tag, Wallet } from 'lucide-react';
import type { CabinetStats } from '../../lib/types';
// ─── Single stat card ──────────────────────────────────────────────────────────
interface StatCardProps {
icon: React.ElementType;
label: string;
value: string;
sub?: string;
iconColor: string;
iconBg: string;
}
const StatCard: React.FC<StatCardProps> = ({
icon: Icon,
label,
value,
sub,
iconColor,
iconBg,
}) => (
<div className="bg-white rounded-2xl border border-slate-100 p-5 shadow-sm hover:shadow-md transition-shadow duration-200">
<div
className={`w-10 h-10 rounded-xl flex items-center justify-center mb-4 ${iconBg}`}
>
<Icon size={18} className={iconColor} />
</div>
<p className="text-2xl font-bold text-slate-900 tabular-nums">{value}</p>
<p className="text-xs text-slate-500 mt-0.5">{label}</p>
{sub && <p className="text-[11px] text-slate-400 mt-1">{sub}</p>}
</div>
);
// ─── Grid ──────────────────────────────────────────────────────────────────────
interface StatsCardsProps {
stats: CabinetStats;
}
export const StatsCards: React.FC<StatsCardsProps> = ({ stats }) => {
const discountPct = Math.round(
(stats.discountUsed / stats.discountTotal) * 100,
);
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
icon={TrendingUp}
label="Jami tekshiruvlar"
value={String(stats.total)}
iconColor="text-blue-600"
iconBg="bg-blue-50"
/>
<StatCard
icon={Calendar}
label="Bu oy"
value={String(stats.thisMonth)}
sub={`${stats.discountUsed}/${stats.discountTotal} ta hujjat`}
iconColor="text-emerald-600"
iconBg="bg-emerald-50"
/>
<StatCard
icon={Tag}
label="Chegirma holati"
value={`${discountPct}%`}
sub={`${stats.discountUsed}/${stats.discountTotal} ta ishlatilgan`}
iconColor="text-amber-600"
iconBg="bg-amber-50"
/>
<StatCard
icon={Wallet}
label="Balans"
value={`${stats.balance.toLocaleString()} ${stats.currency}`}
iconColor="text-violet-600"
iconBg="bg-violet-50"
/>
</div>
);
};

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { CtaCards } from './CtaCards';
import { StatsCards } from './StatsCards';
import { ModulesSection } from './ModulesSection';
import type { CabinetSection, CabinetStats } from '../../lib/types';
interface DashboardProps {
stats: CabinetStats;
onNavigate: (section: CabinetSection) => void;
userName: string;
}
export const Dashboard: React.FC<DashboardProps> = ({
stats,
onNavigate,
userName,
}) => (
<div className="space-y-6">
<div>
<h2 className="text-xl font-bold text-slate-900">
Xush kelibsiz, {userName} 👋
</h2>
<p className="text-sm text-slate-500 mt-0.5">
Shaxsiy kabinetingizga xush kelibsiz
</p>
</div>
<StatsCards stats={stats} />
<div>
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-widest mb-3">
Tezkor harakatlar
</h3>
<CtaCards onNavigate={onNavigate} />
</div>
<div className="border-t border-slate-100 pt-6">
<ModulesSection />
</div>
</div>
);

View File

@@ -0,0 +1,114 @@
'use client';
import React from 'react';
import dynamic from 'next/dynamic';
import { AnimatePresence, motion } from 'framer-motion';
import { Sidebar } from './Sidebar';
import { Dashboard } from './dashboard';
import { useCabinet } from '../lib/hooks/useCabinet';
import {
MOCK_USER,
MOCK_STATS,
MOCK_PLAGIAT,
MOCK_SI,
MOCK_PAYMENTS,
} from '../lib/mock';
import type { CabinetSection } from '../lib/types';
// ─── Lazy sections (separate JS chunks) ───────────────────────────────────────
const Skeleton = () => (
<div className="space-y-3">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-12 bg-slate-100 rounded-xl animate-pulse" />
))}
</div>
);
const PlagiatTable = dynamic(
() =>
import('./tables/PlagiatTable').then((m) => ({ default: m.PlagiatTable })),
{ loading: Skeleton },
);
const SiTable = dynamic(
() => import('./tables/SiTable').then((m) => ({ default: m.SiTable })),
{ loading: Skeleton },
);
const PaymentsTable = dynamic(
() =>
import('./tables/PaymentsTable').then((m) => ({
default: m.PaymentsTable,
})),
{ loading: Skeleton },
);
const ProfileSection = dynamic(
() => import('./profile').then((m) => ({ default: m.ProfileSection })),
{ loading: Skeleton },
);
// ─── Section switcher ──────────────────────────────────────────────────────────
function SectionContent({
section,
onNavigate,
}: {
section: CabinetSection;
onNavigate: (s: CabinetSection) => void;
}) {
switch (section) {
case 'dashboard':
return (
<Dashboard
stats={MOCK_STATS}
onNavigate={onNavigate}
userName={MOCK_USER.name}
/>
);
case 'plagiat':
return <PlagiatTable data={MOCK_PLAGIAT} />;
case 'si':
return <SiTable data={MOCK_SI} />;
case 'payments':
return <PaymentsTable data={MOCK_PAYMENTS} />;
case 'profile':
return <ProfileSection user={MOCK_USER} stats={MOCK_STATS} />;
}
}
// ─── Animation ────────────────────────────────────────────────────────────────
const FADE = {
initial: { opacity: 0, y: 10 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -6 },
transition: { duration: 0.18, ease: 'easeOut' },
} as const;
// ─── CabinetLayout ────────────────────────────────────────────────────────────
export const CabinetLayout: React.FC = () => {
const { activeSection, navigate, isSidebarOpen, toggleSidebar } =
useCabinet();
const fullName = `${MOCK_USER.name} ${MOCK_USER.surname}`;
return (
<div className="flex border-t min-h-screen">
<Sidebar
active={activeSection}
onNavigate={navigate}
isOpen={isSidebarOpen}
onClose={toggleSidebar}
userName={fullName}
/>
<div className="flex-1 flex flex-col min-w-0">
<main className="flex-1 p-4 md:p-6 lg:p-8 max-w-6xl mx-auto w-full">
<AnimatePresence mode="wait">
<motion.div key={activeSection} {...FADE}>
<SectionContent section={activeSection} onNavigate={navigate} />
</motion.div>
</AnimatePresence>
</main>
</div>
</div>
);
};

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { Tag } from 'lucide-react';
import type { CabinetStats } from '../../lib/types';
interface DiscountProgressProps {
stats: CabinetStats;
}
export const DiscountProgress: React.FC<DiscountProgressProps> = ({
stats,
}) => {
const pct = Math.round((stats.discountUsed / stats.discountTotal) * 100);
const remaining = stats.discountTotal - stats.discountUsed;
return (
<div className="bg-gradient-to-br from-amber-50 to-orange-50 border border-amber-100 rounded-2xl p-5">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-xl bg-amber-100 flex items-center justify-center">
<Tag size={15} className="text-amber-600" />
</div>
<div>
<h3 className="text-sm font-semibold text-amber-800">
Bu oyda chegirma
</h3>
<p className="text-xs text-amber-600 mt-0.5">
{remaining > 0
? `${remaining} ta hujjatdan keyin chegirma tugaydi`
: 'Bu oyda barcha chegirmalar ishlatildi'}
</p>
</div>
</div>
<span className="text-2xl font-bold text-amber-700 tabular-nums">
{stats.discountUsed}/{stats.discountTotal}
</span>
</div>
{/* Bar */}
<div className="h-2 bg-amber-100 rounded-full overflow-hidden mb-2.5">
<div
className="h-full bg-amber-400 rounded-full transition-[width] duration-700 ease-out"
style={{ width: `${pct}%` }}
/>
</div>
<div className="flex justify-between text-[11px] text-amber-600">
<span>{stats.discountUsed} ta ishlatildi</span>
<span>{pct}%</span>
</div>
</div>
);
};

View File

@@ -0,0 +1,170 @@
'use client';
import React from 'react';
import { User, Mail, Phone, Lock, Save, CheckCircle } from 'lucide-react';
import { useProfile } from '../../lib/hooks/useProfile';
import type { UserProfile } from '../../lib/types';
// ─── Input field ───────────────────────────────────────────────────────────────
interface InputFieldProps {
label: string;
value: string;
onChange: (val: string) => void;
type?: string;
icon: React.ElementType;
placeholder?: string;
}
const InputField: React.FC<InputFieldProps> = ({
label,
value,
onChange,
type = 'text',
icon: Icon,
placeholder,
}) => (
<div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5">
{label}
</label>
<div className="relative">
<Icon
size={14}
className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"
/>
<input
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="
w-full pl-9 pr-4 py-2.5 text-sm rounded-xl
border border-slate-200 bg-white
text-slate-800 placeholder:text-slate-300
focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400
transition-all duration-150
"
/>
</div>
</div>
);
// ─── Form ──────────────────────────────────────────────────────────────────────
interface ProfileFormProps {
initial: UserProfile;
}
export const ProfileForm: React.FC<ProfileFormProps> = ({ initial }) => {
const { form, isSaving, saved, handleChange, handleSave } =
useProfile(initial);
return (
<div className="space-y-5">
{/* Personal info */}
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
<h3 className="text-sm font-semibold text-slate-800 mb-4">
Shaxsiy ma&apos;lumotlar
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<InputField
label="Ism"
value={form.name}
onChange={(v) => handleChange('name', v)}
icon={User}
placeholder="Ali"
/>
<InputField
label="Familiya"
value={form.surname}
onChange={(v) => handleChange('surname', v)}
icon={User}
placeholder="Karimov"
/>
<InputField
label="Email"
value={form.email}
onChange={(v) => handleChange('email', v)}
type="email"
icon={Mail}
placeholder="ali@example.com"
/>
<InputField
label="Telefon"
value={form.phone}
onChange={(v) => handleChange('phone', v)}
icon={Phone}
placeholder="+998 90 123 45 67"
/>
</div>
</div>
{/* Password */}
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
<h3 className="text-sm font-semibold text-slate-800 mb-4">
Parol o&apos;zgartirish
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<InputField
label="Joriy parol"
value={form.currentPassword}
onChange={(v) => handleChange('currentPassword', v)}
type="password"
icon={Lock}
placeholder="••••••••"
/>
<InputField
label="Yangi parol"
value={form.newPassword}
onChange={(v) => handleChange('newPassword', v)}
type="password"
icon={Lock}
placeholder="••••••••"
/>
<InputField
label="Tasdiqlash"
value={form.confirmPassword}
onChange={(v) => handleChange('confirmPassword', v)}
type="password"
icon={Lock}
placeholder="••••••••"
/>
</div>
</div>
{/* Save */}
<div className="flex justify-end">
<button
onClick={handleSave}
disabled={isSaving}
className={`
flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-semibold text-white
transition-all duration-200
${
isSaving
? 'bg-blue-300 cursor-not-allowed'
: saved
? 'bg-emerald-500 hover:bg-emerald-600'
: 'bg-blue-500 hover:bg-blue-600 active:scale-95 shadow-md hover:shadow-lg'
}
`}
>
{saved ? (
<>
<CheckCircle size={15} /> Saqlandi
</>
) : isSaving ? (
<>
<span className="w-4 h-4 border-2 border-white/40 border-t-white rounded-full animate-spin" />
Saqlanmoqda
</>
) : (
<>
<Save size={15} /> Saqlash
</>
)}
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { DiscountProgress } from './DiscountProgress';
import { ProfileForm } from './ProfileForm';
import type { CabinetStats, UserProfile } from '../../lib/types';
interface ProfileSectionProps {
user: UserProfile;
stats: CabinetStats;
}
export const ProfileSection: React.FC<ProfileSectionProps> = ({
user,
stats,
}) => (
<div className="space-y-6">
<div>
<h2 className="text-xl font-bold text-slate-900">Profil</h2>
<p className="text-sm text-slate-500 mt-0.5">
Ma&apos;lumotlaringizni boshqaring
</p>
</div>
<DiscountProgress stats={stats} />
<ProfileForm initial={user} />
</div>
);

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { CheckCircle, Clock, XCircle } from 'lucide-react';
import type { Payment } from '../../lib/types';
// ─── Helpers ───────────────────────────────────────────────────────────────────
const STATUS_MAP = {
paid: {
label: "To'landi",
icon: CheckCircle,
cls: 'text-emerald-600 bg-emerald-50',
},
pending: {
label: 'Kutilmoqda',
icon: Clock,
cls: 'text-amber-600 bg-amber-50',
},
failed: { label: 'Xato', icon: XCircle, cls: 'text-red-600 bg-red-50' },
} as const;
// ─── Component ─────────────────────────────────────────────────────────────────
interface PaymentsTableProps {
data: Payment[];
}
export const PaymentsTable: React.FC<PaymentsTableProps> = ({ data }) => (
<div className="space-y-4">
<div>
<h2 className="text-xl font-bold text-slate-900">
To&apos;lovlar tarixi
</h2>
<p className="text-sm text-slate-500 mt-0.5">
{data.length} ta to&apos;lov
</p>
</div>
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-100">
{['#', 'Xizmat', 'Summa', 'Chegirma', 'Sana', 'Holat'].map(
(h) => (
<th
key={h}
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap"
>
{h}
</th>
),
)}
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{data.map((row) => {
const s = STATUS_MAP[row.status];
const Icon = s.icon;
return (
<tr
key={row.id}
className="hover:bg-slate-50/60 transition-colors"
>
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
{String(row.id).padStart(2, '0')}
</td>
<td className="px-5 py-3.5 text-slate-800 font-medium whitespace-nowrap">
{row.service}
</td>
<td className="px-5 py-3.5 text-slate-800 font-semibold tabular-nums whitespace-nowrap">
{row.amount.toLocaleString()} UZS
</td>
<td className="px-5 py-3.5 tabular-nums whitespace-nowrap">
{row.discount > 0 ? (
<span className="text-emerald-600 font-medium">
-{row.discount.toLocaleString()} UZS
</span>
) : (
<span className="text-slate-300"></span>
)}
</td>
<td className="px-5 py-3.5 text-slate-500 whitespace-nowrap">
{row.date}
</td>
<td className="px-5 py-3.5">
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${s.cls}`}
>
<Icon size={12} />
{s.label}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,128 @@
import React from 'react';
import { Download, Clock, CheckCircle, XCircle } from 'lucide-react';
import type { PlagiatCheck } from '../../lib/types';
// ─── Helpers ───────────────────────────────────────────────────────────────────
const STATUS_MAP = {
completed: {
label: 'Yakunlandi',
icon: CheckCircle,
cls: 'text-emerald-600 bg-emerald-50',
},
pending: {
label: 'Kutilmoqda',
icon: Clock,
cls: 'text-amber-600 bg-amber-50',
},
failed: { label: 'Xato', icon: XCircle, cls: 'text-red-600 bg-red-50' },
} as const;
const PercentBadge: React.FC<{ value: number }> = ({ value }) => {
const cls =
value < 15
? 'text-emerald-700 bg-emerald-50'
: value < 30
? 'text-amber-700 bg-amber-50'
: 'text-red-700 bg-red-50';
return (
<span
className={`inline-block px-2 py-0.5 rounded-md text-xs font-semibold ${cls}`}
>
{value}%
</span>
);
};
// ─── Component ─────────────────────────────────────────────────────────────────
interface PlagiatTableProps {
data: PlagiatCheck[];
}
export const PlagiatTable: React.FC<PlagiatTableProps> = ({ data }) => (
<div className="space-y-4">
<div>
<h2 className="text-xl font-bold text-slate-900">Plagiat tekshiruvlar</h2>
<p className="text-sm text-slate-500 mt-0.5">
{data.length} ta tekshiruv
</p>
</div>
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-100">
{['#', 'Fayl', 'Turi', '%', 'Sana', 'Holat', 'Yuklab olish'].map(
(h) => (
<th
key={h}
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap last:text-center"
>
{h}
</th>
),
)}
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{data.map((row) => {
const s = STATUS_MAP[row.status];
const Icon = s.icon;
return (
<tr
key={row.id}
className="hover:bg-slate-50/60 transition-colors"
>
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
{String(row.id).padStart(2, '0')}
</td>
<td className="px-5 py-3.5">
<span className="text-slate-800 font-medium max-w-[150px] truncate block">
{row.file}
</span>
</td>
<td className="px-5 py-3.5 text-slate-500 whitespace-nowrap">
{row.type}
</td>
<td className="px-5 py-3.5">
{row.status === 'pending' ? (
<span className="text-slate-300 text-xs"></span>
) : (
<PercentBadge value={row.percent} />
)}
</td>
<td className="px-5 py-3.5 text-slate-500 whitespace-nowrap">
{row.date}
</td>
<td className="px-5 py-3.5">
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${s.cls}`}
>
<Icon size={12} />
{s.label}
</span>
</td>
<td className="px-5 py-3.5 text-center">
{row.downloadUrl ? (
<a
href={row.downloadUrl}
className="inline-flex items-center justify-center w-8 h-8 rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 transition-colors"
aria-label="Yuklab olish"
>
<Download size={15} />
</a>
) : (
<span className="text-slate-200 text-xs"></span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,128 @@
import React from 'react';
import { FileText, Clock, CheckCircle, XCircle } from 'lucide-react';
import type { SiCheck } from '../../lib/types';
// ─── Helpers ───────────────────────────────────────────────────────────────────
const STATUS_MAP = {
completed: {
label: 'Yakunlandi',
icon: CheckCircle,
cls: 'text-emerald-600 bg-emerald-50',
},
pending: {
label: 'Kutilmoqda',
icon: Clock,
cls: 'text-amber-600 bg-amber-50',
},
failed: { label: 'Xato', icon: XCircle, cls: 'text-red-600 bg-red-50' },
} as const;
const SiPercentBadge: React.FC<{ value: number }> = ({ value }) => {
const cls =
value < 20
? 'text-emerald-700 bg-emerald-50'
: value < 40
? 'text-amber-700 bg-amber-50'
: 'text-red-700 bg-red-50';
return (
<span
className={`inline-block px-2 py-0.5 rounded-md text-xs font-semibold ${cls}`}
>
{value}%
</span>
);
};
// ─── Component ─────────────────────────────────────────────────────────────────
interface SiTableProps {
data: SiCheck[];
}
export const SiTable: React.FC<SiTableProps> = ({ data }) => (
<div className="space-y-4">
<div>
<h2 className="text-xl font-bold text-slate-900">SI detektor</h2>
<p className="text-sm text-slate-500 mt-0.5">
{data.length} ta tekshiruv
</p>
</div>
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-100">
{['#', 'Fayl', "So'z", 'SI%', 'Sana', 'Holat', 'Hisobot'].map(
(h) => (
<th
key={h}
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap last:text-center"
>
{h}
</th>
),
)}
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{data.map((row) => {
const s = STATUS_MAP[row.status];
const Icon = s.icon;
return (
<tr
key={row.id}
className="hover:bg-slate-50/60 transition-colors"
>
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
{String(row.id).padStart(2, '0')}
</td>
<td className="px-5 py-3.5">
<span className="text-slate-800 font-medium max-w-[150px] truncate block">
{row.file}
</span>
</td>
<td className="px-5 py-3.5 text-slate-500 tabular-nums">
{row.words.toLocaleString()}
</td>
<td className="px-5 py-3.5">
{row.status === 'pending' ? (
<span className="text-slate-300 text-xs"></span>
) : (
<SiPercentBadge value={row.siPercent} />
)}
</td>
<td className="px-5 py-3.5 text-slate-500 whitespace-nowrap">
{row.date}
</td>
<td className="px-5 py-3.5">
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${s.cls}`}
>
<Icon size={12} />
{s.label}
</span>
</td>
<td className="px-5 py-3.5 text-center">
{row.reportUrl ? (
<a
href={row.reportUrl}
className="inline-flex items-center justify-center w-8 h-8 rounded-lg text-slate-400 hover:text-violet-600 hover:bg-violet-50 transition-colors"
aria-label="Hisobot"
>
<FileText size={15} />
</a>
) : (
<span className="text-slate-200 text-xs"></span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);