diff --git a/package-lock.json b/package-lock.json index 75440b7..94db69e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.13", "framer-motion": "^12.38.0", + "jszip": "^3.10.1", "lucide-react": "^0.503.0", "next": "15.5.9", "next-intl": "^4.3.9", @@ -4965,6 +4966,12 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6570,6 +6577,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -6597,6 +6610,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -7165,6 +7184,18 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7209,6 +7240,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -8160,6 +8200,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8314,6 +8360,12 @@ "node": ">=6.0.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8505,6 +8557,27 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/recharts": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", @@ -8713,6 +8786,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/safe-json-stringify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", @@ -8822,6 +8901,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -9049,6 +9134,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -9680,6 +9774,12 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", diff --git a/package.json b/package.json index 9b02c25..b33017f 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.13", "framer-motion": "^12.38.0", + "jszip": "^3.10.1", "lucide-react": "^0.503.0", "next": "15.5.9", "next-intl": "^4.3.9", diff --git a/public/modules.html b/public/modules.html new file mode 100644 index 0000000..7e24569 --- /dev/null +++ b/public/modules.html @@ -0,0 +1,178 @@ + + + +
+ +
+
30
Jami modullar
+
10
Bepul internet manbalari
+
5
AI tahlil modullari
+
4
Kategoriya
+
+ +
Ilmiy va ta'lim bazalari
+
+
+
+
eLIBRARY.RU
Rus va xorijiy tillardagi ilmiy maqolalarning to'liq matnlari bazasi
Ilmiy
+
+
+
+
Публикации eLIBRARY (tarjima va qayta bayon)
Tarjima va parafraz qilingan maqolalarni aniqlash
AI tahlil
+
+
+
+
RDK to'plami
Rossiya Davlat kutubxonasidan dissertatsiya va avtoreferatlar
Ilmiy
+
+
+
+
BMK dissertatsiyalari
Belarus milliy kutubxonasi dissertatsiyalari va avtoreferatlari
Ilmiy
+
+
+
+
IEEE
Xalqaro elektrotexnika va elektronika muhandislari instituti bazasi
Ilmiy
+
+
+
+
IEEE parafraz moduli
IEEE maqolalarining qayta bayon qilingan variantlarini aniqlash
AI tahlil
+
+
+
+
Elektron-kutubxona tizimlari
Book.ru, Юрайт, Лань, Айбукс va boshqa ELS bazalari
Ilmiy
+
+
+
+
OTMlar halqasi
O'zbekiston oliy ta'lim muassasalari birgalikdagi bazasi
Ilmiy
+
+
+
+
Коллекция НБУ
O'zbekiston milliy kutubxonasi to'plami
Ilmiy
+
+
+ +
Huquqiy va normativ bazalar
+
+
+
+
Patentlar
SSSR, O'zbekiston, Rossiya va MDH davlatlari patentlari bazasi
Huquqiy
+
+
+
+
ИПС Адилет
O'zbekiston qonunchilik bazasi hujjatlari
Huquqiy
+
+
+ +
Internet tekshiruv modullari
+
+
+
+
Internet PLUS moduli
Internet bo'ylab kengaytirilgan chuqur skanerlash
Internet
+
+
+
+
Internet RU — parafraz
Rus internet segmentidagi qayta bayon qilingan qarzlar
InternetAI tahlil
+
+
+
+
Internet EN — parafraz
Ingliz internet segmentidagi qayta bayon qilingan qarzlar
InternetAI tahlil
+
+
+
+
Internet RU — tarjima
Rus internet segmentidagi tarjima qilingan qarzlar
InternetAI tahlil
+
+
+
+
Internet EN — tarjima
Ingliz internet segmentidagi tarjima qilingan qarzlar
InternetAI tahlil
+
+
+
+
СМИ России и СНГ
Rossiya va MDH ommaviy axborot vositalari maqolalari
OAV
+
+
+
+
Собственная коллекция компании
Antiplag.uz ichki hujjatlar to'plami
Ichki baza
+
+
+ +
Yangi bepul internet manbalari
+
+
+
+
consultant.ru
Rossiya qonunchiligining elektron bazasi
BepulHuquqiy
+
+
+
+
kremlin.ru
Rossiya prezidenti farmonlari va rasmiy qonunlar
BepulHuquqiy
+
+
+
+
pravo.gov.ru
Rossiya rasmiy huquqiy hujjatlar nashriyoti portali
BepulHuquqiy
+
+
+
+
docs.cntd.ru
Texnik normalar va standartlar hujjatlari bazasi
BepulStandartlar
+
+
+
+
rumc.mininuniver.ru
Minin universiteti adaptiv ta'lim dasturlari resursi
BepulTa'lim
+
+
+
+
moodle.kstu.ru
KSTU universiteti Moodle platformasi o'quv resurslari
BepulTa'lim
+
+
+
+
freereferats.ru
Dissertatsiya avtoreferatlari ochiq to'plami (PDF)
BepulIlmiy
+
+
+
+
ktzszmoik.gov.by
Belarus nogironlarni ijtimoiy himoya qilish davlat portali
BepulInternet
+
+
+
+
bizlog.ru
Iqtisodiy-boshqaruv terminologiyasi va izohli lug'at
BepulInternet
+
+
+
+
disabilityartsinternational.org
Nogironlik va madaniyat bo'yicha xalqaro resurs
BepulXalqaro
+
+
+ +
Yordamchi modullar
+
+
+
+
Shablon iboralar
Standart kirish so'zlari, universitet nomlari va klişe iboralarni aniqlash
Avtomatik
+
+
+
+
Iqtibos keltirish moduli
Hujjatda to'g'ri rasmiylashtirilgan iqtiboslarni avtomatik aniqlash
Avtomatik
+
+
+ +
diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..570f0d9 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,84 @@ + + + + https://anti-plagiat.uz/uz + 2026-04-06 + daily + 1.0 + + + + + + https://anti-plagiat.uz/uz/plagat + 2026-04-06 + weekly + 0.8 + + + + + + https://anti-plagiat.uz/uz/cabinet + 2026-04-06 + weekly + 0.7 + + + + + + https://anti-plagiat.uz/ru + 2026-04-06 + daily + 1.0 + + + + + + https://anti-plagiat.uz/ru/plagat + 2026-04-06 + weekly + 0.8 + + + + + + https://anti-plagiat.uz/ru/cabinet + 2026-04-06 + weekly + 0.7 + + + + + + https://anti-plagiat.uz/en + 2026-04-06 + daily + 1.0 + + + + + + https://anti-plagiat.uz/en/plagat + 2026-04-06 + weekly + 0.8 + + + + + + https://anti-plagiat.uz/en/cabinet + 2026-04-06 + weekly + 0.7 + + + + + diff --git a/src/app/[locale]/cabinet/page.tsx b/src/app/[locale]/cabinet/page.tsx new file mode 100644 index 0000000..9e6af1b --- /dev/null +++ b/src/app/[locale]/cabinet/page.tsx @@ -0,0 +1,5 @@ +import { CabinetLayout } from '@/widgets/cabinet/ui'; + +export default function CabinetPage() { + return ; +} diff --git a/src/app/[locale]/plagat/page.tsx b/src/app/[locale]/plagiat/page.tsx similarity index 67% rename from src/app/[locale]/plagat/page.tsx rename to src/app/[locale]/plagiat/page.tsx index 130c2f5..f4920e8 100644 --- a/src/app/[locale]/plagat/page.tsx +++ b/src/app/[locale]/plagiat/page.tsx @@ -1,5 +1,5 @@ -import { PlagiarismCheckForm } from '@/widgets/fileUpload/ui/Plagiraismcheckform'; import { HistoryPage } from '@/widgets/history'; +import { PlagiarismCheckForm } from '@/widgets/plagiatCheck/ui/Plagiraismcheckform'; export default function Page() { return ( diff --git a/src/app/[locale]/si/[id]/page.tsx b/src/app/[locale]/si/[id]/page.tsx new file mode 100644 index 0000000..7ec954b --- /dev/null +++ b/src/app/[locale]/si/[id]/page.tsx @@ -0,0 +1,10 @@ +import SiDetailPage from '@/widgets/detail/SiDetailPage'; + +interface Props { + params: Promise<{ id: string }>; +} + +export default async function SiDetail({ params }: Props) { + const { id } = await params; + return ; +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index ec12d90..e20cd3b 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,10 +1,13 @@ import { MetadataRoute } from 'next'; -const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://antiplagiat.uz'; +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://anti-plagiat.uz'; const LOCALES = ['uz', 'ru', 'en'] as const; -// Add your static page slugs here -const STATIC_ROUTES = ['', '/about', '/history', '/contact']; +const STATIC_ROUTES = [ + { path: '', changeFreq: 'daily' as const, priority: 1.0 }, + { path: '/plagiat', changeFreq: 'weekly' as const, priority: 0.8 }, + { path: '/cabinet', changeFreq: 'weekly' as const, priority: 0.7 }, +]; export default function sitemap(): MetadataRoute.Sitemap { const entries: MetadataRoute.Sitemap = []; @@ -12,13 +15,13 @@ export default function sitemap(): MetadataRoute.Sitemap { for (const locale of LOCALES) { for (const route of STATIC_ROUTES) { entries.push({ - url: `${SITE_URL}/${locale}${route}`, + url: `${SITE_URL}/${locale}${route.path}`, lastModified: new Date(), - changeFrequency: route === '' ? 'daily' : 'weekly', - priority: route === '' ? 1.0 : 0.8, + changeFrequency: route.changeFreq, + priority: route.priority, alternates: { languages: Object.fromEntries( - LOCALES.map((l) => [l, `${SITE_URL}/${l}${route}`]), + LOCALES.map((l) => [l, `${SITE_URL}/${l}${route.path}`]), ), }, }); diff --git a/src/features/auth/login/lib/useLoginForm.ts b/src/features/auth/login/lib/useLoginForm.ts index a141fe6..0f30c53 100644 --- a/src/features/auth/login/lib/useLoginForm.ts +++ b/src/features/auth/login/lib/useLoginForm.ts @@ -52,13 +52,11 @@ export function useLoginForm() { console.log('Login successful:', data); toggleLoginModal(); toast.success('Kirish muvaffaqiyatli!'); - route.push('/plagat'); + route.push('/plagiat'); }, onError: (err) => { console.log('Login failed:', err); setError(err instanceof Error ? err.message : 'Unknown error'); - // toggleLoginModal(); - toast.error(err instanceof Error ? err.message : 'Unknown error'); }, }); @@ -74,7 +72,6 @@ export function useLoginForm() { } loginReqest.mutate({ phone: `998${phone}`, password: password }); - sessionStorage.setItem('prev_page', 'login'); }; return { diff --git a/src/features/auth/login/ui/form.tsx b/src/features/auth/login/ui/form.tsx index 9b0b437..e06be18 100644 --- a/src/features/auth/login/ui/form.tsx +++ b/src/features/auth/login/ui/form.tsx @@ -122,7 +122,6 @@ export function LoginForm() { onChange={(e) => setPassword(e.target.value)} require={true} type="password" - maxLength={8} minLength={8} /> diff --git a/src/features/auth/register/lib/useRegisterForm.ts b/src/features/auth/register/lib/useRegisterForm.ts index d969094..57fa690 100644 --- a/src/features/auth/register/lib/useRegisterForm.ts +++ b/src/features/auth/register/lib/useRegisterForm.ts @@ -53,12 +53,10 @@ export function useRegisterForm() { toggleRegisterModal(); setSuccess(true); toast.success("Ro'yxatdan o'tish muvaffaqiyatli!"); - route.push('/plagat'); + route.push('/plagiat'); }, onError: (err) => { - // toggleLoginModal(); console.log('Register failed:', err); - toast.error(err instanceof Error ? err.message : 'Unknown error'); }, }); diff --git a/src/widgets/paymentModal/lib/types.ts b/src/features/modals/paymentModal/lib/types.ts similarity index 57% rename from src/widgets/paymentModal/lib/types.ts rename to src/features/modals/paymentModal/lib/types.ts index f93a154..b5e5096 100644 --- a/src/widgets/paymentModal/lib/types.ts +++ b/src/features/modals/paymentModal/lib/types.ts @@ -1,16 +1,5 @@ // ─── Domain Types ────────────────────────────────────────────────────────────── -export interface ServicePricing { - serviceFee: number; - certificateFee: number; - currency: string; -} - -export interface OrderSummary { - hasCertificate: boolean; - pricing: ServicePricing; -} - export interface PaymePaymentRequest { amount: number; // in tiyin (1 UZS = 100 tiyin) orderId: string; @@ -18,31 +7,21 @@ export interface PaymePaymentRequest { returnUrl: string; } -export interface PaymePaymentResponse { - redirectUrl: string; - transactionId: string; -} - export type PaymentStatus = 'idle' | 'loading' | 'success' | 'error'; // ─── Component Props ─────────────────────────────────────────────────────────── +export interface PriceCalculate { + service_fee: number; + discount?: number; + certificate?: number; + total_price: number; +} export interface PaymentModalProps { isOpen: boolean; onClose: () => void; - hasCertificate: boolean; + price: PriceCalculate; onConfirmPayment: () => void; isLoading: boolean; -} - -export interface PriceSummaryProps { - hasCertificate: boolean; - pricing: ServicePricing; -} - -export interface PaymeButtonProps { - amount: number; - orderId: string; - onSuccess?: (response: PaymePaymentResponse) => void; - onError?: (error: Error) => void; + hasSertificate: boolean; } diff --git a/src/features/modals/paymentModal/lib/utils.ts b/src/features/modals/paymentModal/lib/utils.ts new file mode 100644 index 0000000..1eb56d7 --- /dev/null +++ b/src/features/modals/paymentModal/lib/utils.ts @@ -0,0 +1,12 @@ +// ─── Pricing Utilities ───────────────────────────────────────────────────────── +export const formatPrice = (amount: number, currency: string): string => + `${amount.toLocaleString('uz-UZ')} ${currency}`; + +// ─── Payme API ───────────────────────────────────────────────────────────────── + +/** + * Redirects the user to the Payme checkout page. + */ +export const redirectToPayme = (redirectUrl: string): void => { + window.location.href = redirectUrl; +}; diff --git a/src/widgets/paymentModal/ui/Paymebutton.tsx b/src/features/modals/paymentModal/ui/Paymebutton.tsx similarity index 100% rename from src/widgets/paymentModal/ui/Paymebutton.tsx rename to src/features/modals/paymentModal/ui/Paymebutton.tsx diff --git a/src/widgets/paymentModal/ui/Paymentmodal.tsx b/src/features/modals/paymentModal/ui/Paymentmodal.tsx similarity index 80% rename from src/widgets/paymentModal/ui/Paymentmodal.tsx rename to src/features/modals/paymentModal/ui/Paymentmodal.tsx index ac01019..c3d710d 100644 --- a/src/widgets/paymentModal/ui/Paymentmodal.tsx +++ b/src/features/modals/paymentModal/ui/Paymentmodal.tsx @@ -1,7 +1,6 @@ 'use client'; import React, { useEffect, useRef } from 'react'; import { PaymentModalProps } from '../lib/types'; -import { getPricing } from '../lib/utils'; import { PriceSummary } from './Pricesummary'; import { PaymeButton } from './Paymebutton'; import { useTranslations } from 'next-intl'; @@ -35,38 +34,6 @@ const CloseButton: React.FC<{ onClick: () => void }> = ({ onClick }) => ( ); -// ─── Error Banner ────────────────────────────────────────────────────────────── - -// const ErrorBanner: React.FC<{ message: string; onDismiss: () => void }> = ({ -// message, -// onDismiss, -// }) => ( -//
-// -// -// -//

{message}

-// -//
-// ); - // ─── Security Badge ──────────────────────────────────────────────────────────── const SecurityBadge: React.FC<{ securityText: string }> = ({ @@ -85,12 +52,12 @@ const SecurityBadge: React.FC<{ securityText: string }> = ({ export const PaymentModal: React.FC = ({ isOpen, onClose, - hasCertificate, + price, onConfirmPayment, isLoading, + hasSertificate, }) => { const dialogRef = useRef(null); - const pricing = getPricing(); const status = isLoading ? 'loading' : 'idle'; const t = useTranslations('Payment'); @@ -174,11 +141,11 @@ export const PaymentModal: React.FC = ({

{t('orderSummary')}

- + {/* Certificate badge */} - {hasCertificate && ( + {hasSertificate && (
= ({ // ─── Price Summary ───────────────────────────────────────────────────────────── -export const PriceSummary: React.FC = ({ - hasCertificate, - pricing, +export const PriceSummary = ({ + priceCalculate, +}: { + priceCalculate: PriceCalculate; }) => { - console.log(hasCertificate); - const total = 41200; const t = useTranslations('Payment'); return (
- {/* {hasCertificate && ( + {priceCalculate.discount ? ( - )} */} + ) : ( + '' + )} + + {priceCalculate.certificate ? ( + + ) : ( + '' + )}
diff --git a/src/features/modals/sertificateModal/modalField.tsx b/src/features/modals/sertificateModal/modalField.tsx new file mode 100644 index 0000000..3f2305e --- /dev/null +++ b/src/features/modals/sertificateModal/modalField.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { type ReactNode } from 'react'; + +/* ── Field wrapper ─────────────────────────────────────────── */ +interface FieldProps { + htmlFor: string; + icon: ReactNode; + label: string; + children: ReactNode; +} + +export function Field({ htmlFor, icon, label, children }: FieldProps) { + return ( +
+ + {children} +
+ ); +} + +/* ── Shared input class ────────────────────────────────────── */ +export const inputCls = ` + w-full px-3.5 py-2.5 text-[14px] text-slate-800 + bg-slate-50 border border-slate-200 rounded-xl + placeholder:text-slate-400 + focus:outline-none focus:ring-2 focus:ring-emerald-400/40 focus:border-emerald-400 + hover:border-slate-300 + transition-all duration-150 + disabled:opacity-60 disabled:cursor-not-allowed +`.trim(); diff --git a/src/features/modals/sertificateModal/sertificateModal.tsx b/src/features/modals/sertificateModal/sertificateModal.tsx new file mode 100644 index 0000000..8d888f8 --- /dev/null +++ b/src/features/modals/sertificateModal/sertificateModal.tsx @@ -0,0 +1,198 @@ +'use client'; + +import { + X, + Award, + User, + FileText, + BookOpen, + Loader2, + CheckCircle2, +} from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { useCertificateModal } from './useSertificateModal'; +import { Field, inputCls } from './modalField'; +import { SertificateModalProps } from './types'; +import DocumentsTypes from '@/widgets/plagiatCheck/ui/documentsType'; + +export default function SertificateModal({ + document_id, + open, + setOpen, +}: SertificateModalProps) { + const t = useTranslations('CertificateModal'); + const { + form, + updateField, + loading, + success, + visible, + isFormValid, + inputRef, + handleSubmit, + handleKeyDown, + handleBackdropClick, + } = useCertificateModal({ document_id, open, setOpen }); + + if (!visible) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal panel */} +
+ {/* Top accent bar */} +
+ + {/* Header */} +
+
+
+ +
+

+ {t('title')} +

+
+ +
+ + {/* Divider */} +
+ + {/* Body */} +
+ {/* Full name */} + + } + label={t('authorName')} + > + updateField('fullname', e.target.value)} + disabled={loading || success} + placeholder={t('namePlaceholder')} + className={inputCls} + /> + + + {/* Document theme */} + + } + label={t('documentTheme')} + > + updateField('document_theme', e.target.value)} + disabled={loading || success} + placeholder={t('themePlaceholder')} + className={inputCls} + /> + + + {/* Document type */} + updateField('type', val)} + disabled={loading || success} + /> + + {/* Document ID (read-only) */} +
+ +
+ + {t('documentId')} + + + #{document_id} + +
+
+
+ + {/* Footer */} +
+ +
+
+
+ ); +} diff --git a/src/features/modals/sertificateModal/sertifikat.tsx b/src/features/modals/sertificateModal/sertifikat.tsx new file mode 100644 index 0000000..0f3062a --- /dev/null +++ b/src/features/modals/sertificateModal/sertifikat.tsx @@ -0,0 +1,56 @@ +'use client'; +import { useTranslations } from 'next-intl'; +import { FileDown, Loader2 } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; +import SertificateModal from './sertificateModal'; + +export default function Sertifikat({ document_id }: { document_id: number }) { + const t = useTranslations(); + const [loading, setLoading] = useState(false); + const [openModal, setOpenModal] = useState(false); + useEffect(() => { + setLoading(false); + console.log(loading); + }, []); + + return ( + <> + + { + setOpenModal(false); + }} + /> + + ); +} diff --git a/src/features/modals/sertificateModal/types.ts b/src/features/modals/sertificateModal/types.ts new file mode 100644 index 0000000..0238e90 --- /dev/null +++ b/src/features/modals/sertificateModal/types.ts @@ -0,0 +1,12 @@ +export interface CertificateFormData { + fullname: string; + document_theme: string; + type: number; + document_id: number; +} + +export interface SertificateModalProps { + document_id: number; + open: boolean; + setOpen: () => void; +} diff --git a/src/features/modals/sertificateModal/useSertificateModal.ts b/src/features/modals/sertificateModal/useSertificateModal.ts new file mode 100644 index 0000000..4d9cc3b --- /dev/null +++ b/src/features/modals/sertificateModal/useSertificateModal.ts @@ -0,0 +1,140 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { apiRequest } from '@/shared/request/apiRequest'; +import { links } from '@/shared/request/links'; +import { CertificateFormData } from './types'; +import { SIPaymentResponse } from '../siModal/utils/useFileUpload'; + +interface UseCertificateModalProps { + document_id: number; + open: boolean; + setOpen: () => void; +} + +interface CertificatePayload { + full_name: string; + file_name: string; + document_type: number; +} + +export function useCertificateModal({ + document_id, + open, + setOpen, +}: UseCertificateModalProps) { + const [form, setForm] = useState({ + fullname: '', + document_theme: '', + type: 0, + document_id, + }); + const [success, setSuccess] = useState(false); + const [visible, setVisible] = useState(false); + const inputRef = useRef(null); + + const payment = useMutation({ + mutationFn: (id: number) => + apiRequest('POST', links.demo_pay(id)), + onSuccess: (res) => { + window.open(res?.data?.payment_link, '_self'); + }, + }); + + const certificateMutation = useMutation({ + mutationFn: (payload: CertificatePayload) => + apiRequest('POST', links.sertifikat(document_id), payload, { + responseType: 'arraybuffer', + }).then((res) => res.data), + onSuccess: (data: ArrayBuffer) => { + if (data) { + const blob = new Blob([data], { type: 'application/pdf' }); + const objectUrl = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = objectUrl; + a.download = `certificate-${document_id}.pdf`; + a.click(); + setTimeout(() => URL.revokeObjectURL(objectUrl), 1000); + } + setSuccess(true); + setTimeout(() => { + setOpen(); + setSuccess(false); + resetForm(); + }, 1500); + }, + onError: (error: AxiosError<{ code?: string }>) => { + if (error?.response?.data?.code === 'not_paid') { + payment.mutate(document_id); + } + }, + }); + + const resetForm = () => { + setForm({ + fullname: '', + document_theme: '', + type: 0, + document_id, + }); + }; + + useEffect(() => { + if (open) { + setVisible(true); + setSuccess(false); + setForm((prev) => ({ ...prev, document_id })); + setTimeout(() => inputRef.current?.focus(), 300); + + const data = localStorage.getItem('user'); + if (data) { + const user = JSON.parse(data); + setForm((prev) => ({ + ...prev, + fullname: `${user.name} ${user.surname}`, + })); + } + } else { + setTimeout(() => setVisible(false), 300); + } + }, [open, document_id]); + + const updateField = ( + field: K, + value: CertificateFormData[K], + ) => setForm((prev) => ({ ...prev, [field]: value })); + + const isFormValid = + !!form.fullname.trim() && !!form.document_theme.trim() && !!form.type; + + const handleSubmit = () => { + if (!isFormValid || certificateMutation.isPending) return; + certificateMutation.mutate({ + full_name: form.fullname, + file_name: form.document_theme, + document_type: Number(form.type), + }); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') setOpen(); + if (e.key === 'Enter') handleSubmit(); + }; + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) setOpen(); + }; + + return { + form, + updateField, + loading: certificateMutation.isPending, + success, + visible, + isFormValid, + inputRef, + handleSubmit, + handleKeyDown, + handleBackdropClick, + }; +} diff --git a/src/features/modals/siModal/index.ts b/src/features/modals/siModal/index.ts new file mode 100644 index 0000000..38c58e0 --- /dev/null +++ b/src/features/modals/siModal/index.ts @@ -0,0 +1,14 @@ +export { FileUploadModal } from './ui/fileUploadModal'; +export { useFileUpload } from './utils/useFileUpload'; +export { + countWordsFromFile, + SUPPORTED_EXTENSIONS, + SUPPORTED_MIME_TYPES, +} from './utils/wordCount'; +export { formatPrice, DEFAULT_PRICING } from './utils/pricing'; +export type { + FileUploadModalProps, + UploadedFile, + PricingConfig, + WordCountResult, +} from './utils/tyeps'; diff --git a/src/features/modals/siModal/page.tsx b/src/features/modals/siModal/page.tsx new file mode 100644 index 0000000..9e8b8af --- /dev/null +++ b/src/features/modals/siModal/page.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { useState } from 'react'; +import { ArrowRight, BrainCircuit, Plus } from 'lucide-react'; +import { FileUploadModal } from './ui/fileUploadModal'; +import { useTranslations } from 'next-intl'; + +export function SiCTACard() { + const [isOpen, setIsOpen] = useState(false); + const t = useTranslations('Cabinet'); + + return ( + <> + + setIsOpen(false)} /> + + ); +} + +export function SiButton() { + const [isOpen, setIsOpen] = useState(false); + const t = useTranslations('Cabinet'); + return ( + <> + + setIsOpen(false)} /> + + ); +} diff --git a/src/features/modals/siModal/ui/dropZone.tsx b/src/features/modals/siModal/ui/dropZone.tsx new file mode 100644 index 0000000..d009834 --- /dev/null +++ b/src/features/modals/siModal/ui/dropZone.tsx @@ -0,0 +1,67 @@ +'use client'; + +import { Upload } from 'lucide-react'; +import { SUPPORTED_EXTENSIONS } from '../utils/wordCount'; + +interface DropZoneProps { + isDragging: boolean; + onDrop: (e: React.DragEvent) => void; + onDragOver: (e: React.DragEvent) => void; + onDragLeave: () => void; + onClick: () => void; +} + +export function DropZone({ + isDragging, + onDrop, + onDragOver, + onDragLeave, + onClick, +}: DropZoneProps) { + return ( +
e.key === 'Enter' && onClick()} + className={` + relative flex flex-col items-center justify-center gap-3 + rounded-xl border-2 border-dashed px-6 py-2 + cursor-pointer select-none transition-all duration-200 outline-none + focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-2 + ${ + isDragging + ? 'border-blue-400 bg-blue-50 scale-[1.01]' + : 'border-slate-200 bg-slate-50 hover:border-blue-300 hover:bg-blue-50/50' + } + `} + > +
+ +
+ +
+

+ {isDragging ? 'Drop file here' : 'Drag & drop or click to browse'} +

+

+ Supported: {SUPPORTED_EXTENSIONS.join(', ')} · Max 50 MB +

+
+
+ ); +} diff --git a/src/features/modals/siModal/ui/fileUploadModal.tsx b/src/features/modals/siModal/ui/fileUploadModal.tsx new file mode 100644 index 0000000..467fd5e --- /dev/null +++ b/src/features/modals/siModal/ui/fileUploadModal.tsx @@ -0,0 +1,190 @@ +'use client'; + +import { useEffect } from 'react'; +import { X } from 'lucide-react'; +import { DEFAULT_PRICING, formatPrice } from '../utils/pricing'; +import { FileUploadModalProps } 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'; + +export function FileUploadModal({ + isOpen, + onClose, + pricing = DEFAULT_PRICING, +}: FileUploadModalProps) { + const { + documentName, + setDocumentName, + uploadedFile, + isDragging, + isProcessing, + error, + fileInputRef, + handleFileSelect, + handleDrop, + handleDragOver, + handleDragLeave, + handleRemoveFile, + openFilePicker, + canSubmit, + handleSubmit, + } = useFileUpload(); + + // Close on Escape key + useEffect(() => { + if (!isOpen) return; + const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose(); + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [isOpen, onClose]); + + // Lock body scroll while open + useEffect(() => { + document.body.style.overflow = isOpen ? 'hidden' : ''; + return () => { + document.body.style.overflow = ''; + }; + }, [isOpen]); + + if (!isOpen) return null; + + const wordCount = uploadedFile?.word_count ?? 0; + const totalPrice = uploadedFile?.total_price ?? 0; + + return ( + // Backdrop +
e.target === e.currentTarget && onClose()} + role="dialog" + aria-modal="true" + aria-labelledby="modal-title" + > + {/* Panel */} +
+ {/* Header */} +
+ + +
+ + {/* Document name */} +
+ + 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 + " + /> +
+ + {/* Hidden file input */} + { + const file = e.target.files?.[0]; + if (file) handleFileSelect(file); + }} + /> + + {/* Drop zone OR file chip */} + {uploadedFile ? ( +
+ +
+ ) : ( + + )} + + {/* Error */} + {error && } + + {/* Pricing info — shown once word count is ready */} + {uploadedFile?.status === 'done' && wordCount > 0 && ( + + )} + + {/* Footer */} +
+ +
+
+
+ ); +} diff --git a/src/features/modals/siModal/ui/modalParts.tsx b/src/features/modals/siModal/ui/modalParts.tsx new file mode 100644 index 0000000..05836eb --- /dev/null +++ b/src/features/modals/siModal/ui/modalParts.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { X, FileText, Loader2, AlertCircle } from 'lucide-react'; +import { UploadedFile } from '../utils/tyeps'; + +// ── Spinner ───────────────────────────────────────────────────────────────── +export function Spinner({ className = '' }: { className?: string }) { + return ( + + ); +} + +// ── File chip shown after upload ───────────────────────────────────────────── +interface FileChipProps { + file: UploadedFile; + onRemove: () => void; +} + +export function FileChip({ file, onRemove }: FileChipProps) { + const isLoading = file.status === 'uploading'; + + return ( +
+
+ +
+

+ {file.name} +

+

+ {file.sizeKB} KB +

+
+
+ +
+ {isLoading ? ( + + ) : ( + + {file.status === 'error' ? 'Error' : 'Upload complete'} + + )} + +
+
+ ); +} + +// ── Pricing info block ─────────────────────────────────────────────────────── +interface PricingInfoProps { + wordCount: number; + minimumPrice: string; + totalPrice: string; +} + +export function PricingInfo({ + wordCount, + minimumPrice, + totalPrice, +}: PricingInfoProps) { + return ( +
+

+ Document check price is determined by the number of words in the + document. +

+

+ Minimum payment for one document: {minimumPrice} +

+

+ Document check price: {wordCount.toLocaleString()} words for{' '} + {totalPrice} +

+
+ ); +} + +// ── Error banner ───────────────────────────────────────────────────────────── +export function ErrorBanner({ message }: { message: string }) { + return ( +
+ +

{message}

+
+ ); +} diff --git a/src/features/modals/siModal/utils/pricing.ts b/src/features/modals/siModal/utils/pricing.ts new file mode 100644 index 0000000..db40e75 --- /dev/null +++ b/src/features/modals/siModal/utils/pricing.ts @@ -0,0 +1,12 @@ +import { PricingConfig } from './tyeps'; + +const DEFAULT_PRICING: PricingConfig = { + pricePerWord: 4, // 4 so'm per word + minimumPayment: 10000, // 10 000 so'm minimum +}; + +export function formatPrice(amount: number): string { + return new Intl.NumberFormat('uz-UZ').format(amount) + " so'm"; +} + +export { DEFAULT_PRICING }; diff --git a/src/features/modals/siModal/utils/tyeps.ts b/src/features/modals/siModal/utils/tyeps.ts new file mode 100644 index 0000000..c6a4eb8 --- /dev/null +++ b/src/features/modals/siModal/utils/tyeps.ts @@ -0,0 +1,24 @@ +export interface UploadedFile { + file: File; + name: string; + sizeKB: number; + word_count: number; + total_price: number; + status: 'uploading' | 'done' | 'error'; +} + +export interface PricingConfig { + pricePerWord: number; // in so'm + minimumPayment: number; // in so'm +} + +export interface FileUploadModalProps { + isOpen: boolean; + onClose: () => void; + pricing?: PricingConfig; +} + +export interface WordCountResult { + count: number; + error?: string; +} diff --git a/src/features/modals/siModal/utils/useFileUpload.ts b/src/features/modals/siModal/utils/useFileUpload.ts new file mode 100644 index 0000000..40dd445 --- /dev/null +++ b/src/features/modals/siModal/utils/useFileUpload.ts @@ -0,0 +1,225 @@ +'use client'; + +import { useState, useCallback, useRef } from 'react'; +import { UploadedFile } from './tyeps'; +import { SUPPORTED_EXTENSIONS } from './wordCount'; +import { useMutation } from '@tanstack/react-query'; +import { apiRequest } from '@/shared/request/apiRequest'; +import { links } from '@/shared/request/links'; +import { toast } from 'react-toastify'; + +// ── API response types ──────────────────────────────────────────────────────── + +interface WordCountApiResponse { + word_count: number; + total_price: number; +} + +interface CreateSIOrderResponse { + id: number; + order_id: number; +} + +export interface SIPaymentResponse { + payment_link: string; +} + +// ── Return type ─────────────────────────────────────────────────────────────── + +interface UseFileUploadReturn { + documentName: string; + setDocumentName: (name: string) => void; + uploadedFile: UploadedFile | null; + isDragging: boolean; + isProcessing: boolean; + error: string | null; + fileInputRef: React.RefObject; + handleFileSelect: (file: File) => void; + handleDrop: (e: React.DragEvent) => void; + handleDragOver: (e: React.DragEvent) => void; + handleDragLeave: () => void; + handleRemoveFile: () => void; + openFilePicker: () => void; + handleSubmit: () => void; + canSubmit: boolean; +} + +// ── Hook ───────────────────────────────────────────────────────────────────── + +export function useFileUpload(): UseFileUploadReturn { + const [documentName, setDocumentName] = useState(''); + const [uploadedFile, setUploadedFile] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + + // ── Step 3: Payment ──────────────────────────────────────────────────────── + const siPayment = useMutation({ + mutationKey: ['si-payment'], + mutationFn: (order_id: number) => + apiRequest('POST', links.demo_pay(order_id)), + onSuccess: (res) => { + window.open(res.data.payment_link, '_self'); + }, + onError: (err) => { + toast.error( + err instanceof Error ? err.message : "To'lovda xatolik yuz berdi", + ); + }, + }); + + // ── Step 2: Create SI order ──────────────────────────────────────────────── + const createSIOrder = useMutation({ + mutationKey: ['si-create'], + mutationFn: (data: FormData) => + apiRequest('POST', links.si_create, data), + onSuccess: (res) => { + siPayment.mutate(res.data.order_id); + }, + onError: (err) => { + toast.error(err instanceof Error ? err.message : 'Xatolik yuz berdi'); + }, + }); + + // ── Step 1: Upload file & get word count + price ─────────────────────────── + const wordCountMutation = useMutation({ + mutationKey: ['si-word-count'], + mutationFn: (data: FormData) => + apiRequest('POST', links.wordCount, data), + onSuccess: (res) => { + setUploadedFile((prev) => + prev + ? { + ...prev, + status: 'done', + word_count: res.data.word_count ?? 0, + total_price: res.data.total_price ?? 0, + } + : prev, + ); + }, + onError: (err) => { + const message = err instanceof Error ? err.message : 'Fayl yuklanmadi'; + setError(message); + setUploadedFile((prev) => (prev ? { ...prev, status: 'error' } : prev)); + }, + }); + + // ── File validation ──────────────────────────────────────────────────────── + + const validateFile = (file: File): string | null => { + const ext = '.' + file.name.split('.').pop()?.toLowerCase(); + if (!SUPPORTED_EXTENSIONS.includes(ext)) { + return `Qo'llab-quvvatlanmaydigan fayl turi. Ruxsat etilgan: ${SUPPORTED_EXTENSIONS.join(', ')}`; + } + if (file.size > 50 * 1024 * 1024) { + return 'Fayl hajmi 50 MB dan oshmasligi kerak'; + } + return null; + }; + + // ── Step 1 trigger ───────────────────────────────────────────────────────── + + const handleFileSelect = useCallback( + (file: File) => { + setError(null); + + const validationError = validateFile(file); + if (validationError) { + setError(validationError); + return; + } + + // Optimistic: show file chip immediately while backend responds + setUploadedFile({ + file, + name: file.name, + sizeKB: Math.round(file.size / 1024), + word_count: 0, + total_price: 0, + status: 'uploading', + }); + + // Auto-fill document name if blank + setDocumentName((prev) => + prev.trim() === '' ? file.name.replace(/\.[^/.]+$/, '') : prev, + ); + + const fd = new FormData(); + fd.append('file', file); + wordCountMutation.mutate(fd); + }, + [wordCountMutation], + ); + + // ── Step 2 trigger (Check button) ───────────────────────────────────────── + + const handleSubmit = useCallback(() => { + if (!uploadedFile?.file || !documentName.trim()) return; + const fd = new FormData(); + fd.append('title', documentName.trim()); + fd.append('file', uploadedFile.file); + createSIOrder.mutate(fd); + }, [uploadedFile, documentName, createSIOrder]); + + // ── Drag & drop ──────────────────────────────────────────────────────────── + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files[0]; + if (file) handleFileSelect(file); + }, + [handleFileSelect], + ); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback(() => { + setIsDragging(false); + }, []); + + const handleRemoveFile = useCallback(() => { + setUploadedFile(null); + setError(null); + if (fileInputRef.current) fileInputRef.current.value = ''; + }, []); + + const openFilePicker = useCallback(() => { + fileInputRef.current?.click(); + }, []); + + // ── Derived state ────────────────────────────────────────────────────────── + + const isProcessing = + wordCountMutation.isPending || + createSIOrder.isPending || + siPayment.isPending; + + const canSubmit = + documentName.trim().length > 0 && + uploadedFile?.status === 'done' && + !isProcessing; + + return { + documentName, + setDocumentName, + uploadedFile, + isDragging, + isProcessing, + error, + fileInputRef, + handleFileSelect, + handleDrop, + handleDragOver, + handleDragLeave, + handleRemoveFile, + openFilePicker, + handleSubmit, + canSubmit, + }; +} diff --git a/src/features/modals/siModal/utils/wordCount.ts b/src/features/modals/siModal/utils/wordCount.ts new file mode 100644 index 0000000..6ab09a1 --- /dev/null +++ b/src/features/modals/siModal/utils/wordCount.ts @@ -0,0 +1,93 @@ +import { WordCountResult } from './tyeps'; + +// ── Plain text ────────────────────────────────────────────────────────────── +function countWordsFromText(text: string): number { + return text + .trim() + .split(/\s+/) + .filter((word) => word.length > 0).length; +} + +// ── .txt ──────────────────────────────────────────────────────────────────── +async function countFromTxt(file: File): Promise { + const text = await file.text(); + return { count: countWordsFromText(text) }; +} + +// ── .docx (reads raw XML, strips tags) ───────────────────────────────────── +async function countFromDocx(file: File): Promise { + try { + const { default: JSZip } = await import('jszip'); + const zip = await JSZip.loadAsync(await file.arrayBuffer()); + const xmlFile = zip.file('word/document.xml'); + + if (!xmlFile) { + return { count: 0, error: 'Could not read .docx content' }; + } + + const xml = await xmlFile.async('string'); + const text = xml + .replace(/<[^>]+>/g, ' ') // strip XML tags + .replace(/\s+/g, ' ') + .trim(); + + return { count: countWordsFromText(text) }; + } catch { + return { count: 0, error: 'Failed to parse .docx file' }; + } +} + +// ── .pdf (reads raw text layer; won't work for scanned PDFs) ─────────────── +async function countFromPdf(file: File): Promise { + try { + const buffer = await file.arrayBuffer(); + const bytes = new Uint8Array(buffer); + const raw = new TextDecoder('latin1').decode(bytes); + + // Extract text between BT/ET stream operators + const textMatches = raw.match(/BT[\s\S]*?ET/g) ?? []; + const words: string[] = []; + + for (const block of textMatches) { + // Match Tj / TJ operators + const strings = block.match(/\(([^)]*)\)\s*Tj|\[([^\]]*)\]\s*TJ/g) ?? []; + for (const s of strings) { + const inner = s.replace(/\(|\)\s*Tj|\[|\]\s*TJ/g, ''); + words.push(...inner.split(/\s+/).filter((w) => w.length > 0)); + } + } + + return { count: words.length }; + } catch { + return { count: 0, error: 'Failed to parse .pdf file' }; + } +} + +// ── Public API ────────────────────────────────────────────────────────────── +export async function countWordsFromFile(file: File): Promise { + const ext = file.name.split('.').pop()?.toLowerCase(); + + switch (ext) { + case 'txt': + return countFromTxt(file); + case 'docx': + return countFromDocx(file); + case 'pdf': + return countFromPdf(file); + default: + // Fallback: try reading as text + try { + const text = await file.text(); + return { count: countWordsFromText(text) }; + } catch { + return { count: 0, error: `Unsupported file type: .${ext}` }; + } + } +} + +export const SUPPORTED_EXTENSIONS = ['.txt', '.docx', '.pdf']; +export const SUPPORTED_MIME_TYPES = [ + 'text/plain', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/pdf', +]; diff --git a/src/shared/config/i18n/messages/en.json b/src/shared/config/i18n/messages/en.json index 74e5772..a9322da 100644 --- a/src/shared/config/i18n/messages/en.json +++ b/src/shared/config/i18n/messages/en.json @@ -4,7 +4,7 @@ "about": "Go to the about page" }, "Navbar": { - "logo": "Plagat", + "logo": "Plagiat", "aboutSite": "About Site", "contact": "Contact", "login": "Login", @@ -61,6 +61,8 @@ "date": "Date", "amount": "Amount", "result": "Result", + "fileName":"File name", + "count":"N_", "actions": "", "state": "Payment status", "emptyMessage": "No plagiarism checks found.", @@ -74,7 +76,8 @@ "resultClean": "Clean", "resultPlagiarismFound": "Plagiarism Found", "resultPending": "Pending", - "resultFailed": "Failed" + "resultFailed": "Failed", + "plagiatCheck": "Plagiarism Check" }, "DetailPage": { "id": "ID", @@ -124,7 +127,11 @@ "unknownError": "Unknown error", "words": "words", "aiProbabilityText": "Probability that the text was generated with AI has been detected", - "documentNumber": "Document subject" + "documentNumber": "Document subject", + "scoreAiContent": "Self-citation", + "scoreOriginality": "Originality", + "scorePlagiarism": "Plagiarism", + "scoreCitation": "Citation" }, "Hero": { "badge": "Academic Integrity Platform", @@ -229,7 +236,8 @@ "paymentMethod": "Payment Method", "security": "Secured by Payme · SSL encrypted", "serviceFee": "Service fee", - "certificateLabel": "Certificate", + "discountLabel": "Discount", + "sertificateLabel":"Certificate", "total": "Total", "paymentRequired": "Payment not completed", "connecting": "Connecting to Payme…", @@ -237,5 +245,122 @@ }, "unknownUser": "Username not found", "file": "File", - "upload": "Download certificate" + "upload": "Download certificate", + "Cabinet": { + "plagiatCheck": "Plagiarism Check", + "checkDesc": "Check your document for originality", + "submit": "Submit", + "siDetector": "AI Detector", + "siDesc": "Check text for AI content", + "home": "Home", + "plagiat": "Plagiarism", + "siNav": "AI Detector", + "payments": "Payment History", + "profile": "Profile", + "close": "Close", + "personalCabinet": "Personal Cabinet", + "plagiatChecks": "Plagiarism Checks", + "dashboard": "Dashboard", + "welcome": "Welcome, {userName} 👋", + "welcomeDesc": "Welcome to your personal cabinet", + "quickActions": "Quick Actions", + "totalChecks": "Total Checks", + "thisMonth": "This Month", + "paidAmount": "Amount Paid", + "noData": "No data found", + "checkModules": "Check Modules", + "checkModulesDesc": "All sources used for plagiarism detection", + "modulesCount": "{count} modules", + "totalModules": "Total Modules", + "freeInternetSources": "Free Internet Sources", + "aiAnalysisModules": "AI Analysis Modules", + "categories": "Categories", + "paymentsCount": "{count} payments", + "loading": "Loading...", + "noPayments": "No payment history", + "tableNum": "#", + "service": "Service", + "amount": "Amount", + "discount": "Discount", + "date": "Date", + "status": "Status", + "unknown": "Unknown", + "noSiChecks": "No AI checks yet", + "loadError": "Failed to load data", + "paid": "Paid", + "unpaid": "Unpaid", + "checksCount": "{count} checks", + "tableTitle": "Title", + "tableFile": "File", + "words": "Words", + "action": "Action", + "pay": "Pay", + "view": "View", + "profileDesc": "Manage your information", + "personalInfo": "Personal Information", + "changePassword": "Change Password", + "firstName": "First Name", + "lastName": "Last Name", + "phone": "Phone", + "email": "Email", + "newPassword": "New Password", + "saved": "Saved", + "saving": "Saving…", + "save": "Save", + "firstNameRequired": "First name is required", + "lastNameRequired": "Last name is required", + "phoneInvalid": "Phone number must be 9 digits", + "passwordTooShort": "Password must be at least 8 characters", + "discountThisMonth": "Discount this month", + "discountRemaining": "Discount expires after {remaining} documents", + "discountAllUsed": "All discounts for this month have been used", + "discountUsed": "{count} used" + }, + "SiDetail": { + "siCheck": "AI Check", + "basicInfo": "Basic Information", + "documentInfo": "Document Information", + "documentName": "Document Name", + "checker": "Checker", + "uploadedAt": "Document Upload Time", + "originalFileName": "Original File Name", + "wordCount": "Word Count", + "fileExt": "File Extension", + "fileSize": "File Size", + "amountCharged": "Amount Charged", + "downloadOriginal": "Download Original Document", + "siResultsTitle": "Document AI Detector Results", + "siResultsDesc": "This window displays the analysis results of the text uploaded by the user regarding the probability of being written with the help of artificial intelligence. The detector evaluates the stylistic, grammatical and semantic features of the text and shows in percentage how likely it was generated by artificial intelligence.", + "download": "Download", + "originalText": "Original Text", + "possibleAi": "Possible AI", + "aiContent": "Artificial Intelligence" + }, + "PlagiatResult": { + "plagiarismLevel": "Plagiarism Level", + "aiWritten": "AI Written", + "originality": "Originality", + "citation": "Citation", + "plagiat": "Plagiarism", + "aiGeneration": "AI Generation", + "original": "Original", + "checked": "Checked" + }, + "CertificateModal": { + "title": "Create Certificate", + "close": "Close", + "authorName": "Author's Full Name", + "namePlaceholder": "Enter your name...", + "documentTheme": "Document Theme", + "themePlaceholder": "Enter the topic...", + "documentId": "Document ID", + "creating": "Creating...", + "created": "Certificate Created!", + "create": "Create Certificate" + }, + "DocumentTypes": { + "label": "Document Type", + "loading": "Loading...", + "placeholder": "Select document type..." + } } diff --git a/src/shared/config/i18n/messages/ru.json b/src/shared/config/i18n/messages/ru.json index 08d56a1..efe75fb 100644 --- a/src/shared/config/i18n/messages/ru.json +++ b/src/shared/config/i18n/messages/ru.json @@ -4,7 +4,7 @@ "about": "Перейти на страницу о нас" }, "Navbar": { - "logo": "Plagat", + "logo": "Plagiat", "aboutSite": "О сайте", "contact": "Контакты", "login": "Войти", @@ -61,6 +61,7 @@ "date": "Дата", "amount": "Сумма", "result": "Результат", + "count":"H_", "actions": "", "state": "Статус оплаты", "emptyMessage": "Проверки на плагиат не найдены.", @@ -74,7 +75,8 @@ "resultClean": "Чисто", "resultPlagiarismFound": "Обнаружен плагиат", "resultPending": "В ожидании", - "resultFailed": "Не удалось" + "resultFailed": "Не удалось", + "plagiatCheck": "Проверка на плагиат" }, "DetailPage": { "id": "ID", @@ -124,7 +126,11 @@ "unknownError": "Неизвестная ошибка", "words": "слов", "aiProbabilityText": "Обнаружена вероятность того, что текст создан с помощью ИИ", - "documentNumber": "Тема документа" + "documentNumber": "Тема документа", + "scoreAiContent": "Самоцитирование", + "scoreOriginality": "Оригинальность", + "scorePlagiarism": "Плагиат", + "scoreCitation": "Цитирование" }, "Hero": { "badge": "Платформа академической честности", @@ -229,7 +235,8 @@ "paymentMethod": "Способ оплаты", "security": "Защищено Payme · SSL шифрование", "serviceFee": "Стоимость услуги", - "certificateLabel": "Сертификат", + "discountLabel": "Скидка", + "sertificateLabel":"Сертификат", "total": "Итого", "paymentRequired": "Оплата не произведена", "connecting": "Подключение к Payme…", @@ -237,5 +244,122 @@ }, "unknownUser": "Имя пользователя не найдено", "file": "Файл", - "upload": "Скачать сертификат" + "upload": "Скачать сертификат", + "Cabinet": { + "plagiatCheck": "Проверка на плагиат", + "checkDesc": "Проверьте ваш документ на оригинальность", + "submit": "Отправить", + "siDetector": "ИИ детектор", + "siDesc": "Проверьте текст на искусственный интеллект", + "home": "Главная", + "plagiat": "Плагиат", + "siNav": "ИИ детектор", + "payments": "История платежей", + "profile": "Профиль", + "close": "Закрыть", + "personalCabinet": "Личный кабинет", + "plagiatChecks": "Проверки на плагиат", + "dashboard": "Dashboard", + "welcome": "Добро пожаловать, {userName} 👋", + "welcomeDesc": "Добро пожаловать в ваш личный кабинет", + "quickActions": "Быстрые действия", + "totalChecks": "Всего проверок", + "thisMonth": "Этот месяц", + "paidAmount": "Оплаченная сумма", + "noData": "Данные не найдены", + "checkModules": "Модули проверки", + "checkModulesDesc": "Все источники, используемые для обнаружения плагиата", + "modulesCount": "{count} модулей", + "totalModules": "Всего модулей", + "freeInternetSources": "Бесплатные интернет-источники", + "aiAnalysisModules": "Модули AI анализа", + "categories": "Категория", + "paymentsCount": "{count} платежей", + "loading": "Загрузка...", + "noPayments": "История платежей отсутствует", + "tableNum": "#", + "service": "Услуга", + "amount": "Сумма", + "discount": "Скидка", + "date": "Дата", + "status": "Статус", + "unknown": "Неизвестно", + "noSiChecks": "Проверок ИИ пока нет", + "loadError": "Ошибка загрузки данных", + "paid": "Оплачено", + "unpaid": "Не оплачено", + "checksCount": "{count} проверок", + "tableTitle": "Заголовок", + "tableFile": "Файл", + "words": "Слов", + "action": "Действие", + "pay": "Оплатить", + "view": "Просмотр", + "profileDesc": "Управляйте своими данными", + "personalInfo": "Личные данные", + "changePassword": "Изменить пароль", + "firstName": "Имя", + "lastName": "Фамилия", + "phone": "Телефон", + "email": "Email", + "newPassword": "Новый пароль", + "saved": "Сохранено", + "saving": "Сохранение…", + "save": "Сохранить", + "firstNameRequired": "Имя обязательно", + "lastNameRequired": "Фамилия обязательна", + "phoneInvalid": "Номер телефона должен содержать 9 цифр", + "passwordTooShort": "Пароль должен содержать не менее 8 символов", + "discountThisMonth": "Скидка в этом месяце", + "discountRemaining": "Скидка истекает через {remaining} документов", + "discountAllUsed": "Все скидки этого месяца использованы", + "discountUsed": "{count} использовано" + }, + "SiDetail": { + "siCheck": "Проверка ИИ", + "basicInfo": "Основная информация", + "documentInfo": "Информация о документе", + "documentName": "Название документа", + "checker": "Проверяющий", + "uploadedAt": "Дата загрузки документа", + "originalFileName": "Оригинальное имя файла", + "wordCount": "Количество слов", + "fileExt": "Расширение файла", + "fileSize": "Размер файла", + "amountCharged": "Списанная сумма", + "downloadOriginal": "Скачать оригинальный документ", + "siResultsTitle": "Результаты ИИ детектора документа", + "siResultsDesc": "В этом окне отображены результаты анализа текста, загруженного пользователем, на вероятность написания с помощью искусственного интеллекта. Детектор оценивает стилистические, грамматические и семантические особенности текста и показывает в процентах, насколько вероятно, что он был сгенерирован искусственным интеллектом.", + "download": "Скачать", + "originalText": "Оригинальный текст", + "possibleAi": "Возможный ИИ", + "aiContent": "Искусственный интеллект" + }, + "PlagiatResult": { + "plagiarismLevel": "Уровень плагиата", + "aiWritten": "Написано ИИ", + "originality": "Оригинальность", + "citation": "Цитирование", + "plagiat": "Плагиат", + "aiGeneration": "Генерация ИИ", + "original": "Оригинал", + "checked": "Проверено" + }, + "CertificateModal": { + "title": "Создать сертификат", + "close": "Закрыть", + "authorName": "Полное имя автора", + "namePlaceholder": "Введите ваше имя...", + "documentTheme": "Тема документа", + "themePlaceholder": "Введите тему...", + "documentId": "ID документа", + "creating": "Создание...", + "created": "Сертификат создан!", + "create": "Создать сертификат" + }, + "DocumentTypes": { + "label": "Тип документа", + "loading": "Загрузка...", + "placeholder": "Выберите тип документа..." + } } diff --git a/src/shared/config/i18n/messages/uz.d.json.ts b/src/shared/config/i18n/messages/uz.d.json.ts index 1b6f044..cab81e5 100644 --- a/src/shared/config/i18n/messages/uz.d.json.ts +++ b/src/shared/config/i18n/messages/uz.d.json.ts @@ -7,7 +7,7 @@ declare const messages: { about: "Biz haqimizda sahifasiga o'ting"; }; Navbar: { - logo: 'Plagat'; + logo: 'Plagiat'; aboutSite: 'Sayt haqida'; contact: 'Aloqa'; login: 'Kirish'; @@ -61,6 +61,8 @@ declare const messages: { description: 'Siz tomonidan yuborilgan barcha plagiat tekshiruvlari'; sender: 'Yuboruvchi'; file: 'Fayl'; + fileName: 'Fayl nomi'; + count: 'N_'; date: 'Sana'; amount: 'Summa'; result: 'Natija'; @@ -78,6 +80,7 @@ declare const messages: { resultPlagiarismFound: 'Plagiat topildi'; resultPending: 'Kutilmoqda'; resultFailed: 'Muvaffaqiyatsiz'; + plagiatCheck: 'Plagiat tekshiruvi'; }; DetailPage: { id: 'ID'; @@ -128,6 +131,10 @@ declare const messages: { words: "so'z"; aiProbabilityText: 'Ai yordamida yaratilganlik ehtimoli aniqlandi'; documentNumber: 'Dokument mavzusi'; + scoreAiContent: "O'zidan iqtibos keltirish"; + scoreOriginality: 'Originallik'; + scorePlagiarism: 'Plagiat'; + scoreCitation: 'Iqtibos'; }; Hero: { badge: 'Akademik halollik platformasi'; @@ -232,7 +239,8 @@ declare const messages: { paymentMethod: "To'lov usuli"; security: 'Payme tomonidan himoyalangan · SSL shifrlash'; serviceFee: "Xizmat to'lovi"; - certificateLabel: 'Sertifikat'; + discountLabel: 'Chegirma'; + sertificateLabel: 'Sertifikat'; total: 'Jami'; paymentRequired: "To'lov qilinmagan"; connecting: 'Paymega ulanmoqda…'; @@ -241,5 +249,122 @@ declare const messages: { unknownUser: 'Foydalanuvchi topilmadi'; file: 'Fayl'; upload: 'Sertifikatni yuklab olish'; + Cabinet: { + plagiatCheck: 'Plagiat tekshiruvi'; + checkDesc: 'Hujjatingizni originallik uchun tekshiring'; + submit: 'Yuborish'; + siDetector: 'SI detektor'; + siDesc: "Matnni sun'iy intellekt uchun tekshiring"; + home: 'Bosh sahifa'; + plagiat: 'Plagiat'; + siNav: 'SI detektor'; + payments: "To'lovlar tarixi"; + profile: 'Profil'; + close: 'Yopish'; + personalCabinet: 'Shaxsiy kabinet'; + plagiatChecks: 'Plagiat tekshiruvlar'; + dashboard: 'Dashboard'; + welcome: 'Xush kelibsiz, {userName} 👋'; + welcomeDesc: 'Shaxsiy kabinetingizga xush kelibsiz'; + quickActions: 'Tezkor harakatlar'; + totalChecks: 'Jami tekshiruvlar'; + thisMonth: 'Bu oy'; + paidAmount: "To'langan summa"; + noData: "Ma'lumot topilmadi"; + checkModules: 'Tekshiruv modullari'; + checkModulesDesc: 'Plagiat aniqlashda foydalaniladigan barcha manbalar'; + modulesCount: '{count} ta modul'; + totalModules: 'Jami modullar'; + freeInternetSources: 'Bepul internet manbalari'; + aiAnalysisModules: 'AI tahlil modullari'; + categories: 'Kategoriya'; + paymentsCount: "{count} ta to'lov"; + loading: 'Yuklanmoqda...'; + noPayments: "To'lovlar tarixi mavjud emas"; + tableNum: '#'; + service: 'Xizmat'; + amount: 'Summa'; + discount: 'Chegirma'; + date: 'Sana'; + status: 'Holat'; + unknown: "Noma'lum"; + noSiChecks: "Hozircha SI tekshiruvlar yo'q"; + loadError: "Ma'lumotlarni yuklashda xatolik yuz berdi"; + paid: "To'langan"; + unpaid: "To'lanmagan"; + checksCount: '{count} ta tekshiruv'; + tableTitle: 'Sarlavha'; + tableFile: 'Fayl'; + words: "So'z"; + action: 'Amal'; + pay: "To'lash"; + view: "Ko'rish"; + profileDesc: "Ma'lumotlaringizni boshqaring"; + personalInfo: "Shaxsiy ma'lumotlar"; + changePassword: "Parol o'zgartirish"; + firstName: 'Ism'; + lastName: 'Familiya'; + phone: 'Telefon'; + email: 'Email'; + newPassword: 'Yangi parol'; + saved: 'Saqlandi'; + saving: 'Saqlanmoqda…'; + save: 'Saqlash'; + firstNameRequired: 'Ism kiritilishi shart'; + lastNameRequired: 'Familiya kiritilishi shart'; + phoneInvalid: "Telefon raqami 9 ta raqamdan iborat bo'lishi kerak"; + passwordTooShort: "Parol kamida 8 ta belgidan iborat bo'lishi kerak"; + discountThisMonth: 'Bu oyda chegirma'; + discountRemaining: '{remaining} ta hujjatdan keyin chegirma tugaydi'; + discountAllUsed: 'Bu oyda barcha chegirmalar ishlatildi'; + discountUsed: '{count} ta ishlatildi'; + }; + SiDetail: { + siCheck: 'SI tekshiruv'; + basicInfo: "Asosiy ma'lumotlar"; + documentInfo: "Hujjat haqida ma'lumotlar"; + documentName: 'Hujjat nomi'; + checker: 'Tekshiruvchi'; + uploadedAt: 'Hujjat yuklangan vaqti'; + originalFileName: 'Hujjat faylining original nomi'; + wordCount: "So'zlar soni"; + fileExt: 'Hujjat fayli kenggaytmasi'; + fileSize: "Hujjat fayli o'lchami"; + amountCharged: 'Yechilgan summa'; + downloadOriginal: 'Original hujjatni yuklab olish'; + siResultsTitle: 'Hujjatning SI detektori natijalari'; + siResultsDesc: "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."; + download: 'Yuklab olish'; + originalText: 'Original matn'; + possibleAi: "Ehtimoliy Sun'iy Intellekt"; + aiContent: "Sun'iy Intellekt"; + }; + PlagiatResult: { + plagiarismLevel: 'Plagiat darajasi'; + aiWritten: 'AI yozgan'; + originality: 'Originallik'; + citation: 'Iqtibos'; + plagiat: 'Plagiat'; + aiGeneration: 'AI generatsiya'; + original: 'Original'; + checked: 'Tekshirilgan'; + }; + CertificateModal: { + title: 'Sertifikat yaratish'; + close: 'Yopish'; + authorName: "Muallifning to'liq ismi"; + namePlaceholder: 'Ismingizni kiriting...'; + documentTheme: 'Hujjat mavzusi'; + themePlaceholder: 'Mavzuni kiriting...'; + documentId: 'Hujjat ID'; + creating: 'Yaratilmoqda...'; + created: 'Sertifikat yaratildi!'; + create: 'Sertifikat yaratish'; + }; + DocumentTypes: { + label: 'Hujjat turi'; + loading: 'Yuklanmoqda...'; + placeholder: 'Hujjat turini tanlang...'; + }; }; export default messages; diff --git a/src/shared/config/i18n/messages/uz.json b/src/shared/config/i18n/messages/uz.json index b9c9baa..f3c1d5a 100644 --- a/src/shared/config/i18n/messages/uz.json +++ b/src/shared/config/i18n/messages/uz.json @@ -4,7 +4,7 @@ "about": "Biz haqimizda sahifasiga o'ting" }, "Navbar": { - "logo": "Plagat", + "logo": "Plagiat", "aboutSite": "Sayt haqida", "contact": "Aloqa", "login": "Kirish", @@ -58,6 +58,8 @@ "description": "Siz tomonidan yuborilgan barcha plagiat tekshiruvlari", "sender": "Yuboruvchi", "file": "Fayl", + "fileName":"Fayl nomi", + "count":"N_", "date": "Sana", "amount": "Summa", "result": "Natija", @@ -74,7 +76,8 @@ "resultClean": "Toza", "resultPlagiarismFound": "Plagiat topildi", "resultPending": "Kutilmoqda", - "resultFailed": "Muvaffaqiyatsiz" + "resultFailed": "Muvaffaqiyatsiz", + "plagiatCheck": "Plagiat tekshiruvi" }, "DetailPage": { "id": "ID", @@ -124,7 +127,11 @@ "unknownError": "Noma'lum xato", "words": "so'z", "aiProbabilityText":"Ai yordamida yaratilganlik ehtimoli aniqlandi", - "documentNumber":"Dokument mavzusi" + "documentNumber":"Dokument mavzusi", + "scoreAiContent": "O'zidan iqtibos keltirish", + "scoreOriginality": "Originallik", + "scorePlagiarism": "Plagiat", + "scoreCitation": "Iqtibos" }, "Hero": { "badge": "Akademik halollik platformasi", @@ -229,7 +236,8 @@ "paymentMethod": "To'lov usuli", "security": "Payme tomonidan himoyalangan · SSL shifrlash", "serviceFee": "Xizmat to'lovi", - "certificateLabel": "Sertifikat", + "discountLabel": "Chegirma", + "sertificateLabel":"Sertifikat", "total": "Jami", "paymentRequired":"To'lov qilinmagan", "connecting": "Paymega ulanmoqda…", @@ -237,5 +245,122 @@ }, "unknownUser": "Foydalanuvchi topilmadi", "file": "Fayl", - "upload": "Sertifikatni yuklab olish" + "upload": "Sertifikatni yuklab olish", + "Cabinet": { + "plagiatCheck": "Plagiat tekshiruvi", + "checkDesc": "Hujjatingizni originallik uchun tekshiring", + "submit": "Yuborish", + "siDetector": "SI detektor", + "siDesc": "Matnni sun'iy intellekt uchun tekshiring", + "home": "Bosh sahifa", + "plagiat": "Plagiat", + "siNav": "SI detektor", + "payments": "To'lovlar tarixi", + "profile": "Profil", + "close": "Yopish", + "personalCabinet": "Shaxsiy kabinet", + "plagiatChecks": "Plagiat tekshiruvlar", + "dashboard": "Dashboard", + "welcome": "Xush kelibsiz, {userName} 👋", + "welcomeDesc": "Shaxsiy kabinetingizga xush kelibsiz", + "quickActions": "Tezkor harakatlar", + "totalChecks": "Jami tekshiruvlar", + "thisMonth": "Bu oy", + "paidAmount": "To'langan summa", + "noData": "Ma'lumot topilmadi", + "checkModules": "Tekshiruv modullari", + "checkModulesDesc": "Plagiat aniqlashda foydalaniladigan barcha manbalar", + "modulesCount": "{count} ta modul", + "totalModules": "Jami modullar", + "freeInternetSources": "Bepul internet manbalari", + "aiAnalysisModules": "AI tahlil modullari", + "categories": "Kategoriya", + "paymentsCount": "{count} ta to'lov", + "loading": "Yuklanmoqda...", + "noPayments": "To'lovlar tarixi mavjud emas", + "tableNum": "#", + "service": "Xizmat", + "amount": "Summa", + "discount": "Chegirma", + "date": "Sana", + "status": "Holat", + "unknown": "Noma'lum", + "noSiChecks": "Hozircha SI tekshiruvlar yo'q", + "loadError": "Ma'lumotlarni yuklashda xatolik yuz berdi", + "paid": "To'langan", + "unpaid": "To'lanmagan", + "checksCount": "{count} ta tekshiruv", + "tableTitle": "Sarlavha", + "tableFile": "Fayl", + "words": "So'z", + "action": "Amal", + "pay": "To'lash", + "view": "Ko'rish", + "profileDesc": "Ma'lumotlaringizni boshqaring", + "personalInfo": "Shaxsiy ma'lumotlar", + "changePassword": "Parol o'zgartirish", + "firstName": "Ism", + "lastName": "Familiya", + "phone": "Telefon", + "email": "Email", + "newPassword": "Yangi parol", + "saved": "Saqlandi", + "saving": "Saqlanmoqda…", + "save": "Saqlash", + "firstNameRequired": "Ism kiritilishi shart", + "lastNameRequired": "Familiya kiritilishi shart", + "phoneInvalid": "Telefon raqami 9 ta raqamdan iborat bo'lishi kerak", + "passwordTooShort": "Parol kamida 8 ta belgidan iborat bo'lishi kerak", + "discountThisMonth": "Bu oyda chegirma", + "discountRemaining": "{remaining} ta hujjatdan keyin chegirma tugaydi", + "discountAllUsed": "Bu oyda barcha chegirmalar ishlatildi", + "discountUsed": "{count} ta ishlatildi" + }, + "SiDetail": { + "siCheck": "SI tekshiruv", + "basicInfo": "Asosiy ma'lumotlar", + "documentInfo": "Hujjat haqida ma'lumotlar", + "documentName": "Hujjat nomi", + "checker": "Tekshiruvchi", + "uploadedAt": "Hujjat yuklangan vaqti", + "originalFileName": "Hujjat faylining original nomi", + "wordCount": "So'zlar soni", + "fileExt": "Hujjat fayli kenggaytmasi", + "fileSize": "Hujjat fayli o'lchami", + "amountCharged": "Yechilgan summa", + "downloadOriginal": "Original hujjatni yuklab olish", + "siResultsTitle": "Hujjatning SI detektori natijalari", + "siResultsDesc": "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.", + "download": "Yuklab olish", + "originalText": "Original matn", + "possibleAi": "Ehtimoliy Sun'iy Intellekt", + "aiContent": "Sun'iy Intellekt" + }, + "PlagiatResult": { + "plagiarismLevel": "Plagiat darajasi", + "aiWritten": "AI yozgan", + "originality": "Originallik", + "citation": "Iqtibos", + "plagiat": "Plagiat", + "aiGeneration": "AI generatsiya", + "original": "Original", + "checked": "Tekshirilgan" + }, + "CertificateModal": { + "title": "Sertifikat yaratish", + "close": "Yopish", + "authorName": "Muallifning to'liq ismi", + "namePlaceholder": "Ismingizni kiriting...", + "documentTheme": "Hujjat mavzusi", + "themePlaceholder": "Mavzuni kiriting...", + "documentId": "Hujjat ID", + "creating": "Yaratilmoqda...", + "created": "Sertifikat yaratildi!", + "create": "Sertifikat yaratish" + }, + "DocumentTypes": { + "label": "Hujjat turi", + "loading": "Yuklanmoqda...", + "placeholder": "Hujjat turini tanlang..." + } } diff --git a/src/shared/lib/metadata.ts b/src/shared/lib/metadata.ts index 5890ff9..09b3d27 100644 --- a/src/shared/lib/metadata.ts +++ b/src/shared/lib/metadata.ts @@ -3,6 +3,9 @@ import { SEO_DATA, type SupportedLocale } from '../config/seo.config'; // ─── Site-wide constants ─────────────────────────────────────────────────────── +export const SERTIFICATE_PRICE = 20600; +export const PLAGIAT_SERVICE_FEE = 20600; + const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://antiplagiat.uz'; const OG_IMAGE_URL = `${SITE_URL}/og-image.png`; // 1200×630 px recommended const TWITTER_HANDLE = '@antiplagiatuz'; // update or remove if unused diff --git a/src/shared/request/apiRequest.ts b/src/shared/request/apiRequest.ts index 2e55df5..8c06aa9 100644 --- a/src/shared/request/apiRequest.ts +++ b/src/shared/request/apiRequest.ts @@ -4,8 +4,44 @@ import axios, { AxiosError, InternalAxiosRequestConfig, } from 'axios'; +import { toast } from 'react-toastify'; import { getRouteLang } from './getLanguage'; +// ─── Error message extractor ─────────────────────────────────────────────────── + +function extractErrorMessage(error: AxiosError): string { + const data = error.response?.data as Record | undefined; + + if (!data) { + if (error.code === 'ECONNABORTED') + return 'Request timed out. Please try again.'; + if (!navigator.onLine) return 'No internet connection.'; + return error.message || 'An unexpected error occurred.'; + } + + // Simple string fields: { message, detail, error } + if (typeof data.message === 'string' && data.message) return data.message; + if (typeof data.detail === 'string' && data.detail) return data.detail; + if (typeof data.error === 'string' && data.error) return data.error; + + // Wrapped: { errors: { field: ["msg"] } } + if (data.errors && typeof data.errors === 'object') { + const first = Object.values(data.errors as Record)[0]; + if (Array.isArray(first) && first.length > 0) return String(first[0]); + if (typeof first === 'string') return first; + } + + // DRF field-level errors at top level: { phone: ["msg"], name: ["msg"] } + for (const val of Object.values(data)) { + if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'string') { + return val[0]; + } + if (typeof val === 'string' && val) return val; + } + + return 'An unexpected error occurred.'; +} + // ─── Constants ───────────────────────────────────────────────────────────────── // const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL; @@ -107,9 +143,14 @@ api.interceptors.response.use( }; const status = error.response?.status; + const requestUrl = originalRequest.url ?? ''; + const isAuthEndpoint = + requestUrl.includes('/users/login/') || + requestUrl.includes('/users/register/'); - // Only attempt refresh on 401 and only once per request - if (status !== 401 || originalRequest._retry) { + // For auth endpoints, 401 means wrong credentials — show error, don't refresh + if (isAuthEndpoint || status !== 401 || originalRequest._retry) { + toast.error(extractErrorMessage(error)); return Promise.reject(error); } @@ -178,7 +219,7 @@ api.interceptors.response.use( // ─── Public request function ─────────────────────────────────────────────────── export const apiRequest = async ( - method: 'GET' | 'POST' | 'PUT' | 'DELETE', + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', url: string, data?: unknown, config?: Omit, @@ -195,5 +236,7 @@ export const apiRequest = async ( }, }); + console.log('resposne: ', response); + return response; }; diff --git a/src/shared/request/links.ts b/src/shared/request/links.ts index f9a684e..b434082 100644 --- a/src/shared/request/links.ts +++ b/src/shared/request/links.ts @@ -6,5 +6,16 @@ export const links = { detail: (id: number) => `/shared/documents/${id}/`, payment: (order_id: number) => `/users/payme/link/${order_id}/`, sertifikat: (document_id: number) => - `/shared/certificate/${document_id}/pdf/`, + `/shared/certificate/${document_id}/download/`, + si: '/shared/ai_document/list/', + si_id: (si_id: number) => `/shared/ai_document/${si_id}/`, + si_payment: (document_id: number) => + `/shared/ai_document/pay/${document_id}/`, + si_create: '/shared/ai_document/create/', + document_types: '/shared/documents/types/', + pay_history: '/shared/orders/all/', + statistics: '/shared/statistics/', + wordCount: '/shared/check_file/', + users: '/users/profile/', + demo_pay: (order_id: number) => `/users/payme/link/${order_id}/`, }; diff --git a/src/shared/request/plagiarismapi.ts b/src/shared/request/plagiarismapi.ts index b5ee827..e5869b8 100644 --- a/src/shared/request/plagiarismapi.ts +++ b/src/shared/request/plagiarismapi.ts @@ -3,7 +3,7 @@ import { PlagiarismSubmissionPayload, PlagiarismSubmissionResponse, -} from '@/widgets/fileUpload/lib/types'; +} from '@/widgets/plagiatCheck/lib/types'; const API_BASE_URL = process.env.VITE_API_BASE_URL ?? '/api'; const ENDPOINT = `${API_BASE_URL}/plagiarism/submit`; diff --git a/src/shared/ui/navigation-menu.tsx b/src/shared/ui/navigation-menu.tsx index c6a844d..427bbdc 100644 --- a/src/shared/ui/navigation-menu.tsx +++ b/src/shared/ui/navigation-menu.tsx @@ -18,7 +18,7 @@ function NavigationMenu({ data-slot="navigation-menu" data-viewport={viewport} className={cn( - 'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center', + ' relative flex max-w-max flex-1 items-center justify-center', className, )} {...props} @@ -75,7 +75,7 @@ function NavigationMenuTrigger({ > {children}{' '}