profile page ui complated

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-06 17:55:54 +05:00
parent 27b1510842
commit db0fad7e00
18 changed files with 976 additions and 85 deletions

100
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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>

View File

@@ -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}`]),
), ),
}, },
}); });

View 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';

View 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&apos;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}
/>
</>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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 };

View 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;
}

View 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,
};
}

View 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',
];

View File

@@ -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}

View 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>
);

View File

@@ -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>
</> </>
); );

View File

@@ -1,49 +1,33 @@
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; <>
} <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Plagiat */}
<Link
href={'/plagat'}
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-blue-500 to-blue-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
>
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
<FileSearch size={72} className="text-white" />
</div>
<FileSearch size={26} className="text-white mb-4" />
<h3 className="text-white font-semibold text-base mb-1">
Plagiat tekshiruvi
</h3>
<p className="text-blue-100 text-sm mb-4 leading-relaxed">
Hujjatingizni originallik uchun tekshiring
</p>
<span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors">
Yuborish <ArrowRight size={12} />
</span>
</Link>
export const CtaCards: React.FC<CtaCardsProps> = ({ onNavigate }) => ( {/* SI */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <SiCTACard />
{/* Plagiat */} </div>
<button </>
onClick={() => onNavigate('plagiat')}
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-blue-500 to-blue-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
>
<div className="absolute right-3 top-3 opacity-10 pointer-events-none">
<FileSearch size={72} className="text-white" />
</div>
<FileSearch size={26} className="text-white mb-4" />
<h3 className="text-white font-semibold text-base mb-1">
Plagiat tekshiruvi
</h3>
<p className="text-blue-100 text-sm mb-4 leading-relaxed">
Hujjatingizni originallik uchun tekshiring
</p>
<span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors">
Yuborish <ArrowRight size={12} />
</span>
</button>
{/* SI */}
<button
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&apos;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>
); );

View File

@@ -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}>