profile page ui complated
This commit is contained in:
100
package-lock.json
generated
100
package-lock.json
generated
@@ -35,6 +35,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.38.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.503.0",
|
||||||
"next": "15.5.9",
|
"next": "15.5.9",
|
||||||
"next-intl": "^4.3.9",
|
"next-intl": "^4.3.9",
|
||||||
@@ -4965,6 +4966,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -6570,6 +6577,12 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@@ -6597,6 +6610,12 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||||
@@ -7165,6 +7184,18 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jszip": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||||
|
"license": "(MIT OR GPL-3.0-or-later)",
|
||||||
|
"dependencies": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"setimmediate": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -7209,6 +7240,15 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||||
@@ -8160,6 +8200,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -8314,6 +8360,12 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -8505,6 +8557,27 @@
|
|||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream/node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/recharts": {
|
"node_modules/recharts": {
|
||||||
"version": "2.15.4",
|
"version": "2.15.4",
|
||||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
|
||||||
@@ -8713,6 +8786,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-json-stringify": {
|
"node_modules/safe-json-stringify": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
|
||||||
@@ -8822,6 +8901,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/setimmediate": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.34.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
@@ -9049,6 +9134,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-argv": {
|
"node_modules/string-argv": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
|
||||||
@@ -9680,6 +9774,12 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vaul": {
|
"node_modules/vaul": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.38.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.503.0",
|
||||||
"next": "15.5.9",
|
"next": "15.5.9",
|
||||||
"next-intl": "^4.3.9",
|
"next-intl": "^4.3.9",
|
||||||
|
|||||||
@@ -1,57 +1,84 @@
|
|||||||
<?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">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://antiplagiat.uz/uz</loc>
|
<loc>https://anti-plagiat.uz/uz</loc>
|
||||||
<lastmod>2026-04-04</lastmod>
|
<lastmod>2026-04-06</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://antiplagiat.uz/uz"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz"/>
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://antiplagiat.uz/ru"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru"/>
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://antiplagiat.uz/en"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en"/>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://antiplagiat.uz/uz/plagat</loc>
|
<loc>https://anti-plagiat.uz/uz/plagat</loc>
|
||||||
<lastmod>2026-04-04</lastmod>
|
<lastmod>2026-04-06</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://antiplagiat.uz/uz/plagat"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/plagat"/>
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://antiplagiat.uz/ru/plagat"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/plagat"/>
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://antiplagiat.uz/en/plagat"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/plagat"/>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://antiplagiat.uz/ru</loc>
|
<loc>https://anti-plagiat.uz/uz/cabinet</loc>
|
||||||
<lastmod>2026-04-04</lastmod>
|
<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>
|
||||||
|
<loc>https://anti-plagiat.uz/ru</loc>
|
||||||
|
<lastmod>2026-04-06</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://antiplagiat.uz/uz"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz"/>
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://antiplagiat.uz/ru"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru"/>
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://antiplagiat.uz/en"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en"/>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://antiplagiat.uz/ru/plagat</loc>
|
<loc>https://anti-plagiat.uz/ru/plagat</loc>
|
||||||
<lastmod>2026-04-04</lastmod>
|
<lastmod>2026-04-06</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://antiplagiat.uz/uz/plagat"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/plagat"/>
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://antiplagiat.uz/ru/plagat"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/plagat"/>
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://antiplagiat.uz/en/plagat"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/plagat"/>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://antiplagiat.uz/en</loc>
|
<loc>https://anti-plagiat.uz/ru/cabinet</loc>
|
||||||
<lastmod>2026-04-04</lastmod>
|
<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>
|
||||||
|
<loc>https://anti-plagiat.uz/en</loc>
|
||||||
|
<lastmod>2026-04-06</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://antiplagiat.uz/uz"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz"/>
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://antiplagiat.uz/ru"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru"/>
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://antiplagiat.uz/en"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en"/>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://antiplagiat.uz/en/plagat</loc>
|
<loc>https://anti-plagiat.uz/en/plagat</loc>
|
||||||
<lastmod>2026-04-04</lastmod>
|
<lastmod>2026-04-06</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
<xhtml:link rel="alternate" hreflang="uz" href="https://antiplagiat.uz/uz/plagat"/>
|
<xhtml:link rel="alternate" hreflang="uz" href="https://anti-plagiat.uz/uz/plagat"/>
|
||||||
<xhtml:link rel="alternate" hreflang="ru" href="https://antiplagiat.uz/ru/plagat"/>
|
<xhtml:link rel="alternate" hreflang="ru" href="https://anti-plagiat.uz/ru/plagat"/>
|
||||||
<xhtml:link rel="alternate" hreflang="en" href="https://antiplagiat.uz/en/plagat"/>
|
<xhtml:link rel="alternate" hreflang="en" href="https://anti-plagiat.uz/en/plagat"/>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://anti-plagiat.uz/en/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>
|
||||||
</urlset>
|
</urlset>
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://antiplagiat.uz';
|
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://anti-plagiat.uz';
|
||||||
const LOCALES = ['uz', 'ru', 'en'] as const;
|
const LOCALES = ['uz', 'ru', 'en'] as const;
|
||||||
|
|
||||||
// Add your static page slugs here
|
const STATIC_ROUTES = [
|
||||||
const STATIC_ROUTES = ['', '/plagat'];
|
{ path: '', changeFreq: 'daily' as const, priority: 1.0 },
|
||||||
|
{ path: '/plagat', changeFreq: 'weekly' as const, priority: 0.8 },
|
||||||
|
{ path: '/cabinet', changeFreq: 'weekly' as const, priority: 0.7 },
|
||||||
|
];
|
||||||
|
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
const entries: MetadataRoute.Sitemap = [];
|
const entries: MetadataRoute.Sitemap = [];
|
||||||
@@ -12,13 +15,13 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
for (const locale of LOCALES) {
|
for (const locale of LOCALES) {
|
||||||
for (const route of STATIC_ROUTES) {
|
for (const route of STATIC_ROUTES) {
|
||||||
entries.push({
|
entries.push({
|
||||||
url: `${SITE_URL}/${locale}${route}`,
|
url: `${SITE_URL}/${locale}${route.path}`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: route === '' ? 'daily' : 'weekly',
|
changeFrequency: route.changeFreq,
|
||||||
priority: route === '' ? 1.0 : 0.8,
|
priority: route.priority,
|
||||||
alternates: {
|
alternates: {
|
||||||
languages: Object.fromEntries(
|
languages: Object.fromEntries(
|
||||||
LOCALES.map((l) => [l, `${SITE_URL}/${l}${route}`]),
|
LOCALES.map((l) => [l, `${SITE_URL}/${l}${route.path}`]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
14
src/features/modals/siModal/index.ts
Normal file
14
src/features/modals/siModal/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export { FileUploadModal } from './ui/fileUploadModal';
|
||||||
|
export { useFileUpload } from './utils/useFileUpload';
|
||||||
|
export {
|
||||||
|
countWordsFromFile,
|
||||||
|
SUPPORTED_EXTENSIONS,
|
||||||
|
SUPPORTED_MIME_TYPES,
|
||||||
|
} from './utils/wordCount';
|
||||||
|
export { calculatePrice, formatPrice, DEFAULT_PRICING } from './utils/pricing';
|
||||||
|
export type {
|
||||||
|
FileUploadModalProps,
|
||||||
|
UploadedFile,
|
||||||
|
PricingConfig,
|
||||||
|
WordCountResult,
|
||||||
|
} from './utils/tyeps';
|
||||||
52
src/features/modals/siModal/page.tsx
Normal file
52
src/features/modals/siModal/page.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ArrowRight, BrainCircuit } from 'lucide-react';
|
||||||
|
import { FileUploadModal } from './ui/fileUploadModal';
|
||||||
|
|
||||||
|
export default function SiCTACard() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [lastSubmission, setLastSubmission] = useState<{
|
||||||
|
name: string;
|
||||||
|
words: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const handleSubmit = (
|
||||||
|
documentName: string,
|
||||||
|
_file: File,
|
||||||
|
wordCount: number,
|
||||||
|
) => {
|
||||||
|
// Here you would send the file to your backend for plagiarism check.
|
||||||
|
// The word count is already computed client-side for instant pricing display.
|
||||||
|
console.log(lastSubmission);
|
||||||
|
console.log('Submitting:', { documentName, wordCount });
|
||||||
|
setLastSubmission({ name: documentName, words: wordCount });
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-violet-500 to-violet-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
|
||||||
|
>
|
||||||
|
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
|
||||||
|
<BrainCircuit size={72} className="text-white" />
|
||||||
|
</div>
|
||||||
|
<BrainCircuit size={26} className="text-white mb-4" />
|
||||||
|
<h3 className="text-white font-semibold text-base mb-1">SI detektor</h3>
|
||||||
|
<p className="text-violet-100 text-sm mb-4 leading-relaxed">
|
||||||
|
Matnni sun'iy intellekt uchun tekshiring
|
||||||
|
</p>
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors">
|
||||||
|
Yuborish <ArrowRight size={12} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<FileUploadModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
src/features/modals/siModal/ui/dropZone.tsx
Normal file
67
src/features/modals/siModal/ui/dropZone.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Upload } from 'lucide-react';
|
||||||
|
import { SUPPORTED_EXTENSIONS } from '../utils/wordCount';
|
||||||
|
|
||||||
|
interface DropZoneProps {
|
||||||
|
isDragging: boolean;
|
||||||
|
onDrop: (e: React.DragEvent) => void;
|
||||||
|
onDragOver: (e: React.DragEvent) => void;
|
||||||
|
onDragLeave: () => void;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DropZone({
|
||||||
|
isDragging,
|
||||||
|
onDrop,
|
||||||
|
onDragOver,
|
||||||
|
onDragLeave,
|
||||||
|
onClick,
|
||||||
|
}: DropZoneProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={onClick}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && onClick()}
|
||||||
|
className={`
|
||||||
|
relative flex flex-col items-center justify-center gap-3
|
||||||
|
rounded-xl border-2 border-dashed px-6 py-2
|
||||||
|
cursor-pointer select-none transition-all duration-200 outline-none
|
||||||
|
focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-2
|
||||||
|
${
|
||||||
|
isDragging
|
||||||
|
? 'border-blue-400 bg-blue-50 scale-[1.01]'
|
||||||
|
: 'border-slate-200 bg-slate-50 hover:border-blue-300 hover:bg-blue-50/50'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
w-12 h-12 rounded-full flex items-center justify-center
|
||||||
|
transition-all duration-200
|
||||||
|
${isDragging ? 'bg-blue-100 scale-110' : 'bg-white shadow-sm'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
size={22}
|
||||||
|
className={`transition-colors duration-200 ${
|
||||||
|
isDragging ? 'text-blue-500' : 'text-slate-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-medium text-slate-700">
|
||||||
|
{isDragging ? 'Drop file here' : 'Drag & drop or click to browse'}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-slate-400">
|
||||||
|
Supported: {SUPPORTED_EXTENSIONS.join(', ')} · Max 50 MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
199
src/features/modals/siModal/ui/fileUploadModal.tsx
Normal file
199
src/features/modals/siModal/ui/fileUploadModal.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { calculatePrice, DEFAULT_PRICING, formatPrice } from '../utils/pricing';
|
||||||
|
import { FileUploadModalProps } from '../utils/tyeps';
|
||||||
|
import { useFileUpload } from '../utils/useFileUpload';
|
||||||
|
import { SUPPORTED_EXTENSIONS } from '../utils/wordCount';
|
||||||
|
import { ErrorBanner, FileChip, PricingInfo, Spinner } from './modalParts';
|
||||||
|
import { DropZone } from './dropZone';
|
||||||
|
|
||||||
|
export function FileUploadModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
pricing = DEFAULT_PRICING,
|
||||||
|
}: FileUploadModalProps) {
|
||||||
|
const {
|
||||||
|
documentName,
|
||||||
|
setDocumentName,
|
||||||
|
uploadedFile,
|
||||||
|
isDragging,
|
||||||
|
isProcessing,
|
||||||
|
error,
|
||||||
|
fileInputRef,
|
||||||
|
handleFileSelect,
|
||||||
|
handleDrop,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragLeave,
|
||||||
|
handleRemoveFile,
|
||||||
|
openFilePicker,
|
||||||
|
} = useFileUpload();
|
||||||
|
|
||||||
|
// Close on Escape key
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose();
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
// Lock body scroll while open
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = isOpen ? 'hidden' : '';
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const canSubmit =
|
||||||
|
documentName.trim().length > 0 &&
|
||||||
|
uploadedFile?.status === 'done' &&
|
||||||
|
!isProcessing;
|
||||||
|
|
||||||
|
const wordCount = uploadedFile?.wordCount ?? 0;
|
||||||
|
const totalPrice = calculatePrice(wordCount, pricing);
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!canSubmit || !uploadedFile) return;
|
||||||
|
onSubmit(documentName.trim(), uploadedFile.file, wordCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
// Backdrop
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
fixed inset-0 z-50 flex items-center justify-center p-4
|
||||||
|
bg-slate-900/40 backdrop-blur-sm
|
||||||
|
animate-in fade-in duration-200
|
||||||
|
"
|
||||||
|
onClick={(e) => e.target === e.currentTarget && onClose()}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
>
|
||||||
|
{/* Panel */}
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
relative w-full max-w-140 rounded-2xl bg-white shadow-2xl
|
||||||
|
animate-in zoom-in-95 slide-in-from-bottom-4 duration-300
|
||||||
|
flex flex-col gap-5 p-6
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2
|
||||||
|
id="modal-title"
|
||||||
|
className="text-xl font-semibold text-slate-800 tracking-tight"
|
||||||
|
>
|
||||||
|
Select file
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="
|
||||||
|
w-8 h-8 rounded-full flex items-center justify-center
|
||||||
|
text-slate-400 hover:text-slate-600 hover:bg-slate-100
|
||||||
|
transition-colors focus-visible:outline-none focus-visible:ring-2
|
||||||
|
focus-visible:ring-slate-300
|
||||||
|
"
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Document name */}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Hidden file input */}
|
||||||
|
<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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Drop zone OR file chip */}
|
||||||
|
{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 */}
|
||||||
|
{error && <ErrorBanner message={error} />}
|
||||||
|
|
||||||
|
{/* Pricing info — shown once word count is ready */}
|
||||||
|
{uploadedFile?.status === 'done' && wordCount > 0 && (
|
||||||
|
<PricingInfo
|
||||||
|
wordCount={wordCount}
|
||||||
|
minimumPrice={formatPrice(pricing.minimumPayment)}
|
||||||
|
totalPrice={formatPrice(totalPrice)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
src/features/modals/siModal/ui/modalParts.tsx
Normal file
126
src/features/modals/siModal/ui/modalParts.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { X, FileText, Loader2, AlertCircle } from 'lucide-react';
|
||||||
|
import { UploadedFile } from '../utils/tyeps';
|
||||||
|
|
||||||
|
// ── Spinner ─────────────────────────────────────────────────────────────────
|
||||||
|
export function Spinner({ className = '' }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<Loader2
|
||||||
|
className={`animate-spin text-white ${className}`}
|
||||||
|
size={16}
|
||||||
|
aria-label="Processing…"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── File chip shown after upload ─────────────────────────────────────────────
|
||||||
|
interface FileChipProps {
|
||||||
|
file: UploadedFile;
|
||||||
|
onRemove: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileChip({ file, onRemove }: FileChipProps) {
|
||||||
|
const isLoading = file.status === 'uploading';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
flex items-center justify-between rounded-xl px-4 py-3 transition-all duration-300
|
||||||
|
${
|
||||||
|
file.status === 'error'
|
||||||
|
? 'bg-red-500/10 border border-red-400/30'
|
||||||
|
: 'bg-emerald-500 shadow-lg shadow-emerald-500/25'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<FileText
|
||||||
|
size={18}
|
||||||
|
className={file.status === 'error' ? 'text-red-400' : 'text-white/80'}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p
|
||||||
|
className={`text-sm font-medium truncate max-w-65 ${
|
||||||
|
file.status === 'error' ? 'text-red-300' : 'text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className={`text-xs ${
|
||||||
|
file.status === 'error' ? 'text-red-400' : 'text-emerald-100/70'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{file.sizeKB} KB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 shrink-0">
|
||||||
|
{isLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`text-xs font-medium ${
|
||||||
|
file.status === 'error' ? 'text-red-400' : 'text-white/90'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{file.status === 'error' ? 'Error' : 'Upload complete'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={onRemove}
|
||||||
|
className="w-6 h-6 rounded-full bg-white/20 hover:bg-white/30 flex items-center justify-center transition-colors"
|
||||||
|
aria-label="Remove file"
|
||||||
|
>
|
||||||
|
<X size={12} className="text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pricing info block ───────────────────────────────────────────────────────
|
||||||
|
interface PricingInfoProps {
|
||||||
|
wordCount: number;
|
||||||
|
minimumPrice: string;
|
||||||
|
totalPrice: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PricingInfo({
|
||||||
|
wordCount,
|
||||||
|
minimumPrice,
|
||||||
|
totalPrice,
|
||||||
|
}: PricingInfoProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
animate-in fade-in slide-in-from-bottom-2 duration-300
|
||||||
|
rounded-xl bg-amber-50 border border-amber-100 px-4 py-3 space-y-1
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed">
|
||||||
|
Document check price is determined by the number of words in the
|
||||||
|
document.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-semibold text-amber-500">
|
||||||
|
Minimum payment for one document: {minimumPrice}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-semibold text-emerald-600">
|
||||||
|
Document check price: {wordCount.toLocaleString()} words for{' '}
|
||||||
|
{totalPrice}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Error banner ─────────────────────────────────────────────────────────────
|
||||||
|
export function ErrorBanner({ message }: { message: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-2 rounded-xl bg-red-50 border border-red-200 px-4 py-3 animate-in fade-in duration-200">
|
||||||
|
<AlertCircle size={16} className="text-red-500 mt-0.5 shrink-0" />
|
||||||
|
<p className="text-sm text-red-600">{message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/features/modals/siModal/utils/pricing.ts
Normal file
20
src/features/modals/siModal/utils/pricing.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { PricingConfig } from './tyeps';
|
||||||
|
|
||||||
|
const DEFAULT_PRICING: PricingConfig = {
|
||||||
|
pricePerWord: 4, // 4 so'm per word
|
||||||
|
minimumPayment: 10000, // 10 000 so'm minimum
|
||||||
|
};
|
||||||
|
|
||||||
|
export function calculatePrice(
|
||||||
|
wordCount: number,
|
||||||
|
config: PricingConfig = DEFAULT_PRICING,
|
||||||
|
): number {
|
||||||
|
const calculated = wordCount * config.pricePerWord;
|
||||||
|
return Math.max(calculated, config.minimumPayment);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPrice(amount: number): string {
|
||||||
|
return new Intl.NumberFormat('uz-UZ').format(amount) + " so'm";
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DEFAULT_PRICING };
|
||||||
24
src/features/modals/siModal/utils/tyeps.ts
Normal file
24
src/features/modals/siModal/utils/tyeps.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export interface UploadedFile {
|
||||||
|
file: File;
|
||||||
|
name: string;
|
||||||
|
sizeKB: number;
|
||||||
|
wordCount: number;
|
||||||
|
status: 'uploading' | 'done' | 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PricingConfig {
|
||||||
|
pricePerWord: number; // in so'm
|
||||||
|
minimumPayment: number; // in so'm
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileUploadModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (documentName: string, file: File, wordCount: number) => void;
|
||||||
|
pricing?: PricingConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WordCountResult {
|
||||||
|
count: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
128
src/features/modals/siModal/utils/useFileUpload.ts
Normal file
128
src/features/modals/siModal/utils/useFileUpload.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback, useRef } from 'react';
|
||||||
|
import { UploadedFile } from './tyeps';
|
||||||
|
import { countWordsFromFile, SUPPORTED_EXTENSIONS } from './wordCount';
|
||||||
|
|
||||||
|
interface UseFileUploadReturn {
|
||||||
|
documentName: string;
|
||||||
|
setDocumentName: (name: string) => void;
|
||||||
|
uploadedFile: UploadedFile | null;
|
||||||
|
isDragging: boolean;
|
||||||
|
isProcessing: boolean;
|
||||||
|
error: string | null;
|
||||||
|
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
handleFileSelect: (file: File) => Promise<void>;
|
||||||
|
handleDrop: (e: React.DragEvent) => void;
|
||||||
|
handleDragOver: (e: React.DragEvent) => void;
|
||||||
|
handleDragLeave: () => void;
|
||||||
|
handleRemoveFile: () => void;
|
||||||
|
openFilePicker: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFileUpload(): UseFileUploadReturn {
|
||||||
|
const [documentName, setDocumentName] = useState('');
|
||||||
|
const [uploadedFile, setUploadedFile] = useState<UploadedFile | null>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const validateFile = (file: File): string | null => {
|
||||||
|
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||||
|
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
||||||
|
return `Unsupported file type. Allowed: ${SUPPORTED_EXTENSIONS.join(', ')}`;
|
||||||
|
}
|
||||||
|
if (file.size > 50 * 1024 * 1024) {
|
||||||
|
return 'File size must be less than 50 MB';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = useCallback(async (file: File) => {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const validationError = validateFile(file);
|
||||||
|
if (validationError) {
|
||||||
|
setError(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimistic UI: show file immediately
|
||||||
|
const optimistic: UploadedFile = {
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
sizeKB: Math.round(file.size / 1024),
|
||||||
|
wordCount: 0,
|
||||||
|
status: 'uploading',
|
||||||
|
};
|
||||||
|
setUploadedFile(optimistic);
|
||||||
|
setIsProcessing(true);
|
||||||
|
|
||||||
|
// Auto-fill document name if empty
|
||||||
|
setDocumentName((prev) =>
|
||||||
|
prev.trim() === '' ? file.name.replace(/\.[^/.]+$/, '') : prev,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Count words on the frontend (no round-trip needed)
|
||||||
|
const result = await countWordsFromFile(file);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
setError(result.error);
|
||||||
|
setUploadedFile({ ...optimistic, status: 'error', wordCount: 0 });
|
||||||
|
} else {
|
||||||
|
setUploadedFile({
|
||||||
|
...optimistic,
|
||||||
|
status: 'done',
|
||||||
|
wordCount: result.count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDrop = useCallback(
|
||||||
|
(e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
if (file) handleFileSelect(file);
|
||||||
|
},
|
||||||
|
[handleFileSelect],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragLeave = useCallback(() => {
|
||||||
|
setIsDragging(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRemoveFile = useCallback(() => {
|
||||||
|
setUploadedFile(null);
|
||||||
|
setError(null);
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openFilePicker = useCallback(() => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
documentName,
|
||||||
|
setDocumentName,
|
||||||
|
uploadedFile,
|
||||||
|
isDragging,
|
||||||
|
isProcessing,
|
||||||
|
error,
|
||||||
|
fileInputRef,
|
||||||
|
handleFileSelect,
|
||||||
|
handleDrop,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragLeave,
|
||||||
|
handleRemoveFile,
|
||||||
|
openFilePicker,
|
||||||
|
};
|
||||||
|
}
|
||||||
93
src/features/modals/siModal/utils/wordCount.ts
Normal file
93
src/features/modals/siModal/utils/wordCount.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { WordCountResult } from './tyeps';
|
||||||
|
|
||||||
|
// ── Plain text ──────────────────────────────────────────────────────────────
|
||||||
|
function countWordsFromText(text: string): number {
|
||||||
|
return text
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((word) => word.length > 0).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── .txt ────────────────────────────────────────────────────────────────────
|
||||||
|
async function countFromTxt(file: File): Promise<WordCountResult> {
|
||||||
|
const text = await file.text();
|
||||||
|
return { count: countWordsFromText(text) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── .docx (reads raw XML, strips tags) ─────────────────────────────────────
|
||||||
|
async function countFromDocx(file: File): Promise<WordCountResult> {
|
||||||
|
try {
|
||||||
|
const { default: JSZip } = await import('jszip');
|
||||||
|
const zip = await JSZip.loadAsync(await file.arrayBuffer());
|
||||||
|
const xmlFile = zip.file('word/document.xml');
|
||||||
|
|
||||||
|
if (!xmlFile) {
|
||||||
|
return { count: 0, error: 'Could not read .docx content' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const xml = await xmlFile.async('string');
|
||||||
|
const text = xml
|
||||||
|
.replace(/<[^>]+>/g, ' ') // strip XML tags
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return { count: countWordsFromText(text) };
|
||||||
|
} catch {
|
||||||
|
return { count: 0, error: 'Failed to parse .docx file' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── .pdf (reads raw text layer; won't work for scanned PDFs) ───────────────
|
||||||
|
async function countFromPdf(file: File): Promise<WordCountResult> {
|
||||||
|
try {
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const raw = new TextDecoder('latin1').decode(bytes);
|
||||||
|
|
||||||
|
// Extract text between BT/ET stream operators
|
||||||
|
const textMatches = raw.match(/BT[\s\S]*?ET/g) ?? [];
|
||||||
|
const words: string[] = [];
|
||||||
|
|
||||||
|
for (const block of textMatches) {
|
||||||
|
// Match Tj / TJ operators
|
||||||
|
const strings = block.match(/\(([^)]*)\)\s*Tj|\[([^\]]*)\]\s*TJ/g) ?? [];
|
||||||
|
for (const s of strings) {
|
||||||
|
const inner = s.replace(/\(|\)\s*Tj|\[|\]\s*TJ/g, '');
|
||||||
|
words.push(...inner.split(/\s+/).filter((w) => w.length > 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count: words.length };
|
||||||
|
} catch {
|
||||||
|
return { count: 0, error: 'Failed to parse .pdf file' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public API ──────────────────────────────────────────────────────────────
|
||||||
|
export async function countWordsFromFile(file: File): Promise<WordCountResult> {
|
||||||
|
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case 'txt':
|
||||||
|
return countFromTxt(file);
|
||||||
|
case 'docx':
|
||||||
|
return countFromDocx(file);
|
||||||
|
case 'pdf':
|
||||||
|
return countFromPdf(file);
|
||||||
|
default:
|
||||||
|
// Fallback: try reading as text
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
return { count: countWordsFromText(text) };
|
||||||
|
} catch {
|
||||||
|
return { count: 0, error: `Unsupported file type: .${ext}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SUPPORTED_EXTENSIONS = ['.txt', '.docx', '.pdf'];
|
||||||
|
export const SUPPORTED_MIME_TYPES = [
|
||||||
|
'text/plain',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/pdf',
|
||||||
|
];
|
||||||
@@ -145,7 +145,7 @@ function NavigationMenuIndicator({
|
|||||||
<NavigationMenuPrimitive.Indicator
|
<NavigationMenuPrimitive.Indicator
|
||||||
data-slot="navigation-menu-indicator"
|
data-slot="navigation-menu-indicator"
|
||||||
className={cn(
|
className={cn(
|
||||||
'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
|
'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
43
src/widgets/cabinet/ui/CabinetNav.tsx
Normal file
43
src/widgets/cabinet/ui/CabinetNav.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import { Menu } from 'lucide-react';
|
||||||
|
import type { CabinetSection } from '../lib/types';
|
||||||
|
|
||||||
|
// ─── Labels ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const SECTION_LABELS: Record<CabinetSection, string> = {
|
||||||
|
dashboard: 'Dashboard',
|
||||||
|
plagiat: 'Plagiat tekshiruvlar',
|
||||||
|
si: 'SI detektor',
|
||||||
|
payments: "To'lovlar tarixi",
|
||||||
|
profile: 'Profil',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Props ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface CabinetNavProps {
|
||||||
|
activeSection: CabinetSection;
|
||||||
|
onMenuClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const CabinetNav: React.FC<CabinetNavProps> = ({
|
||||||
|
activeSection,
|
||||||
|
onMenuClick,
|
||||||
|
}) => (
|
||||||
|
<header className="h-14 px-4 md:px-6 flex lg:hidden items-center justify-between border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-20">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onMenuClick}
|
||||||
|
className="lg:hidden p-2 rounded-lg text-slate-500 hover:bg-slate-100 transition-colors"
|
||||||
|
aria-label="Menu"
|
||||||
|
>
|
||||||
|
<Menu size={18} />
|
||||||
|
</button>
|
||||||
|
<h1 className="text-sm font-semibold text-slate-800">
|
||||||
|
{SECTION_LABELS[activeSection]}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
@@ -140,6 +140,13 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-4 py-4 border-t border-slate-100">
|
||||||
|
<p className="text-[11px] text-slate-400 text-center">
|
||||||
|
© 2026 Plagat.uz
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FileSearch, BrainCircuit, ArrowRight } from 'lucide-react';
|
import { FileSearch, ArrowRight } from 'lucide-react';
|
||||||
import type { CabinetSection } from '../../lib/types';
|
import Link from 'next/link';
|
||||||
|
import SiCTACard from '@/features/modals/siModal/page';
|
||||||
|
|
||||||
interface CtaCardsProps {
|
export const CtaCards = () => (
|
||||||
onNavigate: (section: CabinetSection) => void;
|
<>
|
||||||
}
|
|
||||||
|
|
||||||
export const CtaCards: React.FC<CtaCardsProps> = ({ onNavigate }) => (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{/* Plagiat */}
|
{/* Plagiat */}
|
||||||
<button
|
<Link
|
||||||
onClick={() => onNavigate('plagiat')}
|
href={'/plagat'}
|
||||||
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-blue-500 to-blue-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
|
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-blue-500 to-blue-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
|
||||||
>
|
>
|
||||||
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
|
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
|
||||||
@@ -26,24 +24,10 @@ export const CtaCards: React.FC<CtaCardsProps> = ({ onNavigate }) => (
|
|||||||
<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">
|
||||||
Yuborish <ArrowRight size={12} />
|
Yuborish <ArrowRight size={12} />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Link>
|
||||||
|
|
||||||
{/* SI */}
|
{/* SI */}
|
||||||
<button
|
<SiCTACard />
|
||||||
onClick={() => onNavigate('si')}
|
|
||||||
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-violet-500 to-violet-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
|
|
||||||
>
|
|
||||||
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
|
|
||||||
<BrainCircuit size={72} className="text-white" />
|
|
||||||
</div>
|
|
||||||
<BrainCircuit size={26} className="text-white mb-4" />
|
|
||||||
<h3 className="text-white font-semibold text-base mb-1">SI detektor</h3>
|
|
||||||
<p className="text-violet-100 text-sm mb-4 leading-relaxed">
|
|
||||||
Matnni sun'iy intellekt uchun tekshiring
|
|
||||||
</p>
|
|
||||||
<span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors">
|
|
||||||
Yuborish <ArrowRight size={12} />
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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 { 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 {
|
import {
|
||||||
@@ -91,7 +92,7 @@ export const CabinetLayout: React.FC = () => {
|
|||||||
const fullName = `${MOCK_USER.name} ${MOCK_USER.surname}`;
|
const fullName = `${MOCK_USER.name} ${MOCK_USER.surname}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex border-t min-h-screen">
|
<div className="flex bg-slate-50 min-h-screen">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
active={activeSection}
|
active={activeSection}
|
||||||
onNavigate={navigate}
|
onNavigate={navigate}
|
||||||
@@ -101,6 +102,8 @@ export const CabinetLayout: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<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">
|
<main className="flex-1 p-4 md:p-6 lg:p-8 max-w-6xl mx-auto w-full">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.div key={activeSection} {...FADE}>
|
<motion.div key={activeSection} {...FADE}>
|
||||||
|
|||||||
Reference in New Issue
Block a user