Compare commits

...

4 Commits

Author SHA1 Message Date
nabijonovdavronbek619@gmail.com
9c2bc0f762 clear 2026-04-23 05:51:47 +05:00
nabijonovdavronbek619@gmail.com
dc99f9a8d7 navbar changed 2026-04-20 10:29:28 +05:00
nabijonovdavronbek619@gmail.com
aea8854a13 add service and sertificate prices , 2026-04-18 13:50:32 +05:00
nabijonovdavronbek619@gmail.com
75bd3467e9 update navbar , create pages 2026-04-18 11:34:41 +05:00
27 changed files with 490 additions and 246 deletions

BIN
public/lanka.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -26,7 +26,7 @@ AntiPlagiat.uz quyidagi xizmatlarni taqdim etadi:
1. **Plagiat tekshiruvi** — PDF, DOC, DOCX, TXT formatdagi hujjatlarni plagiatga tekshirish. Maks fayl hajmi: 20 MB. Natija foiz ko'rinishida beriladi.
2. **SI (Sun'iy Intellekt) detektor** — Matnning sun'iy intellekt tomonidan yozilganligini aniqlash. AI generatsiya ehtimolini foizda ko'rsatadi.
3. **Rasmiy sertifikat** — Orijinallik tasdiqlangan hujjatlar uchun rasmiy sertifikat beriladi.
3. **Rasmiy sertifikat** — Originallik tasdiqlangan hujjatlar uchun rasmiy sertifikat beriladi.
4. **Batafsil hisobot** — O'xshashlik manbalari, mos keladigan so'zlar, AI tahlili ballari ko'rsatiladi.
---

View File

@@ -0,0 +1,26 @@
'use client';
import { useEffect, useState } from 'react';
import { Dashboard } from '@/widgets/cabinet/ui/dashboard';
import { MOCK_USER } from '@/widgets/cabinet/lib/mock';
export default function Page() {
const [userName, setUserName] = useState(MOCK_USER.first_name);
useEffect(() => {
const data = localStorage.getItem('user');
if (data) {
try {
const user = JSON.parse(data);
setUserName(user.name || MOCK_USER.first_name);
} catch {
// ignore
}
}
}, []);
return (
<div className="max-w-6xl mx-auto w-full pt-20 px-4 pb-10">
<Dashboard userName={userName} />
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { PaymentsTable } from '@/widgets/cabinet/ui/tables/PaymentsTable';
export default function Page() {
return (
<div className="max-w-6xl mx-auto w-full pt-20 px-4 pb-10">
<PaymentsTable />
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { ProfileSection } from '@/widgets/cabinet/ui/profile';
import { MOCK_STATS } from '@/widgets/cabinet/lib/mock';
export default function Page() {
return (
<div className="max-w-6xl mx-auto w-full pt-20 px-4 pb-10">
<ProfileSection stats={MOCK_STATS} />
</div>
);
}

View File

@@ -0,0 +1,12 @@
import { SiTable } from '@/widgets/cabinet/ui/tables/SiTable';
import { SiUploadSection } from '@/features/modals/siModal/ui/SiUploadSection';
import React from 'react';
export default function Page() {
return (
<div className="max-w-6xl mx-auto w-full pt-20 px-4">
<SiUploadSection />
<SiTable />
</div>
);
}

View File

@@ -52,7 +52,7 @@ export function useLoginForm() {
console.log('Login successful:', data);
toggleLoginModal();
toast.success('Kirish muvaffaqiyatli!');
route.push('/plagiat');
route.push('/dashboard');
},
onError: (err) => {
console.log('Login failed:', err);

View File

@@ -1,54 +1,30 @@
'use client';
import { useState } from 'react';
import { ArrowRight, BrainCircuit, Plus } from 'lucide-react';
import { FileUploadModal } from './ui/fileUploadModal';
import { ArrowRight, BrainCircuit } from 'lucide-react';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
export function SiCTACard() {
const [isOpen, setIsOpen] = useState(false);
const t = useTranslations('Cabinet');
return (
<>
<button
onClick={() => setIsOpen(true)}
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">
{t('siDetector')}
</h3>
<p className="text-violet-100 text-sm mb-4 leading-relaxed">
{t('siDesc')}
</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>
</button>
<FileUploadModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
);
}
export function SiButton() {
const [isOpen, setIsOpen] = useState<boolean>(false);
const t = useTranslations('Cabinet');
return (
<>
<button
onClick={() => setIsOpen(true)}
className="flex items-center gap-2 py-1 px-2 group relative overflow-hidden rounded-sm bg-linear-to-br from-violet-500 to-violet-600 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
>
<Plus size={15} className="text-white" />
<h3 className="text-white font-semibold text-base">
{t('siDetector')}
</h3>
</button>
<FileUploadModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
<Link
href="/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">
{t('siDetector')}
</h3>
<p className="text-violet-100 text-sm mb-4 leading-relaxed">
{t('siDesc')}
</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>
);
}

View File

@@ -0,0 +1,117 @@
'use client';
import { DEFAULT_PRICING, formatPrice } from '../utils/pricing';
import { PricingConfig } from '../utils/tyeps';
import { useFileUpload } from '../utils/useFileUpload';
import { SUPPORTED_EXTENSIONS } from '../utils/wordCount';
import { ErrorBanner, FileChip, PricingInfo, Spinner } from './modalParts';
import { DropZone } from './dropZone';
import { useTranslations } from 'next-intl';
interface SiUploadSectionProps {
pricing?: PricingConfig;
}
export function SiUploadSection({
pricing = DEFAULT_PRICING,
}: SiUploadSectionProps) {
const t = useTranslations('Cabinet');
const {
documentName,
setDocumentName,
uploadedFile,
isDragging,
isProcessing,
error,
fileInputRef,
handleFileSelect,
handleDrop,
handleDragOver,
handleDragLeave,
handleRemoveFile,
openFilePicker,
canSubmit,
handleSubmit,
} = useFileUpload();
const wordCount = uploadedFile?.word_count ?? 0;
const totalPrice = uploadedFile?.total_price ?? 0;
return (
<div className="w-full rounded-2xl bg-white border border-slate-100 shadow-sm flex flex-col gap-5 p-6 mb-8">
<div>
<h2 className="text-xl font-semibold text-slate-800 tracking-tight">
{t('siDetector')}
</h2>
<p className="text-sm text-slate-500 mt-0.5">{t('siDesc')}</p>
</div>
<div className="flex flex-col gap-1.5">
<label
htmlFor="doc-name"
className="text-sm font-medium text-slate-700"
>
Document name{' '}
<span className="text-red-500" aria-hidden>
*
</span>
</label>
<input
id="doc-name"
type="text"
value={documentName}
onChange={(e) => setDocumentName(e.target.value)}
placeholder="Enter document name…"
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-800 placeholder:text-slate-400 outline-none transition-all duration-150 focus:border-blue-400 focus:ring-4 focus:ring-blue-400/10"
/>
</div>
<input
ref={fileInputRef}
type="file"
accept={SUPPORTED_EXTENSIONS.join(',')}
className="hidden"
aria-hidden="true"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFileSelect(file);
}}
/>
{uploadedFile ? (
<div className="animate-in fade-in slide-in-from-top-1 duration-200">
<FileChip file={uploadedFile} onRemove={handleRemoveFile} />
</div>
) : (
<DropZone
isDragging={isDragging}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={openFilePicker}
/>
)}
{error && <ErrorBanner message={error} />}
{uploadedFile?.status === 'done' && wordCount > 0 && (
<PricingInfo
wordCount={wordCount}
minimumPrice={formatPrice(pricing.minimumPayment)}
totalPrice={formatPrice(totalPrice)}
/>
)}
<div className="flex justify-end">
<button
onClick={handleSubmit}
disabled={!canSubmit}
className="relative flex items-center gap-2 rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-blue-500/25 transition-all duration-150 hover:bg-blue-700 hover:shadow-blue-500/40 active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-2"
>
{isProcessing && <Spinner />}
Check
</button>
</div>
</div>
);
}

View File

@@ -10,7 +10,9 @@
"login": "Login",
"signup": "Sign up",
"profile": "Profile",
"logout": "Logout"
"logout": "Logout",
"aboutPlagiat": "About Plagiarism",
"howItWorks": "How It Works"
},
"Footer": {
"product": "Product",
@@ -51,7 +53,9 @@
"certificateDescription": "An official certificate will be attached to your originality report.",
"submitting": "Submitting…",
"submitButton": "Submit for Originality Check",
"dismiss": "Dismiss"
"dismiss": "Dismiss",
"service_price": "Service price {PLAGIAT_SERVICE_FEE} UZS",
"sertificate_price": "Certificate price {SERTIFICATE_PRICE} UZS"
},
"HistoryPage": {
"title": "Check History",
@@ -61,8 +65,8 @@
"date": "Date",
"amount": "Amount",
"result": "Result",
"fileName":"File name",
"count":"N_",
"fileName": "File name",
"count": "N_",
"actions": "",
"state": "Payment status",
"emptyMessage": "No plagiarism checks found.",
@@ -237,7 +241,7 @@
"security": "Secured by Payme · SSL encrypted",
"serviceFee": "Service fee",
"discountLabel": "Discount",
"sertificateLabel":"Certificate",
"sertificateLabel": "Certificate",
"total": "Total",
"paymentRequired": "Payment not completed",
"connecting": "Connecting to Payme…",

View File

@@ -10,7 +10,9 @@
"login": "Войти",
"signup": "Регистрация",
"profile": "Профиль",
"logout": "Выйти"
"logout": "Выйти",
"aboutPlagiat": "О плагиате",
"howItWorks": "Как это работает"
},
"Footer": {
"product": "Продукт",
@@ -51,7 +53,9 @@
"certificateDescription": "Официальный сертификат будет прикреплен к вашему отчету об оригинальности.",
"submitting": "Отправка…",
"submitButton": "Отправить на проверку оригинальности",
"dismiss": "Закрыть"
"dismiss": "Закрыть",
"service_price": "Стоимость услуги {PLAGIAT_SERVICE_FEE} сум",
"sertificate_price": "Стоимость сертификата {SERTIFICATE_PRICE} сум"
},
"HistoryPage": {
"title": "История проверок",
@@ -61,7 +65,7 @@
"date": "Дата",
"amount": "Сумма",
"result": "Результат",
"count":"H_",
"count": "H_",
"actions": "",
"state": "Статус оплаты",
"emptyMessage": "Проверки на плагиат не найдены.",
@@ -236,7 +240,7 @@
"security": "Защищено Payme · SSL шифрование",
"serviceFee": "Стоимость услуги",
"discountLabel": "Скидка",
"sertificateLabel":"Сертификат",
"sertificateLabel": "Сертификат",
"total": "Итого",
"paymentRequired": "Оплата не произведена",
"connecting": "Подключение к Payme…",

View File

@@ -14,6 +14,8 @@ declare const messages: {
signup: "Ro'yxatdan o'tish";
profile: 'Profil';
logout: 'Chiqish';
aboutPlagiat: 'Plagiat haqida';
howItWorks: 'Bu qanday ishlaydi';
};
Footer: {
product: 'Mahsulot';
@@ -35,11 +37,11 @@ declare const messages: {
terms: 'Foydalanish shartlari';
};
PlagiarismCheck: {
badge: 'Orijinallik tekshiruvi';
badge: 'Originallik tekshiruvi';
title: 'Hujjatni yuboring';
submissionSuccess: 'Yuborish muvaffaqiyatli yakunlandi! ID';
secureNote: 'Hujjatingiz xavfsiz qayta ishlanadi va tahlil muddati tugagach saqlanmaydi.';
description: "Hujjatning orijinalligini tekshirish uchun yuklang. Natijalar odatda bir necha daqiqada tayyor bo'ladi.";
description: "Hujjatning originalligini tekshirish uchun yuklang. Natijalar odatda bir necha daqiqada tayyor bo'ladi.";
documentTopic: 'Hujjat mavzusi';
topicPlaceholder: "masalan: Sun'iy intellektning ta'limga ta'siri";
senderFullName: "Yuboruvchi to'liq ismi";
@@ -51,10 +53,12 @@ declare const messages: {
autoFilled: "Avto-to'ldirilgan";
removeFile: 'Faylni olib tashlash';
certificateTitle: 'Natijani sertifikat bilan qaytarish';
certificateDescription: 'Rasmiy sertifikat sizning orijinallik hisobotingizga ilova qilinadi.';
certificateDescription: 'Rasmiy sertifikat sizning originallik hisobotingizga ilova qilinadi.';
submitting: 'Yuborilmoqda…';
submitButton: 'Orijinallik tekshiruvi uchun yuborish';
submitButton: 'Originallik tekshiruvi uchun yuborish';
dismiss: 'Yopish';
service_price: "Xizmat narxi {PLAGIAT_SERVICE_FEE} so'm";
sertificate_price: "Sertifikat narxi {SERTIFICATE_PRICE} so'm";
};
HistoryPage: {
title: 'Tekshiruv tarixi';
@@ -188,10 +192,10 @@ declare const messages: {
card2Title: 'Nima uchun hujjatingizni tekshirish kerak?';
card2Desc: "Akademik halollikni ta'minlang, jarimalardan qoching va obro'ingizni saqlang. Bizning xizmatimiz kompleks plagiat aniqlashni taqdim etadi.";
card3Title: 'Siz nima olasiz';
card3Desc: "Batafsil plagiat hisoboti, o'xshashlik foizi, topilgan manbalar va orijinallikning rasmiy sertifikati.";
card3Desc: "Batafsil plagiat hisoboti, o'xshashlik foizi, topilgan manbalar va originallikning rasmiy sertifikati.";
};
Ticker: {
item1: 'Orijinallik tasdiqlangan';
item1: 'Originallik tasdiqlangan';
item2: 'Akademik halollik';
item3: 'Ishonchli hisobotlar';
item4: 'Chuqur tahlil';

View File

@@ -10,7 +10,9 @@
"login": "Kirish",
"signup": "Ro'yxatdan o'tish",
"profile": "Profil",
"logout": "Chiqish"
"logout": "Chiqish",
"aboutPlagiat": "Plagiat haqida",
"howItWorks": "Bu qanday ishlaydi"
},
"Footer": {
"product": "Mahsulot",
@@ -32,11 +34,11 @@
"terms": "Foydalanish shartlari"
},
"PlagiarismCheck": {
"badge": "Orijinallik tekshiruvi",
"badge": "Originallik tekshiruvi",
"title": "Hujjatni yuboring",
"submissionSuccess": "Yuborish muvaffaqiyatli yakunlandi! ID",
"secureNote": "Hujjatingiz xavfsiz qayta ishlanadi va tahlil muddati tugagach saqlanmaydi.",
"description": "Hujjatning orijinalligini tekshirish uchun yuklang. Natijalar odatda bir necha daqiqada tayyor bo'ladi.",
"description": "Hujjatning originalligini tekshirish uchun yuklang. Natijalar odatda bir necha daqiqada tayyor bo'ladi.",
"documentTopic": "Hujjat mavzusi",
"topicPlaceholder": "masalan: Sun'iy intellektning ta'limga ta'siri",
"senderFullName": "Yuboruvchi to'liq ismi",
@@ -48,18 +50,20 @@
"autoFilled": "Avto-to'ldirilgan",
"removeFile": "Faylni olib tashlash",
"certificateTitle": "Natijani sertifikat bilan qaytarish",
"certificateDescription": "Rasmiy sertifikat sizning orijinallik hisobotingizga ilova qilinadi.",
"certificateDescription": "Rasmiy sertifikat sizning originallik hisobotingizga ilova qilinadi.",
"submitting": "Yuborilmoqda…",
"submitButton": "Orijinallik tekshiruvi uchun yuborish",
"dismiss": "Yopish"
"submitButton": "Originallik tekshiruvi uchun yuborish",
"dismiss": "Yopish",
"service_price": "Xizmat narxi {PLAGIAT_SERVICE_FEE} so'm",
"sertificate_price": "Sertifikat narxi {SERTIFICATE_PRICE} so'm"
},
"HistoryPage": {
"title": "Tekshiruv tarixi",
"description": "Siz tomonidan yuborilgan barcha plagiat tekshiruvlari",
"sender": "Yuboruvchi",
"file": "Fayl",
"fileName":"Fayl nomi",
"count":"N_",
"fileName": "Fayl nomi",
"count": "N_",
"date": "Sana",
"amount": "Summa",
"result": "Natija",
@@ -126,8 +130,8 @@
"downloadCertificate": "Sertifikatni yuklab olish",
"unknownError": "Noma'lum xato",
"words": "so'z",
"aiProbabilityText":"Ai yordamida yaratilganlik ehtimoli aniqlandi",
"documentNumber":"Dokument mavzusi",
"aiProbabilityText": "Ai yordamida yaratilganlik ehtimoli aniqlandi",
"documentNumber": "Dokument mavzusi",
"scoreAiContent": "O'zidan iqtibos keltirish",
"scoreOriginality": "Originallik",
"scorePlagiarism": "Plagiat",
@@ -185,10 +189,10 @@
"card2Title": "Nima uchun hujjatingizni tekshirish kerak?",
"card2Desc": "Akademik halollikni ta'minlang, jarimalardan qoching va obro'ingizni saqlang. Bizning xizmatimiz kompleks plagiat aniqlashni taqdim etadi.",
"card3Title": "Siz nima olasiz",
"card3Desc": "Batafsil plagiat hisoboti, o'xshashlik foizi, topilgan manbalar va orijinallikning rasmiy sertifikati."
"card3Desc": "Batafsil plagiat hisoboti, o'xshashlik foizi, topilgan manbalar va originallikning rasmiy sertifikati."
},
"Ticker": {
"item1": "Orijinallik tasdiqlangan",
"item1": "Originallik tasdiqlangan",
"item2": "Akademik halollik",
"item3": "Ishonchli hisobotlar",
"item4": "Chuqur tahlil",
@@ -237,9 +241,9 @@
"security": "Payme tomonidan himoyalangan · SSL shifrlash",
"serviceFee": "Xizmat to'lovi",
"discountLabel": "Chegirma",
"sertificateLabel":"Sertifikat",
"sertificateLabel": "Sertifikat",
"total": "Jami",
"paymentRequired":"To'lov qilinmagan",
"paymentRequired": "To'lov qilinmagan",
"connecting": "Paymega ulanmoqda…",
"payButton": "Payme orqali to'lash"
},

View File

@@ -143,11 +143,19 @@ api.interceptors.response.use(
};
const status = error.response?.status;
// const responseData = error.response?.data as Record<string, unknown> | undefined;
const requestUrl = originalRequest.url ?? '';
const isAuthEndpoint =
requestUrl.includes('/users/login/') ||
requestUrl.includes('/users/register/');
// 403 with token_not_valid means the token is expired — clear and redirect
if (status === 403) {
TokenStorage.clear();
redirectToMain();
return Promise.reject(error);
}
// For auth endpoints, 401 means wrong credentials — show error, don't refresh
if (isAuthEndpoint || status !== 401 || originalRequest._retry) {
toast.error(extractErrorMessage(error));

View File

@@ -2,8 +2,6 @@
import React from 'react';
import dynamic from 'next/dynamic';
import { AnimatePresence, motion } from 'framer-motion';
import { Sidebar } from './Sidebar';
import { CabinetNav } from './CabinetNav';
import { Dashboard } from './dashboard';
import { useCabinet } from '../lib/hooks/useCabinet';
import { MOCK_USER, MOCK_STATS } from '../lib/mock';
@@ -69,31 +67,15 @@ const FADE = {
// ─── CabinetLayout ────────────────────────────────────────────────────────────
export const CabinetLayout: React.FC = () => {
const { activeSection, navigate, isSidebarOpen, toggleSidebar } =
useCabinet();
const fullName = `${MOCK_USER.first_name} ${MOCK_USER.last_name}`;
const { activeSection } = useCabinet();
return (
<div className="flex bg-slate-50 min-h-screen">
<Sidebar
active={activeSection}
onNavigate={navigate}
isOpen={isSidebarOpen}
onClose={toggleSidebar}
userName={fullName}
/>
<div className="flex-1 flex flex-col min-w-0">
<CabinetNav activeSection={activeSection} onMenuClick={toggleSidebar} />
<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} />
</motion.div>
</AnimatePresence>
</main>
</div>
</div>
<main className="flex-1 p-4 md:p-6 lg:p-8 max-w-6xl mx-auto w-full pt-20">
<AnimatePresence mode="wait">
<motion.div key={activeSection} {...FADE}>
<SectionContent section={activeSection} />
</motion.div>
</AnimatePresence>
</main>
);
};

View File

@@ -1,11 +1,12 @@
'use client';
import React from 'react';
import React, { useState } from 'react';
import { Clock, XCircle, ReceiptText } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
import PaymentStatus from '@/widgets/detail/paidStatus';
import { Pagination } from '@/widgets/history/ui/pagination';
// import { toast } from 'react-toastify';
// import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
@@ -36,9 +37,11 @@ function formatPrice(price: string) {
// ─── Component ─────────────────────────────────────────────────────────────────
const PAGE_SIZE = 10;
export function PaymentsTable() {
const t = useTranslations('Cabinet');
// const [isPaymentOpen, setIsPaymentOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['pay_history'],
queryFn: (): Promise<Inspection[]> =>
@@ -47,6 +50,12 @@ export function PaymentsTable() {
),
});
const totalPages = Math.ceil((data?.length ?? 0) / PAGE_SIZE);
const pageItems = data?.slice(
(currentPage - 1) * PAGE_SIZE,
currentPage * PAGE_SIZE,
);
// const payment = useMutation({
// mutationKey: ['payload'],
// mutationFn: ({ order_id }: { order_id: number }) =>
@@ -93,31 +102,30 @@ export function PaymentsTable() {
<p className="text-sm">{t('noPayments')}</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-100">
{[
t('tableNum'),
t('service'),
t('amount'),
t('discount'),
t('date'),
t('status'),
].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 service_fee = row.total_price + row.discount;
return (
<>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-100">
{[
t('tableNum'),
t('service'),
t('amount'),
t('discount'),
t('date'),
t('status'),
].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">
{pageItems!.map((row) => (
<tr
key={row.id}
className="hover:bg-slate-50/60 transition-colors"
@@ -155,25 +163,17 @@ export function PaymentsTable() {
</span>
)}
</td>
{/* <PaymentModal
isOpen={isPaymentOpen}
onClose={() => setIsPaymentOpen(false)}
price={{
service_fee: Number(service_fee),
discount: Number(row.discount) || 0,
total_price: Number(row.total_price) || 0,
}}
onConfirmPayment={() => {
handleSubmit({ document_id: 0 });
}}
isLoading={payment.isPending}
/> */}
</tr>
);
})}
</tbody>
</table>
</div>
))}
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</>
)}
</div>
</div>

View File

@@ -1,5 +1,5 @@
'use client';
import React from 'react';
import React, { useState } from 'react';
import { Download, CreditCard, Eye } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
@@ -9,8 +9,8 @@ import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
import { toast } from 'react-toastify';
import type { SiDocument } from '../../lib/types';
import { SiButton } from '@/features/modals/siModal/page';
import { useRouter, useParams } from 'next/navigation';
import { Pagination } from '@/widgets/history/ui/pagination';
// ─── State badge ───────────────────────────────────────────────────────────────
@@ -181,9 +181,18 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
// ─── SiTable ───────────────────────────────────────────────────────────────────
const PAGE_SIZE = 10;
export const SiTable: React.FC = () => {
const t = useTranslations('Cabinet');
const { items, isLoading, isError } = useSiHistory();
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(items.length / PAGE_SIZE);
const pageItems = items.slice(
(currentPage - 1) * PAGE_SIZE,
currentPage * PAGE_SIZE,
);
return (
<div className="space-y-4">
@@ -194,7 +203,6 @@ export const SiTable: React.FC = () => {
{isLoading ? '...' : t('checksCount', { count: items.length })}
</p>
</div>
<SiButton />
</div>
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
@@ -230,12 +238,21 @@ export const SiTable: React.FC = () => {
{!isLoading && !isError && items.length === 0 && <EmptyState />}
{!isLoading &&
!isError &&
items.map((item, i) => (
<SiRow key={item.id} item={item} index={i + 1} />
pageItems.map((item, i) => (
<SiRow
key={item.id}
item={item}
index={(currentPage - 1) * PAGE_SIZE + i + 1}
/>
))}
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</div>
</div>
);

View File

@@ -36,14 +36,15 @@ export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => {
useEffect(() => {
if (data) {
const start = (currentPage - 1) * pageSize;
setState({
items: data?.results || [],
items: (data.results || []).slice(start, start + pageSize),
status: 'success',
error: null,
});
setTotal(data.total || 0);
}
}, [data]);
}, [data, currentPage, pageSize]);
useEffect(() => {
refetch();

View File

@@ -37,7 +37,7 @@ const InfoSection: FC = () => {
};
return (
<div style={{ background: C.surfaceWarm }}>
<div id="info-section" style={{ background: C.surfaceWarm }}>
<Section style={{ paddingTop: 96, paddingBottom: 96 }}>
{/* Heading */}
<motion.div

View File

@@ -19,6 +19,7 @@ const StepsSection = () => {
return (
<div
id="steps-section"
style={{
background: C.surface,
borderTop: `1px solid ${C.border}`,

View File

@@ -1,20 +1,23 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from '@/shared/config/i18n/navigation';
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');
const router = useRouter();
if (data) {
route.push('/plagiat');
useEffect(() => {
const token = localStorage.getItem('access_token');
const user = localStorage.getItem('user');
if (token && user) {
router.push('/plagiat');
}
}, []);
}, [router]);
return (
<>
<Hero />

View File

@@ -7,82 +7,39 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/shared/ui/dropdown-menu';
import SubMenuLink from './SubMenuLink';
import { ChangeLang } from './ChangeLang';
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
import { useTranslations } from 'next-intl';
import { useUserPlagiatStore } from '@/shared/zustand/user';
import {
ChevronDown,
LogOut,
User,
LayoutDashboard,
FileSearch,
BrainCircuit,
CreditCard,
} from 'lucide-react';
import { ChevronDown, LogOut } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useCabinetNav } from '@/shared/zustand/cabinetNav';
function AuthButtons() {
const t = useTranslations('Navbar');
const t_cab = useTranslations('Cabinet');
const setNavItem = useCabinetNav((state) => state.setNavItem);
const [token, setToken] = useState<string | null>(null);
const [localUser, setLocalUser] = useState<{
id: number;
name: string;
surname: string;
} | null>(null);
const [open, setOpen] = useState(false);
const auth = {
login: { title: t('login'), url: '#' },
signup: { title: t('signup'), url: '#' },
};
const userItem = [
{ title: t('profile'), url: '/cabinet', icon: User, key: 'profile' },
{
url: '/cabinet',
title: t_cab('dashboard'),
icon: LayoutDashboard,
key: 'dashboard',
},
{
url: '/cabinet',
title: t_cab('plagiat'),
icon: FileSearch,
key: 'plagiat',
},
{
url: '/cabinet',
title: t_cab('siNav'),
icon: BrainCircuit,
key: 'si',
},
{
url: '/cabinet',
title: t_cab('payments'),
icon: CreditCard,
key: 'payments',
},
{ title: t('logout'), url: '/', icon: LogOut, key: 'logout' },
];
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
const toggleRegisterModal = useRegisterModal(
(state) => state.toggleRegisterModal,
);
const user = useUserPlagiatStore((state) => state.user);
const clearUser = useUserPlagiatStore((state) => state.clearUser);
const clearTokens = () => {
localStorage.removeItem('access');
localStorage.removeItem('refresh');
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
clearUser();
};
console.log('Current user:', user);
useEffect(() => {
const token_data = localStorage.getItem('access_token');
setToken(token_data);
const data = localStorage.getItem('user');
if (data) {
setLocalUser(JSON.parse(data));
@@ -91,9 +48,9 @@ function AuthButtons() {
}
}, [user]);
if (localUser) {
if (localUser && token) {
return (
<div className="flex flex-row max-sm:items-center max-sm:justify-around gap-3">
<div className="flex flex-row max-sm:items-center max-sm:justify-around gap-3 items-center">
<div className="sm:flex hidden">
<ChangeLang />
</div>
@@ -102,24 +59,21 @@ function AuthButtons() {
{localUser.name}
<ChevronDown className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent className="">
{userItem.map((subItem) => (
<DropdownMenuItem key={subItem.title} asChild>
<SubMenuLink
logOut={() => {
setOpen(false);
if (subItem.url !== '/cabinet') {
clearTokens();
} else {
setNavItem(
subItem.key as import('@/widgets/cabinet/lib/types').CabinetSection,
);
}
}}
item={subItem}
/>
</DropdownMenuItem>
))}
<DropdownMenuContent>
<DropdownMenuItem
onClick={() => {
clearTokens();
setOpen(false);
}}
>
<Link
className="flex flex-row gap-4 rounded-md p-3 cursor-pointer"
href={'/'}
>
<LogOut className="size-5 shrink-0 text-foreground" />
<span className="text-sm font-semibold">{t('logout')}</span>
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -127,16 +81,14 @@ function AuthButtons() {
}
return (
<div className="flex flex-row max-sm:items-center max-sm:justify-around gap-3">
<div className="flex flex-row max-sm:items-center max-sm:justify-around gap-3 items-center">
<div className="sm:flex hidden">
<ChangeLang />
</div>
<Button variant="outline" onClick={() => toggleLoginModal()}>
<Link href={auth.login.url}>{auth.login.title}</Link>
</Button>
<Button onClick={() => toggleRegisterModal()}>
<Link href={auth.signup.url}>{auth.signup.title}</Link>
{t('login')}
</Button>
<Button onClick={() => toggleRegisterModal()}>{t('signup')}</Button>
</div>
);
}

View File

@@ -1,3 +1,4 @@
'use client';
import { Button } from '@/shared/ui/button';
import {
Sheet,
@@ -13,16 +14,43 @@ import { AuthButtons } from './authButtons';
import { useTranslations } from 'next-intl';
import { Logo_image } from '@/image';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { useUserPlagiatStore } from '@/shared/zustand/user';
const Navbar = () => {
const t = useTranslations('Navbar');
const t_cab = useTranslations('Cabinet');
const [token, setToken] = useState<string | null>(null);
const user = useUserPlagiatStore((state) => state.user);
const scrollTo = (id: string) => {
const el = document.getElementById(id);
if (el) el.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
const data = localStorage.getItem('access_token');
if (data) {
setToken(data);
} else {
setToken(null);
}
}, [user]);
const navItems = [
{ title: t_cab('dashboard'), href: '/dashboard' as const },
{ title: t_cab('plagiat'), href: '/plagiat' as const },
{ title: t_cab('siNav'), href: '/si' as const },
{ title: t_cab('payments'), href: '/payments' as const },
{ title: t('profile'), href: '/profile' as const },
];
return (
<section className="py-1 flex items-center justify-center w-full ">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
{/* Desktop Menu */}
<nav className="justify-between items-center flex max-sm:flex-col gap-5">
<div className="flex items-center justify-between gap-6">
<nav className="justify-between items-center md:flex hidden max-sm:flex-col gap-5">
<div className="flex items-center justify-around gap-2 w-full">
{/* Logo */}
<Link
href={'/'}
@@ -36,6 +64,36 @@ const Navbar = () => {
height={10}
/>
</Link>
{token ? (
<div className="flex items-center gap-2">
{navItems.map((item) => (
<Link
key={item.title}
href={item.href}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md hover:bg-muted transition-colors"
>
{item.title}
</Link>
))}
</div>
) : (
<nav className="hidden sm:flex items-center gap-1">
<button
onClick={() => scrollTo('info-section')}
className="px-3 py-1.5 text-sm font-medium rounded-md hover:bg-muted transition-colors"
>
{t('aboutPlagiat')}
</button>
<button
onClick={() => scrollTo('steps-section')}
className="px-3 py-1.5 text-sm font-medium rounded-md hover:bg-muted transition-colors"
>
{t('howItWorks')}
</button>
</nav>
)}
<div className="flex sm:hidden items-center justify-center">
<ChangeLang />
</div>
@@ -44,11 +102,20 @@ const Navbar = () => {
</nav>
{/* Mobile Menu */}
<div className="hidden">
<div className="flex items-center justify-between">
<div className="md:hidden flex">
<div className="flex items-center justify-between w-full">
{/* Logo */}
<Link href={'/'} className="flex items-center gap-2">
{t('logo')}
<Link
href={'/'}
className="flex items-center gap-2 text-2xl font-bold "
>
<Image
src={Logo_image}
className="min-h-2"
alt="Anti-Plagiat.uz"
width={140}
height={10}
/>
</Link>
<Sheet>
<div className="space-x-2">
@@ -62,11 +129,48 @@ const Navbar = () => {
<SheetContent className="overflow-y-auto">
<SheetHeader>
<SheetTitle>
<Link href={'/'} className="flex items-center gap-2">
{t('logo')}
<Link
href={'/'}
className="flex items-center gap-2 text-2xl font-bold "
>
<Image
src={Logo_image}
className="min-h-2"
alt="Anti-Plagiat.uz"
width={140}
height={10}
/>
</Link>
</SheetTitle>
</SheetHeader>
{token ? (
<div className="flex flex-col items-start gap-2 pl-2">
{navItems.map((item) => (
<Link
key={item.title}
href={item.href}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md hover:bg-muted transition-colors"
>
{item.title}
</Link>
))}
</div>
) : (
<nav className="flex flex-col items-start gap-1 pl-2">
<button
onClick={() => scrollTo('info-section')}
className="px-3 py-1.5 text-sm font-medium rounded-md hover:bg-muted transition-colors"
>
{t('aboutPlagiat')}
</button>
<button
onClick={() => scrollTo('steps-section')}
className="px-3 py-1.5 text-sm font-medium rounded-md hover:bg-muted transition-colors"
>
{t('howItWorks')}
</button>
</nav>
)}
<div className="flex flex-col gap-6 p-4">
<AuthButtons />
</div>

View File

@@ -21,7 +21,7 @@ import { SERTIFICATE_PRICE, PLAGIAT_SERVICE_FEE } from '@/shared/lib/metadata';
const INITIAL_FORM: PlagiarismFormState = {
title: '',
file: null,
certificate: true,
certificate: false,
text: '',
type: 0,
};

View File

@@ -13,6 +13,7 @@ import { usePlagiarismForm } from '../lib/usePlagiraism';
import { useTranslations } from 'next-intl';
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
import DocumentsTypes from './documentsType';
import { PLAGIAT_SERVICE_FEE } from '@/shared/lib/metadata';
export const inputCls = `
w-full px-3.5 py-3.5 text-[14px] text-slate-800
@@ -113,7 +114,7 @@ export function PlagiarismCheckForm() {
)}
{/* left part */}
<div className="flex flex-col gap-9 md:max-w-[50%] w-full">
<div className="flex flex-col gap-6 md:max-w-[50%] w-full">
{/* Topic */}
<FieldWrapper
label={t('documentTopic')}
@@ -131,6 +132,9 @@ export function PlagiarismCheckForm() {
maxLength={200}
disabled={isLoading}
/>
<p className="text-sm text-stone-500 ml-2 ">
{t('service_price', { PLAGIAT_SERVICE_FEE })}
</p>
</FieldWrapper>
{/* Sender Full Name (read-only) */}

View File

@@ -1,3 +1,5 @@
import { SERTIFICATE_PRICE } from '@/shared/lib/metadata';
import { useTranslations } from 'next-intl';
import React, { useEffect } from 'react';
// ─── FieldWrapper ────────────────────────────────────────────────────────────
@@ -219,6 +221,7 @@ export function CertificateCheckbox({
title = 'Return result with certificate',
description = 'An official certificate will be attached to your originality report.',
}: CertificateCheckboxProps) {
const t = useTranslations('PlagiarismCheck');
return (
<label
className={`
@@ -260,6 +263,9 @@ export function CertificateCheckbox({
</div>
<div>
<p className="text-sm font-semibold text-stone-800">{title}</p>
<p className="text-sm text-stone-500">
{t('sertificate_price', { SERTIFICATE_PRICE })}
</p>
<p className="text-xs text-stone-500 mt-0.5">{description}</p>
</div>
</label>