Compare commits
29 Commits
73158a1972
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c2bc0f762 | ||
|
|
dc99f9a8d7 | ||
|
|
aea8854a13 | ||
|
|
75bd3467e9 | ||
|
|
ee34ef0905 | ||
|
|
46552fdd55 | ||
|
|
fa9df55856 | ||
|
|
2f8fc09027 | ||
|
|
18724e135b | ||
|
|
a6a221faf5 | ||
|
|
be582a6474 | ||
|
|
b4fd590eda | ||
|
|
553db7194a | ||
|
|
aba0d25cbd | ||
|
|
9a62c214a5 | ||
|
|
83d75ad499 | ||
|
|
10fcded48f | ||
|
|
ff8e003ef5 | ||
|
|
4a755d5d03 | ||
|
|
6e485484fb | ||
|
|
3af699c468 | ||
|
|
d3a868da68 | ||
|
|
0c3b5f50ad | ||
|
|
60c3ddf43c | ||
|
|
69abdd6562 | ||
|
|
1efb97e4c9 | ||
|
|
e8c4bd7bd1 | ||
|
|
00cbac1292 | ||
|
|
016094fa34 |
@@ -8,3 +8,4 @@ Disallow: /admin/
|
|||||||
|
|
||||||
# Sitemap location
|
# Sitemap location
|
||||||
Sitemap: https://antiplagiat.uz/sitemap.xml
|
Sitemap: https://antiplagiat.uz/sitemap.xml
|
||||||
|
# enjoy
|
||||||
|
|||||||
BIN
public/lanka.jpg
Normal file
BIN
public/lanka.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
106
public/llms.txt
Normal file
106
public/llms.txt
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# AntiPlagiat.uz — AI uchun sayt ma'lumotlari (llms.txt)
|
||||||
|
# Standart: https://llmstxt.org
|
||||||
|
# Yangilangan: 2026-04-12
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# SAYT HAQIDA / ABOUT THE SITE
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
> AntiPlagiat.uz — O'zbekistonning akademik plagiat tekshiruvi platformasi.
|
||||||
|
> Dissertatsiya, kurs ishi va ilmiy maqolalarni onlayn tekshiring.
|
||||||
|
> Natija daqiqalar ichida tayyor bo'ladi. Rasmiy sertifikat beriladi.
|
||||||
|
|
||||||
|
**Sayt nomi:** AntiPlagiat.uz
|
||||||
|
**Egalik qiluvchi:** Pedagok.uz
|
||||||
|
**Manzil:** https://anti-plagiat.uz
|
||||||
|
**Kanon manzil:** https://antiplagiat.uz
|
||||||
|
**Kategoriya:** Ta'lim / Academic integrity / Plagiarism detection
|
||||||
|
**Til:** O'zbek (uz), Rus (ru), Ingliz (en)
|
||||||
|
**Mamlakatlar:** O'zbekiston va MDH
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Asosiy xizmatlar / Core Services
|
||||||
|
|
||||||
|
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** — Originallik tasdiqlangan hujjatlar uchun rasmiy sertifikat beriladi.
|
||||||
|
4. **Batafsil hisobot** — O'xshashlik manbalari, mos keladigan so'zlar, AI tahlili ballari ko'rsatiladi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Texnik ma'lumotlar / Technical Details
|
||||||
|
|
||||||
|
- **Framework:** Next.js (React, App Router)
|
||||||
|
- **Lokalizatsiya:** /uz, /ru, /en yo'llar orqali
|
||||||
|
- **To'lov tizimi:** Payme (SSL himoyalangan)
|
||||||
|
- **Qo'llab-quvvatlangan formatlar:** PDF, DOC, DOCX, TXT
|
||||||
|
- **Maksimal fayl:** 20 MB
|
||||||
|
- **Aniqlik darajasi:** 98.7%
|
||||||
|
- **Tekshirilgan hujjatlar:** 50,000+
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sahifalar tuzilishi / Site Structure
|
||||||
|
|
||||||
|
### Ommaviy sahifalar (Public pages)
|
||||||
|
|
||||||
|
| URL | Maqsad |
|
||||||
|
|----------------------------------|-------------------------------------|
|
||||||
|
| /uz | Bosh sahifa (O'zbek) |
|
||||||
|
| /ru | Bosh sahifa (Rus) |
|
||||||
|
| /en | Bosh sahifa (Ingliz) |
|
||||||
|
| /uz/plagiat | Plagiat tekshiruvi (autentifikatsiya)|
|
||||||
|
| /ru/plagiat | Plagiat tekshiruvi (Rus) |
|
||||||
|
| /en/plagiat | Plagiarism check (English) |
|
||||||
|
| /uz/si | SI detektor (O'zbek) |
|
||||||
|
| /ru/si | SI detektor (Rus) |
|
||||||
|
| /en/si | AI Content detector (English) |
|
||||||
|
| /uz/history | Tekshiruv tarixi |
|
||||||
|
| /uz/cabinet | Shaxsiy kabinet (dashboard) |
|
||||||
|
|
||||||
|
### Dinamik sahifalar
|
||||||
|
|
||||||
|
| URL shakli | Maqsad |
|
||||||
|
|----------------------------------|-------------------------------------|
|
||||||
|
| /uz/{id} | Muayyan tekshiruv natijasi |
|
||||||
|
| /ru/{id} | Tekshiruv natijasi (Rus) |
|
||||||
|
| /en/{id} | Check result (English) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Foydalanuvchilar / Target Audience
|
||||||
|
|
||||||
|
- Talabalara: kurs ishlari, dissertatsiyalar, referatlar uchun
|
||||||
|
- O'qituvchilar: talabalar ishlarini tekshirish uchun
|
||||||
|
- Ilmiy xodimlar: maqolalar va tadqiqotlarni tekshirish uchun
|
||||||
|
- Muassasalar: akademik halollik nazorati uchun
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Aloqa / Contact
|
||||||
|
|
||||||
|
- **Veb-sayt:** https://anti-plagiat.uz
|
||||||
|
- **Twitter/X:** @antiplagiatuz
|
||||||
|
- **Egalik kompaniya:** Pedagok.uz
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Litsenziya va foydalanish / Usage
|
||||||
|
|
||||||
|
AntiPlagiat.uz kontenti faqat ma'lumot maqsadida foydalanilishi mumkin.
|
||||||
|
Foydalanuvchilar ma'lumotlari maxfiy saqlanadi va tahlil tugagach o'chiriladi.
|
||||||
|
Xizmat O'zbekiston qonunchiligiga muvofiq ishlaydi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Crawling yo'riqnomasi / Crawling Guidelines
|
||||||
|
|
||||||
|
AI botlar va qidiruv tizimlari uchun:
|
||||||
|
- robots.txt: https://anti-plagiat.uz/robots.txt
|
||||||
|
- sitemap.xml: https://anti-plagiat.uz/sitemap.xml
|
||||||
|
- Foydalanuvchi ma'lumotlarini (cabinet, history, individual check results) crawl qilmang
|
||||||
|
- API (/api/*) yo'llarini crawl qilmang
|
||||||
|
- Preferred crawl delay: 2 soniya
|
||||||
112
public/robots.txt
Normal file
112
public/robots.txt
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# robots.txt — anti-plagiat.uz
|
||||||
|
# Yangilangan: 2026-04-12
|
||||||
|
|
||||||
|
# ─── Umumiy qoidalar (barcha botlar) ───────────────────────────────────────
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Allow: /uz/
|
||||||
|
Allow: /ru/
|
||||||
|
Allow: /en/
|
||||||
|
Allow: /uz/plagiat
|
||||||
|
Allow: /ru/plagiat
|
||||||
|
Allow: /en/plagiat
|
||||||
|
Allow: /uz/si
|
||||||
|
Allow: /ru/si
|
||||||
|
Allow: /en/si
|
||||||
|
|
||||||
|
# API va autentifikatsiya marshrutlarini bloklash
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
Disallow: /uz/cabinet/
|
||||||
|
Disallow: /ru/cabinet/
|
||||||
|
Disallow: /en/cabinet/
|
||||||
|
|
||||||
|
# Crawl tezligi (ortiqcha yuklanmasin)
|
||||||
|
Crawl-delay: 2
|
||||||
|
|
||||||
|
# ─── Google ───────────────────────────────────────────────────────────────
|
||||||
|
User-agent: Googlebot
|
||||||
|
Allow: /
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
Crawl-delay: 1
|
||||||
|
|
||||||
|
User-agent: Googlebot-Image
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
# ─── Bing ─────────────────────────────────────────────────────────────────
|
||||||
|
User-agent: Bingbot
|
||||||
|
Allow: /
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
Crawl-delay: 2
|
||||||
|
|
||||||
|
# ─── AI Crawler agentlari ─────────────────────────────────────────────────
|
||||||
|
# GPT / OpenAI
|
||||||
|
User-agent: GPTBot
|
||||||
|
Allow: /
|
||||||
|
Allow: /uz/
|
||||||
|
Allow: /ru/
|
||||||
|
Allow: /en/
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
Disallow: /resources/media/
|
||||||
|
|
||||||
|
# Claude (Anthropic)
|
||||||
|
User-agent: ClaudeBot
|
||||||
|
Allow: /
|
||||||
|
Allow: /uz/
|
||||||
|
Allow: /ru/
|
||||||
|
Allow: /en/
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
|
||||||
|
# Perplexity
|
||||||
|
User-agent: PerplexityBot
|
||||||
|
Allow: /
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
|
||||||
|
# Meta AI
|
||||||
|
User-agent: Meta-ExternalAgent
|
||||||
|
Allow: /
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
|
||||||
|
# Gemini / Google AI
|
||||||
|
User-agent: Google-Extended
|
||||||
|
Allow: /
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
|
||||||
|
# Cohere
|
||||||
|
User-agent: cohere-ai
|
||||||
|
Allow: /
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
|
||||||
|
# Common Crawl (AI training ma'lumotlar to'plami)
|
||||||
|
User-agent: CCBot
|
||||||
|
Allow: /
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
Disallow: /resources/media/
|
||||||
|
|
||||||
|
# ─── Noxush botlar ────────────────────────────────────────────────────────
|
||||||
|
User-agent: MJ12bot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: DotBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: AhrefsBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: SemrushBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
# ─── Sitemap manzili ──────────────────────────────────────────────────────
|
||||||
|
Sitemap: https://anti-plagiat.uz/sitemap.xml
|
||||||
|
|
||||||
|
# ─── AI ma'lumot fayli ────────────────────────────────────────────────────
|
||||||
|
# AI modellar uchun: https://anti-plagiat.uz/llms.txt
|
||||||
@@ -1,84 +1,191 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- Bosh sahifalar / Home pages -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://anti-plagiat.uz/uz</loc>
|
<loc>https://anti-plagiat.uz/uz</loc>
|
||||||
<lastmod>2026-04-06</lastmod>
|
<lastmod>2026-04-12</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz" />
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru" />
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en" />
|
||||||
</url>
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru" />
|
||||||
<url>
|
|
||||||
<loc>https://anti-plagiat.uz/uz/plagat</loc>
|
|
||||||
<lastmod>2026-04-06</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.8</priority>
|
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/plagat"/>
|
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/plagat"/>
|
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/plagat"/>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://anti-plagiat.uz/uz/cabinet</loc>
|
|
||||||
<lastmod>2026-04-06</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.7</priority>
|
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/cabinet"/>
|
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/cabinet"/>
|
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/cabinet"/>
|
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://anti-plagiat.uz/ru</loc>
|
<loc>https://anti-plagiat.uz/ru</loc>
|
||||||
<lastmod>2026-04-06</lastmod>
|
<lastmod>2026-04-12</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz" />
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru" />
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en" />
|
||||||
</url>
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru" />
|
||||||
<url>
|
|
||||||
<loc>https://anti-plagiat.uz/ru/plagat</loc>
|
|
||||||
<lastmod>2026-04-06</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.8</priority>
|
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/plagat"/>
|
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/plagat"/>
|
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/plagat"/>
|
|
||||||
</url>
|
|
||||||
<url>
|
|
||||||
<loc>https://anti-plagiat.uz/ru/cabinet</loc>
|
|
||||||
<lastmod>2026-04-06</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.7</priority>
|
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/cabinet"/>
|
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/cabinet"/>
|
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/cabinet"/>
|
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://anti-plagiat.uz/en</loc>
|
<loc>https://anti-plagiat.uz/en</loc>
|
||||||
<lastmod>2026-04-06</lastmod>
|
<lastmod>2026-04-12</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz" />
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru" />
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru" />
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- Plagiat tekshiruvi sahifalari -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://anti-plagiat.uz/en/plagat</loc>
|
<loc>https://anti-plagiat.uz/uz/plagiat</loc>
|
||||||
<lastmod>2026-04-06</lastmod>
|
<lastmod>2026-04-12</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/plagiat" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/plagiat" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/plagiat" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/plagiat" />
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/ru/plagiat</loc>
|
||||||
|
<lastmod>2026-04-12</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/plagiat" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/plagiat" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/plagiat" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/plagiat" />
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/en/plagiat</loc>
|
||||||
|
<lastmod>2026-04-12</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/plagiat" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/plagiat" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/plagiat" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/plagiat" />
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- SI Detektor sahifalari -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/uz/si</loc>
|
||||||
|
<lastmod>2026-04-12</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/plagat"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/si" />
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/plagat"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/si" />
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/plagat"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/si" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/si" />
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/ru/si</loc>
|
||||||
|
<lastmod>2026-04-12</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/si" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/si" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/si" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/si" />
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/en/si</loc>
|
||||||
|
<lastmod>2026-04-12</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/si" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/si" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/si" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/si" />
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- Tekshiruv tarixi sahifalari -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/uz/history</loc>
|
||||||
|
<lastmod>2026-04-12</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/history" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/history" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/history" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/history" />
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/ru/history</loc>
|
||||||
|
<lastmod>2026-04-12</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/history" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/history" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/history" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/history" />
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/en/history</loc>
|
||||||
|
<lastmod>2026-04-12</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/history" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/history" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/history" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/history" />
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- Shaxsiy kabinet sahifalari -->
|
||||||
|
<!-- (noindex - autentifikatsiya) -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/uz/cabinet</loc>
|
||||||
|
<lastmod>2026-04-12</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.4</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/cabinet" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/cabinet" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/cabinet" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/cabinet" />
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/ru/cabinet</loc>
|
||||||
|
<lastmod>2026-04-12</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.4</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/cabinet" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/cabinet" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/cabinet" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/cabinet" />
|
||||||
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://anti-plagiat.uz/en/cabinet</loc>
|
<loc>https://anti-plagiat.uz/en/cabinet</loc>
|
||||||
<lastmod>2026-04-06</lastmod>
|
<lastmod>2026-04-12</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.4</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/cabinet"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/cabinet" />
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/cabinet"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/cabinet" />
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/cabinet"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/cabinet" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://anti-plagiat.uz/ru/cabinet" />
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
</urlset>
|
</urlset>
|
||||||
|
|||||||
26
src/app/[locale]/dashboard/page.tsx
Normal file
26
src/app/[locale]/dashboard/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
src/app/[locale]/payments/page.tsx
Normal file
9
src/app/[locale]/payments/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/app/[locale]/profile/page.tsx
Normal file
10
src/app/[locale]/profile/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/app/[locale]/si/page.tsx
Normal file
12
src/app/[locale]/si/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ export function useLoginForm() {
|
|||||||
console.log('Login successful:', data);
|
console.log('Login successful:', data);
|
||||||
toggleLoginModal();
|
toggleLoginModal();
|
||||||
toast.success('Kirish muvaffaqiyatli!');
|
toast.success('Kirish muvaffaqiyatli!');
|
||||||
route.push('/plagiat');
|
route.push('/dashboard');
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.log('Login failed:', err);
|
console.log('Login failed:', err);
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ export interface PaymentModalProps {
|
|||||||
price: PriceCalculate;
|
price: PriceCalculate;
|
||||||
onConfirmPayment: () => void;
|
onConfirmPayment: () => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
hasSertificate: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export const PaymentModal: React.FC<PaymentModalProps> = ({
|
|||||||
price,
|
price,
|
||||||
onConfirmPayment,
|
onConfirmPayment,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
hasSertificate,
|
||||||
}) => {
|
}) => {
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
const status = isLoading ? 'loading' : 'idle';
|
const status = isLoading ? 'loading' : 'idle';
|
||||||
@@ -144,18 +145,20 @@ export const PaymentModal: React.FC<PaymentModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Certificate badge */}
|
{/* Certificate badge */}
|
||||||
<div className="flex items-center gap-2 text-sm text-emerald-700 bg-emerald-50 border border-emerald-100 rounded-lg px-3.5 py-2.5">
|
{hasSertificate && (
|
||||||
<svg
|
<div className="flex items-center gap-2 text-sm text-emerald-700 bg-emerald-50 border border-emerald-100 rounded-lg px-3.5 py-2.5">
|
||||||
width="15"
|
<svg
|
||||||
height="15"
|
width="15"
|
||||||
viewBox="0 0 24 24"
|
height="15"
|
||||||
fill="currentColor"
|
viewBox="0 0 24 24"
|
||||||
className="shrink-0"
|
fill="currentColor"
|
||||||
>
|
className="shrink-0"
|
||||||
<path d="M12 1l2.753 5.527 6.247.907-4.5 4.385 1.063 6.181L12 15.027l-5.563 2.973 1.063-6.181L3 7.434l6.247-.907z" />
|
>
|
||||||
</svg>
|
<path d="M12 1l2.753 5.527 6.247.907-4.5 4.385 1.063 6.181L12 15.027l-5.563 2.973 1.063-6.181L3 7.434l6.247-.907z" />
|
||||||
<span>{t('certificateIncluded')}</span>
|
</svg>
|
||||||
</div>
|
<span>{t('certificateIncluded')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Payment method label */}
|
{/* Payment method label */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ export function useCertificateModal({
|
|||||||
|
|
||||||
const certificateMutation = useMutation({
|
const certificateMutation = useMutation({
|
||||||
mutationFn: (payload: CertificatePayload) =>
|
mutationFn: (payload: CertificatePayload) =>
|
||||||
apiRequest('POST', links.sertifikat(document_id), payload).then(
|
apiRequest<ArrayBuffer>('POST', links.sertifikat(document_id), payload, {
|
||||||
(res) => res.data as ArrayBuffer,
|
responseType: 'arraybuffer',
|
||||||
),
|
}).then((res) => res.data),
|
||||||
onSuccess: (data: ArrayBuffer) => {
|
onSuccess: (data: ArrayBuffer) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const blob = new Blob([data], { type: 'application/pdf' });
|
const blob = new Blob([data], { type: 'application/pdf' });
|
||||||
@@ -54,7 +54,7 @@ export function useCertificateModal({
|
|||||||
a.href = objectUrl;
|
a.href = objectUrl;
|
||||||
a.download = `certificate-${document_id}.pdf`;
|
a.download = `certificate-${document_id}.pdf`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(objectUrl);
|
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
|
||||||
}
|
}
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -1,54 +1,30 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { ArrowRight, BrainCircuit } from 'lucide-react';
|
||||||
import { ArrowRight, BrainCircuit, Plus } from 'lucide-react';
|
|
||||||
import { FileUploadModal } from './ui/fileUploadModal';
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export function SiCTACard() {
|
export function SiCTACard() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const t = useTranslations('Cabinet');
|
const t = useTranslations('Cabinet');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Link
|
||||||
<button
|
href="/si"
|
||||||
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"
|
||||||
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">
|
||||||
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
|
<BrainCircuit size={72} className="text-white" />
|
||||||
<BrainCircuit size={72} className="text-white" />
|
</div>
|
||||||
</div>
|
<BrainCircuit size={26} className="text-white mb-4" />
|
||||||
<BrainCircuit size={26} className="text-white mb-4" />
|
<h3 className="text-white font-semibold text-base mb-1">
|
||||||
<h3 className="text-white font-semibold text-base mb-1">
|
{t('siDetector')}
|
||||||
{t('siDetector')}
|
</h3>
|
||||||
</h3>
|
<p className="text-violet-100 text-sm mb-4 leading-relaxed">
|
||||||
<p className="text-violet-100 text-sm mb-4 leading-relaxed">
|
{t('siDesc')}
|
||||||
{t('siDesc')}
|
</p>
|
||||||
</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">
|
||||||
<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} />
|
||||||
{t('submit')} <ArrowRight size={12} />
|
</span>
|
||||||
</span>
|
</Link>
|
||||||
</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)} />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
117
src/features/modals/siModal/ui/SiUploadSection.tsx
Normal file
117
src/features/modals/siModal/ui/SiUploadSection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,9 @@
|
|||||||
"login": "Login",
|
"login": "Login",
|
||||||
"signup": "Sign up",
|
"signup": "Sign up",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"logout": "Logout"
|
"logout": "Logout",
|
||||||
|
"aboutPlagiat": "About Plagiarism",
|
||||||
|
"howItWorks": "How It Works"
|
||||||
},
|
},
|
||||||
"Footer": {
|
"Footer": {
|
||||||
"product": "Product",
|
"product": "Product",
|
||||||
@@ -51,7 +53,9 @@
|
|||||||
"certificateDescription": "An official certificate will be attached to your originality report.",
|
"certificateDescription": "An official certificate will be attached to your originality report.",
|
||||||
"submitting": "Submitting…",
|
"submitting": "Submitting…",
|
||||||
"submitButton": "Submit for Originality Check",
|
"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": {
|
"HistoryPage": {
|
||||||
"title": "Check History",
|
"title": "Check History",
|
||||||
@@ -61,8 +65,8 @@
|
|||||||
"date": "Date",
|
"date": "Date",
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"result": "Result",
|
"result": "Result",
|
||||||
"fileName":"File name",
|
"fileName": "File name",
|
||||||
"count":"N_",
|
"count": "N_",
|
||||||
"actions": "",
|
"actions": "",
|
||||||
"state": "Payment status",
|
"state": "Payment status",
|
||||||
"emptyMessage": "No plagiarism checks found.",
|
"emptyMessage": "No plagiarism checks found.",
|
||||||
@@ -237,7 +241,7 @@
|
|||||||
"security": "Secured by Payme · SSL encrypted",
|
"security": "Secured by Payme · SSL encrypted",
|
||||||
"serviceFee": "Service fee",
|
"serviceFee": "Service fee",
|
||||||
"discountLabel": "Discount",
|
"discountLabel": "Discount",
|
||||||
"sertificateLabel":"Certificate",
|
"sertificateLabel": "Certificate",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"paymentRequired": "Payment not completed",
|
"paymentRequired": "Payment not completed",
|
||||||
"connecting": "Connecting to Payme…",
|
"connecting": "Connecting to Payme…",
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
"login": "Войти",
|
"login": "Войти",
|
||||||
"signup": "Регистрация",
|
"signup": "Регистрация",
|
||||||
"profile": "Профиль",
|
"profile": "Профиль",
|
||||||
"logout": "Выйти"
|
"logout": "Выйти",
|
||||||
|
"aboutPlagiat": "О плагиате",
|
||||||
|
"howItWorks": "Как это работает"
|
||||||
},
|
},
|
||||||
"Footer": {
|
"Footer": {
|
||||||
"product": "Продукт",
|
"product": "Продукт",
|
||||||
@@ -51,7 +53,9 @@
|
|||||||
"certificateDescription": "Официальный сертификат будет прикреплен к вашему отчету об оригинальности.",
|
"certificateDescription": "Официальный сертификат будет прикреплен к вашему отчету об оригинальности.",
|
||||||
"submitting": "Отправка…",
|
"submitting": "Отправка…",
|
||||||
"submitButton": "Отправить на проверку оригинальности",
|
"submitButton": "Отправить на проверку оригинальности",
|
||||||
"dismiss": "Закрыть"
|
"dismiss": "Закрыть",
|
||||||
|
"service_price": "Стоимость услуги {PLAGIAT_SERVICE_FEE} сум",
|
||||||
|
"sertificate_price": "Стоимость сертификата {SERTIFICATE_PRICE} сум"
|
||||||
},
|
},
|
||||||
"HistoryPage": {
|
"HistoryPage": {
|
||||||
"title": "История проверок",
|
"title": "История проверок",
|
||||||
@@ -61,7 +65,7 @@
|
|||||||
"date": "Дата",
|
"date": "Дата",
|
||||||
"amount": "Сумма",
|
"amount": "Сумма",
|
||||||
"result": "Результат",
|
"result": "Результат",
|
||||||
"count":"H_",
|
"count": "H_",
|
||||||
"actions": "",
|
"actions": "",
|
||||||
"state": "Статус оплаты",
|
"state": "Статус оплаты",
|
||||||
"emptyMessage": "Проверки на плагиат не найдены.",
|
"emptyMessage": "Проверки на плагиат не найдены.",
|
||||||
@@ -236,7 +240,7 @@
|
|||||||
"security": "Защищено Payme · SSL шифрование",
|
"security": "Защищено Payme · SSL шифрование",
|
||||||
"serviceFee": "Стоимость услуги",
|
"serviceFee": "Стоимость услуги",
|
||||||
"discountLabel": "Скидка",
|
"discountLabel": "Скидка",
|
||||||
"sertificateLabel":"Сертификат",
|
"sertificateLabel": "Сертификат",
|
||||||
"total": "Итого",
|
"total": "Итого",
|
||||||
"paymentRequired": "Оплата не произведена",
|
"paymentRequired": "Оплата не произведена",
|
||||||
"connecting": "Подключение к Payme…",
|
"connecting": "Подключение к Payme…",
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ declare const messages: {
|
|||||||
signup: "Ro'yxatdan o'tish";
|
signup: "Ro'yxatdan o'tish";
|
||||||
profile: 'Profil';
|
profile: 'Profil';
|
||||||
logout: 'Chiqish';
|
logout: 'Chiqish';
|
||||||
|
aboutPlagiat: 'Plagiat haqida';
|
||||||
|
howItWorks: 'Bu qanday ishlaydi';
|
||||||
};
|
};
|
||||||
Footer: {
|
Footer: {
|
||||||
product: 'Mahsulot';
|
product: 'Mahsulot';
|
||||||
@@ -35,11 +37,11 @@ declare const messages: {
|
|||||||
terms: 'Foydalanish shartlari';
|
terms: 'Foydalanish shartlari';
|
||||||
};
|
};
|
||||||
PlagiarismCheck: {
|
PlagiarismCheck: {
|
||||||
badge: 'Orijinallik tekshiruvi';
|
badge: 'Originallik tekshiruvi';
|
||||||
title: 'Hujjatni yuboring';
|
title: 'Hujjatni yuboring';
|
||||||
submissionSuccess: 'Yuborish muvaffaqiyatli yakunlandi! ID';
|
submissionSuccess: 'Yuborish muvaffaqiyatli yakunlandi! ID';
|
||||||
secureNote: 'Hujjatingiz xavfsiz qayta ishlanadi va tahlil muddati tugagach saqlanmaydi.';
|
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';
|
documentTopic: 'Hujjat mavzusi';
|
||||||
topicPlaceholder: "masalan: Sun'iy intellektning ta'limga ta'siri";
|
topicPlaceholder: "masalan: Sun'iy intellektning ta'limga ta'siri";
|
||||||
senderFullName: "Yuboruvchi to'liq ismi";
|
senderFullName: "Yuboruvchi to'liq ismi";
|
||||||
@@ -51,10 +53,12 @@ declare const messages: {
|
|||||||
autoFilled: "Avto-to'ldirilgan";
|
autoFilled: "Avto-to'ldirilgan";
|
||||||
removeFile: 'Faylni olib tashlash';
|
removeFile: 'Faylni olib tashlash';
|
||||||
certificateTitle: 'Natijani sertifikat bilan qaytarish';
|
certificateTitle: 'Natijani sertifikat bilan qaytarish';
|
||||||
certificateDescription: 'Rasmiy sertifikat sizning orijinallik hisobotingizga ilova qilinadi.';
|
certificateDescription: 'Rasmiy sertifikat sizning originallik hisobotingizga ilova qilinadi.';
|
||||||
submitting: 'Yuborilmoqda…';
|
submitting: 'Yuborilmoqda…';
|
||||||
submitButton: 'Orijinallik tekshiruvi uchun yuborish';
|
submitButton: 'Originallik tekshiruvi uchun yuborish';
|
||||||
dismiss: 'Yopish';
|
dismiss: 'Yopish';
|
||||||
|
service_price: "Xizmat narxi {PLAGIAT_SERVICE_FEE} so'm";
|
||||||
|
sertificate_price: "Sertifikat narxi {SERTIFICATE_PRICE} so'm";
|
||||||
};
|
};
|
||||||
HistoryPage: {
|
HistoryPage: {
|
||||||
title: 'Tekshiruv tarixi';
|
title: 'Tekshiruv tarixi';
|
||||||
@@ -188,10 +192,10 @@ declare const messages: {
|
|||||||
card2Title: 'Nima uchun hujjatingizni tekshirish kerak?';
|
card2Title: 'Nima uchun hujjatingizni tekshirish kerak?';
|
||||||
card2Desc: "Akademik halollikni ta'minlang, jarimalardan qoching va obro'ingizni saqlang. Bizning xizmatimiz kompleks plagiat aniqlashni taqdim etadi.";
|
card2Desc: "Akademik halollikni ta'minlang, jarimalardan qoching va obro'ingizni saqlang. Bizning xizmatimiz kompleks plagiat aniqlashni taqdim etadi.";
|
||||||
card3Title: 'Siz nima olasiz';
|
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: {
|
Ticker: {
|
||||||
item1: 'Orijinallik tasdiqlangan';
|
item1: 'Originallik tasdiqlangan';
|
||||||
item2: 'Akademik halollik';
|
item2: 'Akademik halollik';
|
||||||
item3: 'Ishonchli hisobotlar';
|
item3: 'Ishonchli hisobotlar';
|
||||||
item4: 'Chuqur tahlil';
|
item4: 'Chuqur tahlil';
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
"login": "Kirish",
|
"login": "Kirish",
|
||||||
"signup": "Ro'yxatdan o'tish",
|
"signup": "Ro'yxatdan o'tish",
|
||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"logout": "Chiqish"
|
"logout": "Chiqish",
|
||||||
|
"aboutPlagiat": "Plagiat haqida",
|
||||||
|
"howItWorks": "Bu qanday ishlaydi"
|
||||||
},
|
},
|
||||||
"Footer": {
|
"Footer": {
|
||||||
"product": "Mahsulot",
|
"product": "Mahsulot",
|
||||||
@@ -32,11 +34,11 @@
|
|||||||
"terms": "Foydalanish shartlari"
|
"terms": "Foydalanish shartlari"
|
||||||
},
|
},
|
||||||
"PlagiarismCheck": {
|
"PlagiarismCheck": {
|
||||||
"badge": "Orijinallik tekshiruvi",
|
"badge": "Originallik tekshiruvi",
|
||||||
"title": "Hujjatni yuboring",
|
"title": "Hujjatni yuboring",
|
||||||
"submissionSuccess": "Yuborish muvaffaqiyatli yakunlandi! ID",
|
"submissionSuccess": "Yuborish muvaffaqiyatli yakunlandi! ID",
|
||||||
"secureNote": "Hujjatingiz xavfsiz qayta ishlanadi va tahlil muddati tugagach saqlanmaydi.",
|
"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",
|
"documentTopic": "Hujjat mavzusi",
|
||||||
"topicPlaceholder": "masalan: Sun'iy intellektning ta'limga ta'siri",
|
"topicPlaceholder": "masalan: Sun'iy intellektning ta'limga ta'siri",
|
||||||
"senderFullName": "Yuboruvchi to'liq ismi",
|
"senderFullName": "Yuboruvchi to'liq ismi",
|
||||||
@@ -48,18 +50,20 @@
|
|||||||
"autoFilled": "Avto-to'ldirilgan",
|
"autoFilled": "Avto-to'ldirilgan",
|
||||||
"removeFile": "Faylni olib tashlash",
|
"removeFile": "Faylni olib tashlash",
|
||||||
"certificateTitle": "Natijani sertifikat bilan qaytarish",
|
"certificateTitle": "Natijani sertifikat bilan qaytarish",
|
||||||
"certificateDescription": "Rasmiy sertifikat sizning orijinallik hisobotingizga ilova qilinadi.",
|
"certificateDescription": "Rasmiy sertifikat sizning originallik hisobotingizga ilova qilinadi.",
|
||||||
"submitting": "Yuborilmoqda…",
|
"submitting": "Yuborilmoqda…",
|
||||||
"submitButton": "Orijinallik tekshiruvi uchun yuborish",
|
"submitButton": "Originallik tekshiruvi uchun yuborish",
|
||||||
"dismiss": "Yopish"
|
"dismiss": "Yopish",
|
||||||
|
"service_price": "Xizmat narxi {PLAGIAT_SERVICE_FEE} so'm",
|
||||||
|
"sertificate_price": "Sertifikat narxi {SERTIFICATE_PRICE} so'm"
|
||||||
},
|
},
|
||||||
"HistoryPage": {
|
"HistoryPage": {
|
||||||
"title": "Tekshiruv tarixi",
|
"title": "Tekshiruv tarixi",
|
||||||
"description": "Siz tomonidan yuborilgan barcha plagiat tekshiruvlari",
|
"description": "Siz tomonidan yuborilgan barcha plagiat tekshiruvlari",
|
||||||
"sender": "Yuboruvchi",
|
"sender": "Yuboruvchi",
|
||||||
"file": "Fayl",
|
"file": "Fayl",
|
||||||
"fileName":"Fayl nomi",
|
"fileName": "Fayl nomi",
|
||||||
"count":"N_",
|
"count": "N_",
|
||||||
"date": "Sana",
|
"date": "Sana",
|
||||||
"amount": "Summa",
|
"amount": "Summa",
|
||||||
"result": "Natija",
|
"result": "Natija",
|
||||||
@@ -126,8 +130,8 @@
|
|||||||
"downloadCertificate": "Sertifikatni yuklab olish",
|
"downloadCertificate": "Sertifikatni yuklab olish",
|
||||||
"unknownError": "Noma'lum xato",
|
"unknownError": "Noma'lum xato",
|
||||||
"words": "so'z",
|
"words": "so'z",
|
||||||
"aiProbabilityText":"Ai yordamida yaratilganlik ehtimoli aniqlandi",
|
"aiProbabilityText": "Ai yordamida yaratilganlik ehtimoli aniqlandi",
|
||||||
"documentNumber":"Dokument mavzusi",
|
"documentNumber": "Dokument mavzusi",
|
||||||
"scoreAiContent": "O'zidan iqtibos keltirish",
|
"scoreAiContent": "O'zidan iqtibos keltirish",
|
||||||
"scoreOriginality": "Originallik",
|
"scoreOriginality": "Originallik",
|
||||||
"scorePlagiarism": "Plagiat",
|
"scorePlagiarism": "Plagiat",
|
||||||
@@ -185,10 +189,10 @@
|
|||||||
"card2Title": "Nima uchun hujjatingizni tekshirish kerak?",
|
"card2Title": "Nima uchun hujjatingizni tekshirish kerak?",
|
||||||
"card2Desc": "Akademik halollikni ta'minlang, jarimalardan qoching va obro'ingizni saqlang. Bizning xizmatimiz kompleks plagiat aniqlashni taqdim etadi.",
|
"card2Desc": "Akademik halollikni ta'minlang, jarimalardan qoching va obro'ingizni saqlang. Bizning xizmatimiz kompleks plagiat aniqlashni taqdim etadi.",
|
||||||
"card3Title": "Siz nima olasiz",
|
"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": {
|
"Ticker": {
|
||||||
"item1": "Orijinallik tasdiqlangan",
|
"item1": "Originallik tasdiqlangan",
|
||||||
"item2": "Akademik halollik",
|
"item2": "Akademik halollik",
|
||||||
"item3": "Ishonchli hisobotlar",
|
"item3": "Ishonchli hisobotlar",
|
||||||
"item4": "Chuqur tahlil",
|
"item4": "Chuqur tahlil",
|
||||||
@@ -237,9 +241,9 @@
|
|||||||
"security": "Payme tomonidan himoyalangan · SSL shifrlash",
|
"security": "Payme tomonidan himoyalangan · SSL shifrlash",
|
||||||
"serviceFee": "Xizmat to'lovi",
|
"serviceFee": "Xizmat to'lovi",
|
||||||
"discountLabel": "Chegirma",
|
"discountLabel": "Chegirma",
|
||||||
"sertificateLabel":"Sertifikat",
|
"sertificateLabel": "Sertifikat",
|
||||||
"total": "Jami",
|
"total": "Jami",
|
||||||
"paymentRequired":"To'lov qilinmagan",
|
"paymentRequired": "To'lov qilinmagan",
|
||||||
"connecting": "Paymega ulanmoqda…",
|
"connecting": "Paymega ulanmoqda…",
|
||||||
"payButton": "Payme orqali to'lash"
|
"payButton": "Payme orqali to'lash"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { SEO_DATA, type SupportedLocale } from '../config/seo.config';
|
|||||||
|
|
||||||
// ─── Site-wide constants ───────────────────────────────────────────────────────
|
// ─── 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 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 OG_IMAGE_URL = `${SITE_URL}/og-image.png`; // 1200×630 px recommended
|
||||||
const TWITTER_HANDLE = '@antiplagiatuz'; // update or remove if unused
|
const TWITTER_HANDLE = '@antiplagiatuz'; // update or remove if unused
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function extractErrorMessage(error: AxiosError): string {
|
|||||||
// ─── Constants ─────────────────────────────────────────────────────────────────
|
// ─── Constants ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
|
// const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
const baseUrl = 'https://dev-api.anti-plagiat.uz/api/v1';
|
const baseUrl = 'https://api.anti-plagiat.uz/api/v1';
|
||||||
const DEFAULT_LOCALE = 'uz'; // fallback locale for redirect
|
const DEFAULT_LOCALE = 'uz'; // fallback locale for redirect
|
||||||
|
|
||||||
// ─── Token helpers ─────────────────────────────────────────────────────────────
|
// ─── Token helpers ─────────────────────────────────────────────────────────────
|
||||||
@@ -143,11 +143,19 @@ api.interceptors.response.use(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const status = error.response?.status;
|
const status = error.response?.status;
|
||||||
|
// const responseData = error.response?.data as Record<string, unknown> | undefined;
|
||||||
const requestUrl = originalRequest.url ?? '';
|
const requestUrl = originalRequest.url ?? '';
|
||||||
const isAuthEndpoint =
|
const isAuthEndpoint =
|
||||||
requestUrl.includes('/users/login/') ||
|
requestUrl.includes('/users/login/') ||
|
||||||
requestUrl.includes('/users/register/');
|
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
|
// For auth endpoints, 401 means wrong credentials — show error, don't refresh
|
||||||
if (isAuthEndpoint || status !== 401 || originalRequest._retry) {
|
if (isAuthEndpoint || status !== 401 || originalRequest._retry) {
|
||||||
toast.error(extractErrorMessage(error));
|
toast.error(extractErrorMessage(error));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const links = {
|
export const links = {
|
||||||
login: '/users/login/',
|
login: '/users/login/',
|
||||||
register: '/users/register/',
|
register: '/users/register/',
|
||||||
plagiarismCheck: '/shared/document/',
|
plagiarismCheck: '/shared/documents/',
|
||||||
history: '/shared/documents/list/',
|
history: '/shared/documents/list/',
|
||||||
detail: (id: number) => `/shared/documents/${id}/`,
|
detail: (id: number) => `/shared/documents/${id}/`,
|
||||||
payment: (order_id: number) => `/users/payme/link/${order_id}/`,
|
payment: (order_id: number) => `/users/payme/link/${order_id}/`,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function NavigationMenu({
|
|||||||
data-slot="navigation-menu"
|
data-slot="navigation-menu"
|
||||||
data-viewport={viewport}
|
data-viewport={viewport}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
12
src/shared/zustand/cabinetNav.ts
Normal file
12
src/shared/zustand/cabinetNav.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { CabinetSection } from '@/widgets/cabinet/lib/types';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
type CabinetNavZustand = {
|
||||||
|
navItem: CabinetSection;
|
||||||
|
setNavItem: (item: CabinetSection) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCabinetNav = create<CabinetNavZustand>((set) => ({
|
||||||
|
navItem: 'dashboard',
|
||||||
|
setNavItem: (item: CabinetSection) => set({ navItem: item }),
|
||||||
|
}));
|
||||||
@@ -1,14 +1,43 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useCabinetNav } from '@/shared/zustand/cabinetNav';
|
||||||
import type { CabinetSection } from '../types';
|
import type { CabinetSection } from '../types';
|
||||||
|
|
||||||
|
const VALID_SECTIONS: CabinetSection[] = [
|
||||||
|
'dashboard',
|
||||||
|
'plagiat',
|
||||||
|
'si',
|
||||||
|
'payments',
|
||||||
|
'profile',
|
||||||
|
];
|
||||||
|
|
||||||
export const useCabinet = () => {
|
export const useCabinet = () => {
|
||||||
|
const { navItem, setNavItem } = useCabinetNav();
|
||||||
|
const navItemZustrand = useCabinetNav((state) => state.navItem);
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const [activeSection, setActiveSection] =
|
const [activeSection, setActiveSection] =
|
||||||
useState<CabinetSection>('dashboard');
|
useState<CabinetSection>('dashboard');
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (navItemZustrand) {
|
||||||
|
const activeSection: CabinetSection = VALID_SECTIONS.includes(
|
||||||
|
navItemZustrand as CabinetSection,
|
||||||
|
)
|
||||||
|
? (navItemZustrand as CabinetSection)
|
||||||
|
: 'dashboard';
|
||||||
|
setActiveSection(activeSection);
|
||||||
|
} else {
|
||||||
|
const activeSection: CabinetSection = VALID_SECTIONS.includes(
|
||||||
|
navItem as CabinetSection,
|
||||||
|
)
|
||||||
|
? (navItem as CabinetSection)
|
||||||
|
: 'dashboard';
|
||||||
|
setActiveSection(activeSection);
|
||||||
|
}
|
||||||
|
}, [navItemZustrand]);
|
||||||
|
|
||||||
const navigate = (section: CabinetSection) => {
|
const navigate = (section: CabinetSection) => {
|
||||||
setActiveSection(section);
|
setNavItem(section);
|
||||||
setIsSidebarOpen(false);
|
setIsSidebarOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Sidebar } from './Sidebar';
|
|
||||||
import { CabinetNav } from './CabinetNav';
|
|
||||||
import { Dashboard } from './dashboard';
|
import { Dashboard } from './dashboard';
|
||||||
import { useCabinet } from '../lib/hooks/useCabinet';
|
import { useCabinet } from '../lib/hooks/useCabinet';
|
||||||
import { MOCK_USER, MOCK_STATS } from '../lib/mock';
|
import { MOCK_USER, MOCK_STATS } from '../lib/mock';
|
||||||
@@ -69,31 +67,15 @@ const FADE = {
|
|||||||
// ─── CabinetLayout ────────────────────────────────────────────────────────────
|
// ─── CabinetLayout ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const CabinetLayout: React.FC = () => {
|
export const CabinetLayout: React.FC = () => {
|
||||||
const { activeSection, navigate, isSidebarOpen, toggleSidebar } =
|
const { activeSection } = useCabinet();
|
||||||
useCabinet();
|
|
||||||
const fullName = `${MOCK_USER.first_name} ${MOCK_USER.last_name}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex bg-slate-50 min-h-screen">
|
<main className="flex-1 p-4 md:p-6 lg:p-8 max-w-6xl mx-auto w-full pt-20">
|
||||||
<Sidebar
|
<AnimatePresence mode="wait">
|
||||||
active={activeSection}
|
<motion.div key={activeSection} {...FADE}>
|
||||||
onNavigate={navigate}
|
<SectionContent section={activeSection} />
|
||||||
isOpen={isSidebarOpen}
|
</motion.div>
|
||||||
onClose={toggleSidebar}
|
</AnimatePresence>
|
||||||
userName={fullName}
|
</main>
|
||||||
/>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Clock, XCircle, ReceiptText } from 'lucide-react';
|
import { Clock, XCircle, ReceiptText } from 'lucide-react';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { apiRequest } from '@/shared/request/apiRequest';
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
import { links } from '@/shared/request/links';
|
import { links } from '@/shared/request/links';
|
||||||
import PaymentStatus from '@/widgets/detail/paidStatus';
|
import PaymentStatus from '@/widgets/detail/paidStatus';
|
||||||
import { toast } from 'react-toastify';
|
import { Pagination } from '@/widgets/history/ui/pagination';
|
||||||
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
|
// import { toast } from 'react-toastify';
|
||||||
|
// import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
|
||||||
|
|
||||||
// ─── Types ─────────────────────────────────────────────────────────────────────
|
// ─── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -36,9 +37,11 @@ function formatPrice(price: string) {
|
|||||||
|
|
||||||
// ─── Component ─────────────────────────────────────────────────────────────────
|
// ─── Component ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
export function PaymentsTable() {
|
export function PaymentsTable() {
|
||||||
const t = useTranslations('Cabinet');
|
const t = useTranslations('Cabinet');
|
||||||
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['pay_history'],
|
queryKey: ['pay_history'],
|
||||||
queryFn: (): Promise<Inspection[]> =>
|
queryFn: (): Promise<Inspection[]> =>
|
||||||
@@ -47,29 +50,35 @@ export function PaymentsTable() {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const payment = useMutation({
|
const totalPages = Math.ceil((data?.length ?? 0) / PAGE_SIZE);
|
||||||
mutationKey: ['payload'],
|
const pageItems = data?.slice(
|
||||||
mutationFn: ({ order_id }: { order_id: number }) =>
|
(currentPage - 1) * PAGE_SIZE,
|
||||||
apiRequest<{ payment_link: string }>('POST', links.payment(order_id)),
|
currentPage * PAGE_SIZE,
|
||||||
onSuccess: (res) => {
|
);
|
||||||
window.open(res.data.payment_link, '_self');
|
|
||||||
setIsPaymentOpen(false);
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
const message =
|
|
||||||
err instanceof Error ? err.message : 'An unexpected error occurred.';
|
|
||||||
toast.error(message);
|
|
||||||
setIsPaymentOpen(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = ({ document_id }: { document_id: number }) => {
|
// const payment = useMutation({
|
||||||
if (document_id === 0) {
|
// mutationKey: ['payload'],
|
||||||
toast.error('Id not found');
|
// mutationFn: ({ order_id }: { order_id: number }) =>
|
||||||
return;
|
// apiRequest<{ payment_link: string }>('POST', links.payment(order_id)),
|
||||||
}
|
// onSuccess: (res) => {
|
||||||
payment.mutate({ order_id: document_id });
|
// window.open(res.data.payment_link, '_self');
|
||||||
};
|
// // setIsPaymentOpen(false);
|
||||||
|
// },
|
||||||
|
// onError: (err) => {
|
||||||
|
// const message =
|
||||||
|
// err instanceof Error ? err.message : 'An unexpected error occurred.';
|
||||||
|
// toast.error(message);
|
||||||
|
// // setIsPaymentOpen(false);
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const handleSubmit = ({ document_id }: { document_id: number }) => {
|
||||||
|
// if (document_id === 0) {
|
||||||
|
// toast.error('Id not found');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// payment.mutate({ order_id: document_id });
|
||||||
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -93,31 +102,30 @@ export function PaymentsTable() {
|
|||||||
<p className="text-sm">{t('noPayments')}</p>
|
<p className="text-sm">{t('noPayments')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<>
|
||||||
<table className="w-full text-sm">
|
<div className="overflow-x-auto">
|
||||||
<thead>
|
<table className="w-full text-sm">
|
||||||
<tr className="bg-slate-50 border-b border-slate-100">
|
<thead>
|
||||||
{[
|
<tr className="bg-slate-50 border-b border-slate-100">
|
||||||
t('tableNum'),
|
{[
|
||||||
t('service'),
|
t('tableNum'),
|
||||||
t('amount'),
|
t('service'),
|
||||||
t('discount'),
|
t('amount'),
|
||||||
t('date'),
|
t('discount'),
|
||||||
t('status'),
|
t('date'),
|
||||||
].map((h) => (
|
t('status'),
|
||||||
<th
|
].map((h) => (
|
||||||
key={h}
|
<th
|
||||||
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap"
|
key={h}
|
||||||
>
|
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap"
|
||||||
{h}
|
>
|
||||||
</th>
|
{h}
|
||||||
))}
|
</th>
|
||||||
</tr>
|
))}
|
||||||
</thead>
|
</tr>
|
||||||
<tbody className="divide-y divide-slate-50">
|
</thead>
|
||||||
{data.map((row) => {
|
<tbody className="divide-y divide-slate-50">
|
||||||
const service_fee = row.total_price + row.discount;
|
{pageItems!.map((row) => (
|
||||||
return (
|
|
||||||
<tr
|
<tr
|
||||||
key={row.id}
|
key={row.id}
|
||||||
className="hover:bg-slate-50/60 transition-colors"
|
className="hover:bg-slate-50/60 transition-colors"
|
||||||
@@ -145,14 +153,7 @@ export function PaymentsTable() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3.5">
|
<td className="px-5 py-3.5">
|
||||||
{row.state ? (
|
{row.state ? (
|
||||||
<span
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium text-emerald-600 bg-emerald-50">
|
||||||
onClick={() => {
|
|
||||||
if (row.state === 'unpaid') {
|
|
||||||
setIsPaymentOpen(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium text-emerald-600 bg-emerald-50"
|
|
||||||
>
|
|
||||||
<PaymentStatus status={row.state} />
|
<PaymentStatus status={row.state} />
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -162,25 +163,17 @@ export function PaymentsTable() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
);
|
))}
|
||||||
})}
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Download, CreditCard, Eye } from 'lucide-react';
|
import { Download, CreditCard, Eye } from 'lucide-react';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
@@ -9,8 +9,8 @@ import { apiRequest } from '@/shared/request/apiRequest';
|
|||||||
import { links } from '@/shared/request/links';
|
import { links } from '@/shared/request/links';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import type { SiDocument } from '../../lib/types';
|
import type { SiDocument } from '../../lib/types';
|
||||||
import { SiButton } from '@/features/modals/siModal/page';
|
|
||||||
import { useRouter, useParams } from 'next/navigation';
|
import { useRouter, useParams } from 'next/navigation';
|
||||||
|
import { Pagination } from '@/widgets/history/ui/pagination';
|
||||||
|
|
||||||
// ─── State badge ───────────────────────────────────────────────────────────────
|
// ─── State badge ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -181,9 +181,18 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
|||||||
|
|
||||||
// ─── SiTable ───────────────────────────────────────────────────────────────────
|
// ─── SiTable ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
export const SiTable: React.FC = () => {
|
export const SiTable: React.FC = () => {
|
||||||
const t = useTranslations('Cabinet');
|
const t = useTranslations('Cabinet');
|
||||||
const { items, isLoading, isError } = useSiHistory();
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -194,7 +203,6 @@ export const SiTable: React.FC = () => {
|
|||||||
{isLoading ? '...' : t('checksCount', { count: items.length })}
|
{isLoading ? '...' : t('checksCount', { count: items.length })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<SiButton />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
<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.length === 0 && <EmptyState />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
!isError &&
|
!isError &&
|
||||||
items.map((item, i) => (
|
pageItems.map((item, i) => (
|
||||||
<SiRow key={item.id} item={item} index={i + 1} />
|
<SiRow
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
index={(currentPage - 1) * PAGE_SIZE + i + 1}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import PaymentStatus from './paidStatus';
|
|||||||
import Sertifikat from '@/features/modals/sertificateModal/sertifikat';
|
import Sertifikat from '@/features/modals/sertificateModal/sertifikat';
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────────────
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
const baseUrl = 'https://dev-api.anti-plagiat.uz/api/v1';
|
const baseUrl = 'https://api.anti-plagiat.uz/api/v1';
|
||||||
interface AnalyzeText {
|
interface AnalyzeText {
|
||||||
[key: string]: number | string;
|
[key: string]: number | string;
|
||||||
}
|
}
|
||||||
@@ -399,10 +399,25 @@ export default function DocumentDetailPage({ id }: { id: number }) {
|
|||||||
<PaymentStatus status={doc.state} />
|
<PaymentStatus status={doc.state} />
|
||||||
<Sertifikat document_id={Number(id)} />
|
<Sertifikat document_id={Number(id)} />
|
||||||
{doc.file && (
|
{doc.file && (
|
||||||
<a
|
<button
|
||||||
href={`${baseUrl}${doc.file}`}
|
onClick={async () => {
|
||||||
target="_blank"
|
const url = `${baseUrl}/shared/documents/${doc.id}/download`;
|
||||||
rel="noopener noreferrer"
|
const res = await apiRequest<ArrayBuffer>(
|
||||||
|
'GET',
|
||||||
|
url,
|
||||||
|
undefined,
|
||||||
|
{ responseType: 'arraybuffer', baseURL: baseUrl },
|
||||||
|
);
|
||||||
|
const blob = new Blob([res.data], {
|
||||||
|
type: 'application/pdf',
|
||||||
|
});
|
||||||
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = objectUrl;
|
||||||
|
a.download = `document-${id}.pdf`;
|
||||||
|
a.click();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
|
||||||
|
}}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-900 text-white text-xs font-semibold hover:bg-slate-700 transition-colors"
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-900 text-white text-xs font-semibold hover:bg-slate-700 transition-colors"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -419,7 +434,7 @@ export default function DocumentDetailPage({ id }: { id: number }) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{t('downloadPdf')}
|
{t('downloadPdf')}
|
||||||
</a>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -486,7 +501,7 @@ export default function DocumentDetailPage({ id }: { id: number }) {
|
|||||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
|
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
|
||||||
<div className="flex flex-wrap justify-around gap-8">
|
<div className="flex flex-wrap justify-around gap-8">
|
||||||
<ScoreRing
|
<ScoreRing
|
||||||
value={res.ai}
|
value={res.originality}
|
||||||
label={t('scoreOriginality')}
|
label={t('scoreOriginality')}
|
||||||
color="#10b981"
|
color="#10b981"
|
||||||
/>
|
/>
|
||||||
@@ -502,7 +517,7 @@ export default function DocumentDetailPage({ id }: { id: number }) {
|
|||||||
color="#6366f1"
|
color="#6366f1"
|
||||||
/>
|
/>
|
||||||
<ScoreRing
|
<ScoreRing
|
||||||
value={res.originality}
|
value={res.ai}
|
||||||
label={t('scoreAiContent')}
|
label={t('scoreAiContent')}
|
||||||
color="#ef4444"
|
color="#ef4444"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,14 +36,15 @@ export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
const start = (currentPage - 1) * pageSize;
|
||||||
setState({
|
setState({
|
||||||
items: data?.results || [],
|
items: (data.results || []).slice(start, start + pageSize),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
setTotal(data.total || 0);
|
setTotal(data.total || 0);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data, currentPage, pageSize]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch();
|
refetch();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { apiRequest } from '@/shared/request/apiRequest';
|
|||||||
import { links } from '@/shared/request/links';
|
import { links } from '@/shared/request/links';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
|
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
|
||||||
|
import { PLAGIAT_SERVICE_FEE, SERTIFICATE_PRICE } from '@/shared/lib/metadata';
|
||||||
|
|
||||||
// ─── State badge ───────────────────────────────────────────────────────────────
|
// ─── State badge ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ export const HistoryTableRow: React.FC<
|
|||||||
const payment = useMutation({
|
const payment = useMutation({
|
||||||
mutationKey: ['payment', item.order_id],
|
mutationKey: ['payment', item.order_id],
|
||||||
mutationFn: ({ order_id }: { order_id: number }) =>
|
mutationFn: ({ order_id }: { order_id: number }) =>
|
||||||
apiRequest<{ payment_link: string }>('POST', links.payment(order_id)),
|
apiRequest<{ payment_link: string }>('POST', links.demo_pay(order_id)),
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
window.open(res.data.payment_link, '_self');
|
window.open(res.data.payment_link, '_self');
|
||||||
setIsPaymentOpen(false);
|
setIsPaymentOpen(false);
|
||||||
@@ -60,8 +61,9 @@ export const HistoryTableRow: React.FC<
|
|||||||
});
|
});
|
||||||
|
|
||||||
const price = item.price_calculation ?? {
|
const price = item.price_calculation ?? {
|
||||||
service_fee: 41200,
|
service_fee: item.state === 'unpaid' ? PLAGIAT_SERVICE_FEE : 0,
|
||||||
discount: 0,
|
discount: 0,
|
||||||
|
certificate: item.certificate ? SERTIFICATE_PRICE : 0,
|
||||||
total_price: 41200,
|
total_price: 41200,
|
||||||
currency: 'UZS',
|
currency: 'UZS',
|
||||||
};
|
};
|
||||||
@@ -142,6 +144,7 @@ export const HistoryTableRow: React.FC<
|
|||||||
payment.mutate({ order_id: Number(item.order_id) })
|
payment.mutate({ order_id: Number(item.order_id) })
|
||||||
}
|
}
|
||||||
isLoading={payment.isPending}
|
isLoading={payment.isPending}
|
||||||
|
hasSertificate={item.certificate}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const InfoSection: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ background: C.surfaceWarm }}>
|
<div id="info-section" style={{ background: C.surfaceWarm }}>
|
||||||
<Section style={{ paddingTop: 96, paddingBottom: 96 }}>
|
<Section style={{ paddingTop: 96, paddingBottom: 96 }}>
|
||||||
{/* Heading */}
|
{/* Heading */}
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const StepsSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
id="steps-section"
|
||||||
style={{
|
style={{
|
||||||
background: C.surface,
|
background: C.surface,
|
||||||
borderTop: `1px solid ${C.border}`,
|
borderTop: `1px solid ${C.border}`,
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
import Hero from './components/Hero';
|
import Hero from './components/Hero';
|
||||||
import InfoSection from './components/InfoSection';
|
import InfoSection from './components/InfoSection';
|
||||||
import StepsSection from './components/StepsSection';
|
import StepsSection from './components/StepsSection';
|
||||||
import Ticker from './components/Ticker';
|
import Ticker from './components/Ticker';
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
|
||||||
|
|
||||||
const PlagiarismLanding = () => {
|
const PlagiarismLanding = () => {
|
||||||
const route = useRouter();
|
const router = useRouter();
|
||||||
useEffect(() => {
|
|
||||||
const data = localStorage.getItem('user');
|
|
||||||
|
|
||||||
if (data) {
|
useEffect(() => {
|
||||||
route.push('/plagiat');
|
const token = localStorage.getItem('access_token');
|
||||||
|
const user = localStorage.getItem('user');
|
||||||
|
|
||||||
|
if (token && user) {
|
||||||
|
router.push('/plagiat');
|
||||||
}
|
}
|
||||||
}, []);
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero />
|
<Hero />
|
||||||
|
|||||||
@@ -1,26 +1,5 @@
|
|||||||
import { MenuItem } from './model';
|
|
||||||
import { LanguageRoutes } from '@/shared/config/i18n/types';
|
import { LanguageRoutes } from '@/shared/config/i18n/types';
|
||||||
|
|
||||||
const getMenu = (t: (key: string) => string): MenuItem[] => [
|
|
||||||
{ title: t('aboutSite'), url: '/about' },
|
|
||||||
// {
|
|
||||||
// title: 'Products',
|
|
||||||
// url: '#',
|
|
||||||
// items: [
|
|
||||||
// {
|
|
||||||
// title: 'Blog',
|
|
||||||
// description: 'The latest industry news, updates, and info',
|
|
||||||
// icon: Book,
|
|
||||||
// url: '#',
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: t('contact'),
|
|
||||||
url: '/contact',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const languages: { name: string; key: LanguageRoutes }[] = [
|
const languages: { name: string; key: LanguageRoutes }[] = [
|
||||||
{
|
{
|
||||||
name: "O'zbekcha",
|
name: "O'zbekcha",
|
||||||
@@ -36,4 +15,4 @@ const languages: { name: string; key: LanguageRoutes }[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export { getMenu, languages };
|
export { languages };
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export interface MenuItem {
|
|||||||
description?: string;
|
description?: string;
|
||||||
icon?: React.ComponentType<{ className?: string }>;
|
icon?: React.ComponentType<{ className?: string }>;
|
||||||
items?: MenuItem[];
|
items?: MenuItem[];
|
||||||
|
key: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import { MenuItem } from '../lib/model';
|
import { MenuItem } from '../lib/model';
|
||||||
|
|
||||||
const SubMenuLink = ({
|
const SubMenuLink = ({
|
||||||
@@ -7,11 +9,17 @@ const SubMenuLink = ({
|
|||||||
item: MenuItem;
|
item: MenuItem;
|
||||||
logOut?: () => void;
|
logOut?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isCabinet = pathname.includes('/cabinet');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="flex flex-row gap-4 rounded-md p-3 leading-none no-underline transition-colors outline-none select-none hover:bg-muted hover:text-accent-foreground"
|
className="flex flex-row gap-4 rounded-md p-3 leading-none no-underline transition-colors outline-none select-none hover:bg-muted hover:text-accent-foreground"
|
||||||
href={item.url}
|
href={isCabinet && item.url === '/cabinet' ? undefined : item.url}
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
if (isCabinet && item.url === '/cabinet') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
if (logOut) {
|
if (logOut) {
|
||||||
logOut();
|
logOut();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,56 +2,45 @@
|
|||||||
import { Link } from '@/shared/config/i18n/navigation';
|
import { Link } from '@/shared/config/i18n/navigation';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
DropdownMenu,
|
||||||
NavigationMenuContent,
|
DropdownMenuContent,
|
||||||
NavigationMenuItem,
|
DropdownMenuItem,
|
||||||
NavigationMenuLink,
|
DropdownMenuTrigger,
|
||||||
NavigationMenuList,
|
} from '@/shared/ui/dropdown-menu';
|
||||||
NavigationMenuTrigger,
|
|
||||||
} from '@/shared/ui/navigation-menu';
|
|
||||||
import SubMenuLink from './SubMenuLink';
|
|
||||||
import { ChangeLang } from './ChangeLang';
|
import { ChangeLang } from './ChangeLang';
|
||||||
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
|
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||||
import { LogOut, User } from 'lucide-react';
|
import { ChevronDown, LogOut } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
function AuthButtons() {
|
function AuthButtons() {
|
||||||
const t = useTranslations('Navbar');
|
const t = useTranslations('Navbar');
|
||||||
|
const [token, setToken] = useState<string | null>(null);
|
||||||
const [localUser, setLocalUser] = useState<{
|
const [localUser, setLocalUser] = useState<{
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
surname: string;
|
surname: string;
|
||||||
} | null>(null);
|
} | 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 },
|
|
||||||
{ title: t('logout'), url: '/', icon: LogOut },
|
|
||||||
];
|
|
||||||
|
|
||||||
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
||||||
const toggleRegisterModal = useRegisterModal(
|
const toggleRegisterModal = useRegisterModal(
|
||||||
(state) => state.toggleRegisterModal,
|
(state) => state.toggleRegisterModal,
|
||||||
);
|
);
|
||||||
const user = useUserPlagiatStore((state) => state.user);
|
const user = useUserPlagiatStore((state) => state.user);
|
||||||
const clearUser = useUserPlagiatStore((state) => state.clearUser);
|
const clearUser = useUserPlagiatStore((state) => state.clearUser);
|
||||||
|
|
||||||
const clearTokens = () => {
|
const clearTokens = () => {
|
||||||
localStorage.removeItem('access');
|
localStorage.removeItem('access_token');
|
||||||
localStorage.removeItem('refresh');
|
localStorage.removeItem('refresh_token');
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
clearUser();
|
clearUser();
|
||||||
};
|
};
|
||||||
console.log('Current user:', user);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const token_data = localStorage.getItem('access_token');
|
||||||
|
setToken(token_data);
|
||||||
const data = localStorage.getItem('user');
|
const data = localStorage.getItem('user');
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
setLocalUser(JSON.parse(data));
|
setLocalUser(JSON.parse(data));
|
||||||
} else {
|
} else {
|
||||||
@@ -59,54 +48,47 @@ function AuthButtons() {
|
|||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
if (localUser) {
|
if (localUser && token) {
|
||||||
return (
|
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">
|
<div className="sm:flex hidden">
|
||||||
<ChangeLang />
|
<ChangeLang />
|
||||||
</div>
|
</div>
|
||||||
<NavigationMenu viewport={true}>
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
<NavigationMenuList>
|
<DropdownMenuTrigger className="inline-flex items-center gap-1 text-lg font-medium outline-none">
|
||||||
<NavigationMenuItem>
|
{localUser.name}
|
||||||
<NavigationMenuTrigger className="text-lg">
|
<ChevronDown className="size-4" />
|
||||||
{localUser.name}
|
</DropdownMenuTrigger>
|
||||||
</NavigationMenuTrigger>
|
<DropdownMenuContent>
|
||||||
<NavigationMenuContent className="bg-popover text-popover-foreground">
|
<DropdownMenuItem
|
||||||
{userItem.map((subItem) => (
|
onClick={() => {
|
||||||
<NavigationMenuLink
|
clearTokens();
|
||||||
asChild
|
setOpen(false);
|
||||||
key={subItem.title}
|
}}
|
||||||
className="w-80"
|
>
|
||||||
>
|
<Link
|
||||||
<SubMenuLink
|
className="flex flex-row gap-4 rounded-md p-3 cursor-pointer"
|
||||||
logOut={() => {
|
href={'/'}
|
||||||
if (subItem.url !== '/cabinet') {
|
>
|
||||||
clearTokens();
|
<LogOut className="size-5 shrink-0 text-foreground" />
|
||||||
}
|
<span className="text-sm font-semibold">{t('logout')}</span>
|
||||||
}}
|
</Link>
|
||||||
item={subItem}
|
</DropdownMenuItem>
|
||||||
/>
|
</DropdownMenuContent>
|
||||||
</NavigationMenuLink>
|
</DropdownMenu>
|
||||||
))}
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
</NavigationMenuList>
|
|
||||||
</NavigationMenu>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="sm:flex hidden">
|
||||||
<ChangeLang />
|
<ChangeLang />
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" onClick={() => toggleLoginModal()}>
|
<Button variant="outline" onClick={() => toggleLoginModal()}>
|
||||||
<Link href={auth.login.url}>{auth.login.title}</Link>
|
{t('login')}
|
||||||
</Button>
|
|
||||||
<Button onClick={() => toggleRegisterModal()}>
|
|
||||||
<Link href={auth.signup.url}>{auth.signup.title}</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={() => toggleRegisterModal()}>{t('signup')}</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Accordion } from '@/shared/ui/accordion';
|
'use client';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
@@ -8,25 +8,49 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@/shared/ui/sheet';
|
} from '@/shared/ui/sheet';
|
||||||
import { Menu } from 'lucide-react';
|
import { Menu } from 'lucide-react';
|
||||||
import { getMenu } from '../lib/data';
|
|
||||||
import RenderMobileMenuItem from './RenderMobileMenuItem';
|
|
||||||
import { ChangeLang } from './ChangeLang';
|
import { ChangeLang } from './ChangeLang';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AuthButtons } from './authButtons';
|
import { AuthButtons } from './authButtons';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Logo_image } from '@/image';
|
import { Logo_image } from '@/image';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const t = useTranslations('Navbar');
|
const t = useTranslations('Navbar');
|
||||||
const menu = getMenu(t);
|
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 (
|
return (
|
||||||
<section className="py-1 flex items-center justify-center w-full ">
|
<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">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
||||||
{/* Desktop Menu */}
|
{/* Desktop Menu */}
|
||||||
<nav className="justify-between items-center flex max-sm:flex-col gap-5">
|
<nav className="justify-between items-center md:flex hidden max-sm:flex-col gap-5">
|
||||||
<div className="flex items-center justify-between gap-6">
|
<div className="flex items-center justify-around gap-2 w-full">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link
|
<Link
|
||||||
href={'/'}
|
href={'/'}
|
||||||
@@ -40,6 +64,36 @@ const Navbar = () => {
|
|||||||
height={10}
|
height={10}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</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">
|
<div className="flex sm:hidden items-center justify-center">
|
||||||
<ChangeLang />
|
<ChangeLang />
|
||||||
</div>
|
</div>
|
||||||
@@ -48,11 +102,20 @@ const Navbar = () => {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Mobile Menu */}
|
{/* Mobile Menu */}
|
||||||
<div className="hidden">
|
<div className="md:hidden flex">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between w-full">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link href={'/'} className="flex items-center gap-2">
|
<Link
|
||||||
{t('logo')}
|
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>
|
</Link>
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
@@ -66,19 +129,49 @@ const Navbar = () => {
|
|||||||
<SheetContent className="overflow-y-auto">
|
<SheetContent className="overflow-y-auto">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>
|
<SheetTitle>
|
||||||
<Link href={'/'} className="flex items-center gap-2">
|
<Link
|
||||||
{t('logo')}
|
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>
|
</Link>
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</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">
|
<div className="flex flex-col gap-6 p-4">
|
||||||
<Accordion
|
|
||||||
type="single"
|
|
||||||
collapsible
|
|
||||||
className="flex w-full flex-col gap-4"
|
|
||||||
>
|
|
||||||
{menu.map((item) => RenderMobileMenuItem(item))}
|
|
||||||
</Accordion>
|
|
||||||
<AuthButtons />
|
<AuthButtons />
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@@ -13,19 +13,22 @@ import { useMutation } from '@tanstack/react-query';
|
|||||||
import { links } from '@/shared/request/links';
|
import { links } from '@/shared/request/links';
|
||||||
import { apiRequest } from '@/shared/request/apiRequest';
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
import { PriceCalculate } from '@/features/modals/paymentModal/lib/types';
|
import { PriceCalculate } from '@/features/modals/paymentModal/lib/types';
|
||||||
|
import { SERTIFICATE_PRICE, PLAGIAT_SERVICE_FEE } from '@/shared/lib/metadata';
|
||||||
|
// import { fromTheme } from 'tailwind-merge';
|
||||||
|
|
||||||
// ─── Initial States ──────────────────────────────────────────────────────────
|
// ─── Initial States ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const INITIAL_FORM: PlagiarismFormState = {
|
const INITIAL_FORM: PlagiarismFormState = {
|
||||||
title: '',
|
title: '',
|
||||||
file: null,
|
file: null,
|
||||||
certificate: true,
|
certificate: false,
|
||||||
text: '',
|
text: '',
|
||||||
type: 0,
|
type: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PRICE: PriceCalculate = {
|
const PRICE: PriceCalculate = {
|
||||||
service_fee: 0,
|
service_fee: PLAGIAT_SERVICE_FEE,
|
||||||
|
certificate: SERTIFICATE_PRICE,
|
||||||
discount: 0,
|
discount: 0,
|
||||||
total_price: 0,
|
total_price: 0,
|
||||||
};
|
};
|
||||||
@@ -71,8 +74,8 @@ export function usePlagiarismForm() {
|
|||||||
const priceInfo: PriceCalculate = {
|
const priceInfo: PriceCalculate = {
|
||||||
total_price: resdata?.total_price || 0,
|
total_price: resdata?.total_price || 0,
|
||||||
discount: resdata?.discount || 0,
|
discount: resdata?.discount || 0,
|
||||||
certificate: resdata?.certificate || 0,
|
certificate: form.certificate ? SERTIFICATE_PRICE : 0,
|
||||||
service_fee: resdata?.service_fee || 0,
|
service_fee: PLAGIAT_SERVICE_FEE,
|
||||||
};
|
};
|
||||||
setPrices(priceInfo);
|
setPrices(priceInfo);
|
||||||
console.log('order_id:', resdata.id);
|
console.log('order_id:', resdata.id);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { usePlagiarismForm } from '../lib/usePlagiraism';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
|
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
|
||||||
import DocumentsTypes from './documentsType';
|
import DocumentsTypes from './documentsType';
|
||||||
|
import { PLAGIAT_SERVICE_FEE } from '@/shared/lib/metadata';
|
||||||
|
|
||||||
export const inputCls = `
|
export const inputCls = `
|
||||||
w-full px-3.5 py-3.5 text-[14px] text-slate-800
|
w-full px-3.5 py-3.5 text-[14px] text-slate-800
|
||||||
@@ -113,7 +114,7 @@ export function PlagiarismCheckForm() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* left part */}
|
{/* 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 */}
|
{/* Topic */}
|
||||||
<FieldWrapper
|
<FieldWrapper
|
||||||
label={t('documentTopic')}
|
label={t('documentTopic')}
|
||||||
@@ -131,6 +132,9 @@ export function PlagiarismCheckForm() {
|
|||||||
maxLength={200}
|
maxLength={200}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-sm text-stone-500 ml-2 ">
|
||||||
|
{t('service_price', { PLAGIAT_SERVICE_FEE })}
|
||||||
|
</p>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
|
|
||||||
{/* Sender Full Name (read-only) */}
|
{/* Sender Full Name (read-only) */}
|
||||||
@@ -209,6 +213,7 @@ export function PlagiarismCheckForm() {
|
|||||||
price={prices}
|
price={prices}
|
||||||
onConfirmPayment={handleSubmit}
|
onConfirmPayment={handleSubmit}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
hasSertificate={!!form.certificate}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { SERTIFICATE_PRICE } from '@/shared/lib/metadata';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
// ─── FieldWrapper ────────────────────────────────────────────────────────────
|
// ─── FieldWrapper ────────────────────────────────────────────────────────────
|
||||||
@@ -219,6 +221,7 @@ export function CertificateCheckbox({
|
|||||||
title = 'Return result with certificate',
|
title = 'Return result with certificate',
|
||||||
description = 'An official certificate will be attached to your originality report.',
|
description = 'An official certificate will be attached to your originality report.',
|
||||||
}: CertificateCheckboxProps) {
|
}: CertificateCheckboxProps) {
|
||||||
|
const t = useTranslations('PlagiarismCheck');
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={`
|
className={`
|
||||||
@@ -260,6 +263,9 @@ export function CertificateCheckbox({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-stone-800">{title}</p>
|
<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>
|
<p className="text-xs text-stone-500 mt-0.5">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user