last push
This commit is contained in:
@@ -1,18 +1,9 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { Menu } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type { CabinetSection } from '../lib/types';
|
||||
|
||||
// ─── Labels ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const SECTION_LABELS: Record<CabinetSection, string> = {
|
||||
dashboard: 'Dashboard',
|
||||
plagiat: 'Plagiat tekshiruvlar',
|
||||
si: 'SI detektor',
|
||||
payments: "To'lovlar tarixi",
|
||||
profile: 'Profil',
|
||||
};
|
||||
|
||||
// ─── Props ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface CabinetNavProps {
|
||||
@@ -25,19 +16,31 @@ interface CabinetNavProps {
|
||||
export const CabinetNav: React.FC<CabinetNavProps> = ({
|
||||
activeSection,
|
||||
onMenuClick,
|
||||
}) => (
|
||||
<header className="h-14 px-4 md:px-6 flex lg:hidden items-center justify-between border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-20">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onMenuClick}
|
||||
className="lg:hidden p-2 rounded-lg text-slate-500 hover:bg-slate-100 transition-colors"
|
||||
aria-label="Menu"
|
||||
>
|
||||
<Menu size={18} />
|
||||
</button>
|
||||
<h1 className="text-sm font-semibold text-slate-800">
|
||||
{SECTION_LABELS[activeSection]}
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}) => {
|
||||
const t = useTranslations('Cabinet');
|
||||
|
||||
const SECTION_LABELS: Record<CabinetSection, string> = {
|
||||
dashboard: t('dashboard'),
|
||||
plagiat: t('plagiatChecks'),
|
||||
si: t('siNav'),
|
||||
payments: t('payments'),
|
||||
profile: t('profile'),
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="h-14 px-4 md:px-6 flex lg:hidden items-center justify-between border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-20">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onMenuClick}
|
||||
className="lg:hidden p-2 rounded-lg text-slate-500 hover:bg-slate-100 transition-colors"
|
||||
aria-label="Menu"
|
||||
>
|
||||
<Menu size={18} />
|
||||
</button>
|
||||
<h1 className="text-sm font-semibold text-slate-800">
|
||||
{SECTION_LABELS[activeSection]}
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,23 +10,9 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Link } from '@/shared/config/i18n/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
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 {
|
||||
@@ -45,95 +31,118 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
userName,
|
||||
}) => (
|
||||
<>
|
||||
{/* Mobile backdrop */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-30 bg-black/30 backdrop-blur-sm lg:hidden"
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
}) => {
|
||||
const t = useTranslations('Cabinet');
|
||||
|
||||
<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="lg:hidden flex items-center justify-end px-5 py-4 border-b border-slate-100">
|
||||
<button
|
||||
const NAV_ITEMS = [
|
||||
{ id: 'home' as const, label: t('home'), icon: Home, href: '/' },
|
||||
{
|
||||
id: 'dashboard' as CabinetSection,
|
||||
label: t('dashboard'),
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{ id: 'plagiat' as CabinetSection, label: t('plagiat'), icon: FileSearch },
|
||||
{ id: 'si' as CabinetSection, label: t('siNav'), icon: BrainCircuit },
|
||||
{
|
||||
id: 'payments' as CabinetSection,
|
||||
label: t('payments'),
|
||||
icon: CreditCard,
|
||||
},
|
||||
{ id: 'profile' as CabinetSection, label: t('profile'), icon: User },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile backdrop */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-30 bg-black/30 backdrop-blur-sm lg:hidden"
|
||||
onClick={onClose}
|
||||
className=" 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>
|
||||
<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="lg:hidden flex items-center justify-end px-5 py-4 border-b border-slate-100">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className=" p-1.5 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors"
|
||||
aria-label={t('close')}
|
||||
>
|
||||
<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">
|
||||
{t('personalCabinet')}
|
||||
</p>
|
||||
</div>
|
||||
</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;
|
||||
{/* 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') {
|
||||
if (item.id === 'home') {
|
||||
return (
|
||||
<Link
|
||||
key="home"
|
||||
href={item.href as string}
|
||||
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 (
|
||||
<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"
|
||||
<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>
|
||||
</Link>
|
||||
{isActive && (
|
||||
<span className="ml-auto w-1.5 h-1.5 rounded-full bg-blue-500 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { FileSearch, ArrowRight } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { SiCTACard } from '@/features/modals/siModal/page';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export const CtaCards = () => (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Plagiat */}
|
||||
<Link
|
||||
href={'/plagat'}
|
||||
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>
|
||||
</Link>
|
||||
export const CtaCards = () => {
|
||||
const t = useTranslations('Cabinet');
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Plagiat */}
|
||||
<Link
|
||||
href={'/plagat'}
|
||||
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">
|
||||
{t('plagiatCheck')}
|
||||
</h3>
|
||||
<p className="text-blue-100 text-sm mb-4 leading-relaxed">
|
||||
{t('checkDesc')}
|
||||
</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">
|
||||
{t('submit')} <ArrowRight size={12} />
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* SI */}
|
||||
<SiCTACard />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
{/* SI */}
|
||||
<SiCTACard />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -96,7 +96,7 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
||||
const pay = useMutation({
|
||||
mutationKey: ['si-payment', item.id],
|
||||
mutationFn: () =>
|
||||
apiRequest<{ payment_link: string }>('POST', links.si_payment(item.id)),
|
||||
apiRequest<{ payment_link: string }>('POST', links.demo_pay(item.id)),
|
||||
onSuccess: (res) => {
|
||||
window.open(res.data.payment_link, '_self');
|
||||
},
|
||||
|
||||
@@ -3,16 +3,35 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { apiRequest } from '@/shared/request/apiRequest';
|
||||
import { links } from '@/shared/request/links';
|
||||
import { Download, CloudDownload } from 'lucide-react';
|
||||
import { Download } from 'lucide-react';
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type SiResult = {
|
||||
original: number;
|
||||
ai_possible: number;
|
||||
type SiResultRes = {
|
||||
ai: number;
|
||||
hash: string;
|
||||
text: string;
|
||||
citation: number;
|
||||
plagiarism: number;
|
||||
originality: number;
|
||||
};
|
||||
|
||||
type SiResultData = {
|
||||
ok: boolean;
|
||||
res: SiResultRes;
|
||||
error: string;
|
||||
success: string;
|
||||
text_res: string;
|
||||
analyze_text?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type SiResult = {
|
||||
id: number;
|
||||
document: number;
|
||||
result: SiResultData;
|
||||
};
|
||||
|
||||
type SiDetail = {
|
||||
@@ -134,6 +153,7 @@ function LoadingSkeleton() {
|
||||
// ── Main Page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function SiDetailPage({ id }: { id: number }) {
|
||||
const t = useTranslations('SiDetail');
|
||||
const { locale } = useParams() as { locale: string };
|
||||
useEffect(() => {
|
||||
console.log(locale);
|
||||
@@ -172,9 +192,10 @@ export default function SiDetailPage({ id }: { id: number }) {
|
||||
}
|
||||
|
||||
// Derive SI percentages
|
||||
const original = doc.result?.original ?? 100 - (doc.si_percantage ?? 0);
|
||||
const aiPossible = doc.result?.ai_possible ?? 0;
|
||||
const ai = doc.result?.ai ?? doc.si_percantage ?? 0;
|
||||
const res = doc.result?.result?.res;
|
||||
const original = res?.originality ?? 100 - (doc.si_percantage ?? 0);
|
||||
const aiPossible = res?.plagiarism ?? 0;
|
||||
const ai = res?.ai ?? doc.si_percantage ?? 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 font-sans">
|
||||
@@ -200,7 +221,7 @@ export default function SiDetailPage({ id }: { id: number }) {
|
||||
</svg>
|
||||
</button>
|
||||
<h1 className="text-base font-bold text-slate-800 truncate">
|
||||
{doc.title || 'SI tekshiruv'}
|
||||
{doc.title || t('siCheck')}
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
@@ -211,7 +232,7 @@ export default function SiDetailPage({ id }: { id: number }) {
|
||||
{/* Section header */}
|
||||
<div className="bg-slate-50 border-b border-slate-100 px-6 py-4 text-center">
|
||||
<h2 className="text-sm font-bold uppercase tracking-widest text-blue-600">
|
||||
Asosiy ma'lumotlar
|
||||
{t('basicInfo')}
|
||||
</h2>
|
||||
<div className="w-10 h-0.5 bg-blue-600 mx-auto mt-1.5" />
|
||||
</div>
|
||||
@@ -219,48 +240,42 @@ export default function SiDetailPage({ id }: { id: number }) {
|
||||
<div className="px-6">
|
||||
{/* Sub-header */}
|
||||
<p className="text-[11px] font-bold uppercase tracking-widest text-slate-400 py-4 border-b border-slate-100">
|
||||
Hujjat haqida ma'lumotlar
|
||||
{t('documentInfo')}
|
||||
</p>
|
||||
|
||||
<InfoRow
|
||||
label="Hujjat nomi"
|
||||
label={t('documentName')}
|
||||
value={doc.title || fileName(doc.file)}
|
||||
/>
|
||||
{doc.user && (
|
||||
<InfoRow
|
||||
label="Tekshiruvchi"
|
||||
label={t('checker')}
|
||||
value={`${doc.user.name} ${doc.user.surname}`}
|
||||
/>
|
||||
)}
|
||||
<InfoRow
|
||||
label="Hujjat yuklangan vaqti"
|
||||
label={t('uploadedAt')}
|
||||
value={formatDate(doc.created_at)}
|
||||
/>
|
||||
<InfoRow label={t('originalFileName')} value={fileName(doc.file)} />
|
||||
<InfoRow
|
||||
label="Hujjat faylining original nomi"
|
||||
value={fileName(doc.file)}
|
||||
/>
|
||||
<InfoRow
|
||||
label="So'zlar soni"
|
||||
label={t('wordCount')}
|
||||
value={
|
||||
doc.total_words > 0
|
||||
? doc.total_words.toLocaleString('uz-UZ')
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Hujjat fayli kenggaytmasi"
|
||||
value={fileExtension(doc.file)}
|
||||
/>
|
||||
<InfoRow label={t('fileExt')} value={fileExtension(doc.file)} />
|
||||
{doc.file_size !== undefined && (
|
||||
<InfoRow
|
||||
label="Hujjat fayli o'lchami"
|
||||
label={t('fileSize')}
|
||||
value={fileSizeMb(doc.file_size)}
|
||||
/>
|
||||
)}
|
||||
{doc.total_price !== undefined && (
|
||||
<InfoRow
|
||||
label="Yechilgan summa"
|
||||
label={t('amountCharged')}
|
||||
value={`${Number(doc.total_price).toLocaleString('uz-UZ')} so'm`}
|
||||
/>
|
||||
)}
|
||||
@@ -275,7 +290,7 @@ export default function SiDetailPage({ id }: { id: number }) {
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl border border-cyan-500 text-cyan-600 text-sm font-semibold hover:bg-cyan-50 transition-colors"
|
||||
>
|
||||
<Download size={15} />
|
||||
Original hujjatni yuklab olish
|
||||
{t('downloadOriginal')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
@@ -308,21 +323,15 @@ export default function SiDetailPage({ id }: { id: number }) {
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-slate-800 leading-snug">
|
||||
Hujjatning SI detektori natijalari
|
||||
{t('siResultsTitle')}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 mt-1.5 leading-relaxed max-w-lg">
|
||||
Ushbu oynada foydalanuvchi tomonidan yuklangan matn
|
||||
sun'iy intellekt (SI) yordamida yozilgan bo'lish
|
||||
ehtimoli bo'yicha tahlil natijalari aks etirilgan.
|
||||
Detektor matnning stilistik, grammatik va semantik
|
||||
xususiyatlarini baholab, uning qanchalik darajada sun'iy
|
||||
intellekt tomonidan generatsiya qilingan bo'lishi
|
||||
mumkinligini foizlik ko'rinishida ko'rsatadi.
|
||||
{t('siResultsDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{doc.file && (
|
||||
{/* {doc.file && (
|
||||
<a
|
||||
href={doc.file}
|
||||
target="_blank"
|
||||
@@ -330,24 +339,24 @@ export default function SiDetailPage({ id }: { id: number }) {
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-slate-800 text-white text-sm font-semibold hover:bg-slate-700 transition-colors shrink-0"
|
||||
>
|
||||
<CloudDownload size={16} />
|
||||
Yuklab olish
|
||||
{t('download')}
|
||||
</a>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
{/* Bars */}
|
||||
<div className="px-6 py-6 space-y-5">
|
||||
<SiBar
|
||||
label="Original matn"
|
||||
label={t('originalText')}
|
||||
value={original}
|
||||
color="bg-emerald-400"
|
||||
/>
|
||||
<SiBar
|
||||
label="Ehtimoliy Sun'iy Intellekt"
|
||||
label={t('possibleAi')}
|
||||
value={aiPossible}
|
||||
color="bg-amber-400"
|
||||
/>
|
||||
<SiBar label="Sun'iy Intellekt" value={ai} color="bg-red-500" />
|
||||
<SiBar label={t('aiContent')} value={ai} color="bg-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -10,7 +10,7 @@ import PaymentStatus from './paidStatus';
|
||||
import Sertifikat from '@/features/modals/sertificateModal/sertifikat';
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const baseUrl = 'https://dev-api.anti-plagiat.uz/api/v1';
|
||||
interface AnalyzeText {
|
||||
[key: string]: number | string;
|
||||
}
|
||||
@@ -397,10 +397,10 @@ export default function DocumentDetailPage({ id }: { id: number }) {
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<PaymentStatus status={doc.state} />
|
||||
{doc.certificate && <Sertifikat document_id={Number(id)} />}
|
||||
<Sertifikat document_id={Number(id)} />
|
||||
{doc.file && (
|
||||
<a
|
||||
href={doc.file}
|
||||
href={`${baseUrl}${doc.file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-900 text-white text-xs font-semibold hover:bg-slate-700 transition-colors"
|
||||
@@ -486,21 +486,26 @@ export default function DocumentDetailPage({ id }: { id: number }) {
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
|
||||
<div className="flex flex-wrap justify-around gap-8">
|
||||
<ScoreRing
|
||||
value={res.originality}
|
||||
label="Originality"
|
||||
value={res.ai}
|
||||
label={t('scoreOriginality')}
|
||||
color="#10b981"
|
||||
/>
|
||||
|
||||
<ScoreRing
|
||||
value={res.plagiarism}
|
||||
label="Plagiarism"
|
||||
label={t('scorePlagiarism')}
|
||||
color="#f59e0b"
|
||||
/>
|
||||
<ScoreRing
|
||||
value={res.citation}
|
||||
label="Citation"
|
||||
label={t('scoreCitation')}
|
||||
color="#6366f1"
|
||||
/>
|
||||
<ScoreRing value={res.ai} label="AI Content" color="#ef4444" />
|
||||
<ScoreRing
|
||||
value={res.originality}
|
||||
label={t('scoreAiContent')}
|
||||
color="#ef4444"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { blue } from '../../lib/constant';
|
||||
import { PlagiatData } from '../../lib/types';
|
||||
import BarRow from './BarRow';
|
||||
@@ -14,27 +16,28 @@ export default function GaugeWithBars({
|
||||
originalityPercent,
|
||||
citationPercent,
|
||||
}: Props) {
|
||||
const t = useTranslations('PlagiatResult');
|
||||
return (
|
||||
<div className="flex items-center gap-5 mb-5">
|
||||
<CircleGauge value={plagiarismPercent || 0} />
|
||||
<div className="flex-1">
|
||||
<BarRow
|
||||
label="Plagiat"
|
||||
label={t('plagiat')}
|
||||
value={plagiarismPercent || 0}
|
||||
color={blue[900]}
|
||||
/>
|
||||
<BarRow
|
||||
label="AI generatsiya"
|
||||
label={t('aiGeneration')}
|
||||
value={aiPercent || 0}
|
||||
color={blue[600]}
|
||||
/>
|
||||
<BarRow
|
||||
label="Original"
|
||||
label={t('original')}
|
||||
value={originalityPercent || 0}
|
||||
color={blue[400]}
|
||||
/>
|
||||
<BarRow
|
||||
label="Iqtibos"
|
||||
label={t('citation')}
|
||||
value={citationPercent || 0}
|
||||
color={blue[200]}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { blue } from '../../lib/constant';
|
||||
import { PlagiatData } from '../../lib/types';
|
||||
|
||||
@@ -21,6 +23,7 @@ export default function Header({
|
||||
location,
|
||||
checkedAt,
|
||||
}: Props) {
|
||||
const t = useTranslations('PlagiatResult');
|
||||
return (
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div
|
||||
@@ -41,7 +44,7 @@ export default function Header({
|
||||
className="text-xs px-2.5 py-0.5 rounded-md font-medium"
|
||||
style={{ background: blue[100], color: blue[800] }}
|
||||
>
|
||||
{plagiarismPercent}% plagiat
|
||||
{plagiarismPercent}% {t('plagiat').toLowerCase()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[13px] mt-0.5 truncate" style={{ color: blue[400] }}>
|
||||
@@ -50,7 +53,7 @@ export default function Header({
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<p className="text-[11px]" style={{ color: blue[400] }}>
|
||||
Tekshirilgan
|
||||
{t('checked')}
|
||||
</p>
|
||||
<p className="text-[12px] mt-0.5" style={{ color: blue[600] }}>
|
||||
{checkedAt}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { PlagiatData } from '../../lib/types';
|
||||
import MetricCard from './Metriccard';
|
||||
|
||||
@@ -12,12 +14,16 @@ export default function TopMetrics({
|
||||
originalityPercent,
|
||||
citationPercent,
|
||||
}: Props) {
|
||||
const t = useTranslations('PlagiatResult');
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2.5 mb-5">
|
||||
<MetricCard label="Plagiat darajasi" value={`${plagiarismPercent}%`} />
|
||||
<MetricCard label="AI yozgan" value={`${aiPercent}%`} />
|
||||
<MetricCard label="Originallik" value={`${originalityPercent}%`} />
|
||||
<MetricCard label="Iqtibos" value={`${citationPercent}%`} />
|
||||
<MetricCard
|
||||
label={t('plagiarismLevel')}
|
||||
value={`${plagiarismPercent}%`}
|
||||
/>
|
||||
<MetricCard label={t('aiWritten')} value={`${aiPercent}%`} />
|
||||
<MetricCard label={t('originality')} value={`${originalityPercent}%`} />
|
||||
<MetricCard label={t('citation')} value={`${citationPercent}%`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,26 +145,6 @@ function Skeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Error state ──────────────────────────────────────────────────────────────
|
||||
|
||||
function ErrorState() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-6 bg-slate-50">
|
||||
<div
|
||||
className="rounded-xl p-8 text-center"
|
||||
style={{ background: '#fff', border: `0.5px solid ${blue[100]}` }}
|
||||
>
|
||||
<p className="text-sm font-medium mb-1" style={{ color: blue[900] }}>
|
||||
Ma'lumot topilmadi
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: blue[400] }}>
|
||||
Ushbu tekshiruv mavjud emas yoki o'chirilgan
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function PlagiatResult({ id }: { id: number }) {
|
||||
@@ -186,7 +166,22 @@ export default function PlagiatResult({ id }: { id: number }) {
|
||||
});
|
||||
|
||||
if (isLoading) return <Skeleton />;
|
||||
if (isError || !rawData) return <ErrorState />;
|
||||
if (isError || !rawData)
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-6 bg-slate-50">
|
||||
<div
|
||||
className="rounded-xl p-8 text-center"
|
||||
style={{ background: '#fff', border: `0.5px solid ${blue[100]}` }}
|
||||
>
|
||||
<p className="text-sm font-medium mb-1" style={{ color: blue[900] }}>
|
||||
Ma'lumot topilmadi
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: blue[400] }}>
|
||||
Ushbu tekshiruv mavjud emas yoki o'chirilgan
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const data: PlagiatData = transformResponse(rawData);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const PageHeader: React.FC = () => {
|
||||
>
|
||||
<Plus size={15} className="text-white" />
|
||||
<h3 className="text-white font-semibold text-base">
|
||||
Plagiat tekshiruvi
|
||||
{t('plagiatCheck')}
|
||||
</h3>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
'use client';
|
||||
import { useEffect } from 'react';
|
||||
import Hero from './components/Hero';
|
||||
import InfoSection from './components/InfoSection';
|
||||
import StepsSection from './components/StepsSection';
|
||||
import Ticker from './components/Ticker';
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
|
||||
const PlagiarismLanding = () => {
|
||||
const route = useRouter();
|
||||
useEffect(() => {
|
||||
const data = localStorage.getItem('user');
|
||||
|
||||
if (data) {
|
||||
route.push('/plagat');
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface PlagiarismFormState {
|
||||
file: File | null;
|
||||
certificate: boolean;
|
||||
text?: string;
|
||||
type: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
export type PlagiarismFormErrors = Partial<
|
||||
|
||||
@@ -21,7 +21,7 @@ const INITIAL_FORM: PlagiarismFormState = {
|
||||
file: null,
|
||||
certificate: true,
|
||||
text: '',
|
||||
type: 'boshqa',
|
||||
type: 0,
|
||||
};
|
||||
|
||||
const PRICE: PriceCalculate = {
|
||||
@@ -71,6 +71,7 @@ export function usePlagiarismForm() {
|
||||
const priceInfo: PriceCalculate = {
|
||||
total_price: resdata?.total_price || 0,
|
||||
discount: resdata?.discount || 0,
|
||||
certificate: resdata?.certificate || 0,
|
||||
service_fee: resdata?.service_fee || 0,
|
||||
};
|
||||
setPrices(priceInfo);
|
||||
@@ -90,7 +91,7 @@ export function usePlagiarismForm() {
|
||||
const payment = useMutation({
|
||||
mutationKey: ['payload'],
|
||||
mutationFn: ({ order_id }: { order_id: number }) =>
|
||||
apiRequest<{ payment_link: string }>('POST', links.payment(order_id)),
|
||||
apiRequest<{ payment_link: string }>('POST', links.demo_pay(order_id)),
|
||||
onSuccess: (res) => {
|
||||
console.log('payment res: ', res);
|
||||
window.open(res.data.payment_link, '_self');
|
||||
@@ -116,9 +117,9 @@ export function usePlagiarismForm() {
|
||||
setErrors((prev) => ({ ...prev, file: undefined }));
|
||||
}, []);
|
||||
|
||||
const setOption = useCallback((option: string) => {
|
||||
setForm((prev) => ({ ...prev, document_type: option }));
|
||||
setErrors((prev) => ({ ...prev, document_type: undefined }));
|
||||
const setOption = useCallback((option: number) => {
|
||||
setForm((prev) => ({ ...prev, type: option }));
|
||||
setErrors((prev) => ({ ...prev, type: undefined }));
|
||||
}, []);
|
||||
|
||||
const toggleCertificate = useCallback(() => {
|
||||
@@ -150,7 +151,7 @@ export function usePlagiarismForm() {
|
||||
fd.append('text', form.text || '');
|
||||
fd.append('file', form.file!);
|
||||
fd.append('certificate', String(form.certificate));
|
||||
fd.append('type', form.type);
|
||||
fd.append('type', String(form.type));
|
||||
console.log('sended data: ', fd);
|
||||
checkdocumentRequest.mutate(fd);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { FieldWrapper } from './Plagiraismui';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiRequest } from '@/shared/request/apiRequest';
|
||||
@@ -12,8 +13,8 @@ type DocumentType = {
|
||||
};
|
||||
|
||||
interface DocumentsTypesProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -22,6 +23,7 @@ export default function DocumentsTypes({
|
||||
onChange,
|
||||
disabled,
|
||||
}: DocumentsTypesProps) {
|
||||
const t = useTranslations('DocumentTypes');
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['document_types'],
|
||||
queryFn: (): Promise<DocumentType[]> =>
|
||||
@@ -30,20 +32,24 @@ export default function DocumentsTypes({
|
||||
),
|
||||
});
|
||||
|
||||
const selected = data?.find((item) => item.id === value);
|
||||
|
||||
return (
|
||||
<FieldWrapper htmlFor="document_type" label="Hujjat turi">
|
||||
<FieldWrapper htmlFor="document_type" label={t('label')}>
|
||||
<select
|
||||
id="document_type"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
value={selected?.name}
|
||||
onChange={(e) => {
|
||||
onChange(Number(e.target.value));
|
||||
}}
|
||||
disabled={isLoading || disabled}
|
||||
className={`${inputCls} cursor-pointer`}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{isLoading ? 'Yuklanmoqda...' : 'Hujjat turini tanlang...'}
|
||||
{isLoading ? t('loading') : t('placeholder')}
|
||||
</option>
|
||||
{data?.map((type) => (
|
||||
<option key={type.id} value={String(type.id)}>
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user