update navbar , create pages

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-18 11:34:41 +05:00
parent ee34ef0905
commit 75bd3467e9
19 changed files with 301 additions and 150 deletions

BIN
public/lanka.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,54 +1,30 @@
'use client'; 'use client';
import { useState } from 'react'; import { ArrowRight, BrainCircuit } from 'lucide-react';
import { ArrowRight, BrainCircuit, Plus } from 'lucide-react';
import { FileUploadModal } from './ui/fileUploadModal';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Link from 'next/link';
export function SiCTACard() { export function SiCTACard() {
const [isOpen, setIsOpen] = useState(false);
const t = useTranslations('Cabinet'); const t = useTranslations('Cabinet');
return ( return (
<> <Link
<button href="/si"
onClick={() => setIsOpen(true)} className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-violet-500 to-violet-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
className="group relative overflow-hidden rounded-2xl bg-linear-to-br from-violet-500 to-violet-600 p-6 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md" >
> <div className="absolute right-3 top-3 opacity-10 pointer-events-none">
<div className="absolute right-3 top-3 opacity-10 pointer-events-none"> <BrainCircuit size={72} className="text-white" />
<BrainCircuit size={72} className="text-white" /> </div>
</div> <BrainCircuit size={26} className="text-white mb-4" />
<BrainCircuit size={26} className="text-white mb-4" /> <h3 className="text-white font-semibold text-base mb-1">
<h3 className="text-white font-semibold text-base mb-1"> {t('siDetector')}
{t('siDetector')} </h3>
</h3> <p className="text-violet-100 text-sm mb-4 leading-relaxed">
<p className="text-violet-100 text-sm mb-4 leading-relaxed"> {t('siDesc')}
{t('siDesc')} </p>
</p> <span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors">
<span className="inline-flex items-center gap-1.5 text-white text-xs font-medium bg-white/20 rounded-lg px-3 py-1.5 group-hover:bg-white/30 transition-colors"> {t('submit')} <ArrowRight size={12} />
{t('submit')} <ArrowRight size={12} /> </span>
</span> </Link>
</button>
<FileUploadModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
);
}
export function SiButton() {
const [isOpen, setIsOpen] = useState<boolean>(false);
const t = useTranslations('Cabinet');
return (
<>
<button
onClick={() => setIsOpen(true)}
className="flex items-center gap-2 py-1 px-2 group relative overflow-hidden rounded-sm bg-linear-to-br from-violet-500 to-violet-600 text-left shadow-md hover:shadow-xl transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:shadow-md"
>
<Plus size={15} className="text-white" />
<h3 className="text-white font-semibold text-base">
{t('siDetector')}
</h3>
</button>
<FileUploadModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
); );
} }

View File

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

View File

@@ -10,7 +10,9 @@
"login": "Login", "login": "Login",
"signup": "Sign up", "signup": "Sign up",
"profile": "Profile", "profile": "Profile",
"logout": "Logout" "logout": "Logout",
"aboutPlagiat": "About Plagiarism",
"howItWorks": "How It Works"
}, },
"Footer": { "Footer": {
"product": "Product", "product": "Product",

View File

@@ -10,7 +10,9 @@
"login": "Войти", "login": "Войти",
"signup": "Регистрация", "signup": "Регистрация",
"profile": "Профиль", "profile": "Профиль",
"logout": "Выйти" "logout": "Выйти",
"aboutPlagiat": "О плагиате",
"howItWorks": "Как это работает"
}, },
"Footer": { "Footer": {
"product": "Продукт", "product": "Продукт",

View File

@@ -14,6 +14,8 @@ declare const messages: {
signup: "Ro'yxatdan o'tish"; signup: "Ro'yxatdan o'tish";
profile: 'Profil'; profile: 'Profil';
logout: 'Chiqish'; logout: 'Chiqish';
aboutPlagiat: 'Plagiat haqida';
howItWorks: 'Bu qanday ishlaydi';
}; };
Footer: { Footer: {
product: 'Mahsulot'; product: 'Mahsulot';

View File

@@ -10,7 +10,9 @@
"login": "Kirish", "login": "Kirish",
"signup": "Ro'yxatdan o'tish", "signup": "Ro'yxatdan o'tish",
"profile": "Profil", "profile": "Profil",
"logout": "Chiqish" "logout": "Chiqish",
"aboutPlagiat": "Plagiat haqida",
"howItWorks": "Bu qanday ishlaydi"
}, },
"Footer": { "Footer": {
"product": "Mahsulot", "product": "Mahsulot",

View File

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

View File

@@ -9,7 +9,6 @@ import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links'; import { links } from '@/shared/request/links';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import type { SiDocument } from '../../lib/types'; import type { SiDocument } from '../../lib/types';
import { SiButton } from '@/features/modals/siModal/page';
import { useRouter, useParams } from 'next/navigation'; import { useRouter, useParams } from 'next/navigation';
// ─── State badge ─────────────────────────────────────────────────────────────── // ─── State badge ───────────────────────────────────────────────────────────────
@@ -194,7 +193,6 @@ export const SiTable: React.FC = () => {
{isLoading ? '...' : t('checksCount', { count: items.length })} {isLoading ? '...' : t('checksCount', { count: items.length })}
</p> </p>
</div> </div>
<SiButton />
</div> </div>
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden"> <div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
'use client';
import { Button } from '@/shared/ui/button'; import { Button } from '@/shared/ui/button';
import { import {
Sheet, Sheet,
@@ -13,16 +14,47 @@ import { AuthButtons } from './authButtons';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Logo_image } from '@/image'; import { Logo_image } from '@/image';
import Image from 'next/image'; import Image from 'next/image';
import { useEffect, useState } from 'react';
import { useUserPlagiatStore } from '@/shared/zustand/user';
const Navbar = () => { const Navbar = () => {
const t = useTranslations('Navbar'); const t = useTranslations('Navbar');
const t_cab = useTranslations('Cabinet');
const [localUser, setLocalUser] = useState<{
id: number;
name: string;
surname: string;
} | null>(null);
const user = useUserPlagiatStore((state) => state.user);
const scrollTo = (id: string) => {
const el = document.getElementById(id);
if (el) el.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
const data = localStorage.getItem('user');
if (data) {
setLocalUser(JSON.parse(data));
} else {
setLocalUser(null);
}
}, [user]);
const navItems = [
{ title: t_cab('dashboard'), href: '/dashboard' as const },
{ title: t_cab('plagiat'), href: '/plagiat' as const },
{ title: t_cab('siNav'), href: '/si' as const },
{ title: t_cab('payments'), href: '/payments' as const },
{ title: t('profile'), href: '/profile' as const },
];
return ( return (
<section className="py-1 flex items-center justify-center w-full "> <section className="py-1 flex items-center justify-center w-full ">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
{/* Desktop Menu */} {/* Desktop Menu */}
<nav className="justify-between items-center flex max-sm:flex-col gap-5"> <nav className="justify-between items-center flex max-sm:flex-col gap-5">
<div className="flex items-center justify-between gap-6"> <div className="flex items-center justify-around gap-2 w-full">
{/* Logo */} {/* Logo */}
<Link <Link
href={'/'} href={'/'}
@@ -36,6 +68,36 @@ const Navbar = () => {
height={10} height={10}
/> />
</Link> </Link>
{localUser ? (
<div className="flex items-center gap-2">
{navItems.map((item) => (
<Link
key={item.title}
href={item.href}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md hover:bg-muted transition-colors"
>
{item.title}
</Link>
))}
</div>
) : (
<nav className="hidden sm:flex items-center gap-1">
<button
onClick={() => scrollTo('info-section')}
className="px-3 py-1.5 text-sm font-medium rounded-md hover:bg-muted transition-colors"
>
{t('aboutPlagiat')}
</button>
<button
onClick={() => scrollTo('steps-section')}
className="px-3 py-1.5 text-sm font-medium rounded-md hover:bg-muted transition-colors"
>
{t('howItWorks')}
</button>
</nav>
)}
<div className="flex sm:hidden items-center justify-center"> <div className="flex sm:hidden items-center justify-center">
<ChangeLang /> <ChangeLang />
</div> </div>

View File

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