changed
This commit is contained in:
5
src/app/[locale]/cabinet/page.tsx
Normal file
5
src/app/[locale]/cabinet/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CabinetLayout } from '@/widgets/cabinet/ui';
|
||||
|
||||
export default function CabinetPage() {
|
||||
return <CabinetLayout />;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ function NavigationMenuTrigger({
|
||||
>
|
||||
{children}{' '}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
className="relative top-px ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
|
||||
18
src/widgets/cabinet/lib/hooks/useCabinet.ts
Normal file
18
src/widgets/cabinet/lib/hooks/useCabinet.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import type { CabinetSection } from '../types';
|
||||
|
||||
export const useCabinet = () => {
|
||||
const [activeSection, setActiveSection] =
|
||||
useState<CabinetSection>('dashboard');
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const navigate = (section: CabinetSection) => {
|
||||
setActiveSection(section);
|
||||
setIsSidebarOpen(false);
|
||||
};
|
||||
|
||||
const toggleSidebar = () => setIsSidebarOpen((prev) => !prev);
|
||||
|
||||
return { activeSection, navigate, isSidebarOpen, toggleSidebar };
|
||||
};
|
||||
35
src/widgets/cabinet/lib/hooks/useProfile.ts
Normal file
35
src/widgets/cabinet/lib/hooks/useProfile.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import type { UserProfile } from '../types';
|
||||
|
||||
interface ProfileForm extends UserProfile {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export const useProfile = (initial: UserProfile) => {
|
||||
const [form, setForm] = useState<ProfileForm>({
|
||||
...initial,
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
const handleChange = (field: keyof ProfileForm, value: string) => {
|
||||
setForm((prev) => ({ ...prev, [field]: value }));
|
||||
setSaved(false);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
// TODO: replace with real API call
|
||||
await new Promise((res) => setTimeout(res, 800));
|
||||
setIsSaving(false);
|
||||
setSaved(true);
|
||||
};
|
||||
|
||||
return { form, isSaving, saved, handleChange, handleSave };
|
||||
};
|
||||
151
src/widgets/cabinet/lib/mock.ts
Normal file
151
src/widgets/cabinet/lib/mock.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type {
|
||||
CabinetStats,
|
||||
Payment,
|
||||
PlagiatCheck,
|
||||
SiCheck,
|
||||
UserProfile,
|
||||
} from './types';
|
||||
|
||||
export const MOCK_USER: UserProfile = {
|
||||
name: 'Ali',
|
||||
surname: 'Karimov',
|
||||
email: 'ali.karimov@gmail.com',
|
||||
phone: '+998 90 123 45 67',
|
||||
};
|
||||
|
||||
export const MOCK_STATS: CabinetStats = {
|
||||
total: 24,
|
||||
thisMonth: 7,
|
||||
discountUsed: 7,
|
||||
discountTotal: 10,
|
||||
balance: 0,
|
||||
currency: 'UZS',
|
||||
};
|
||||
|
||||
export const MOCK_PLAGIAT: PlagiatCheck[] = [
|
||||
{
|
||||
id: 1,
|
||||
file: 'diplom_ishi.docx',
|
||||
type: 'Diplom',
|
||||
percent: 12,
|
||||
date: '2026-04-01',
|
||||
status: 'completed',
|
||||
downloadUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
file: 'kurs_ishi_2.pdf',
|
||||
type: 'Kurs ishi',
|
||||
percent: 8,
|
||||
date: '2026-03-28',
|
||||
status: 'completed',
|
||||
downloadUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
file: 'referat_fizika.docx',
|
||||
type: 'Referat',
|
||||
percent: 23,
|
||||
date: '2026-03-20',
|
||||
status: 'completed',
|
||||
downloadUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
file: 'magistr_disser.pdf',
|
||||
type: 'Magistrlik',
|
||||
percent: 5,
|
||||
date: '2026-03-15',
|
||||
status: 'completed',
|
||||
downloadUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
file: 'tahlil_hisobot.docx',
|
||||
type: 'Hisobot',
|
||||
percent: 0,
|
||||
date: '2026-04-05',
|
||||
status: 'pending',
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_SI: SiCheck[] = [
|
||||
{
|
||||
id: 1,
|
||||
file: 'kurs_ishi_1.docx',
|
||||
words: 4200,
|
||||
siPercent: 18,
|
||||
date: '2026-04-02',
|
||||
status: 'completed',
|
||||
reportUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
file: 'maqola_2026.pdf',
|
||||
words: 1800,
|
||||
siPercent: 42,
|
||||
date: '2026-03-29',
|
||||
status: 'completed',
|
||||
reportUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
file: 'tezis_draft.docx',
|
||||
words: 950,
|
||||
siPercent: 7,
|
||||
date: '2026-03-22',
|
||||
status: 'completed',
|
||||
reportUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
file: 'annotatsiya.txt',
|
||||
words: 320,
|
||||
siPercent: 0,
|
||||
date: '2026-04-05',
|
||||
status: 'pending',
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_PAYMENTS: Payment[] = [
|
||||
{
|
||||
id: 1,
|
||||
service: 'Plagiat tekshiruvi',
|
||||
amount: 41200,
|
||||
discount: 5200,
|
||||
date: '2026-04-01',
|
||||
status: 'paid',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
service: 'SI detektor',
|
||||
amount: 30000,
|
||||
discount: 0,
|
||||
date: '2026-03-28',
|
||||
status: 'paid',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
service: 'Plagiat tekshiruvi',
|
||||
amount: 41200,
|
||||
discount: 5200,
|
||||
date: '2026-03-20',
|
||||
status: 'paid',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
service: 'Plagiat tekshiruvi',
|
||||
amount: 41200,
|
||||
discount: 0,
|
||||
date: '2026-03-15',
|
||||
status: 'paid',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
service: 'SI detektor',
|
||||
amount: 30000,
|
||||
discount: 0,
|
||||
date: '2026-04-05',
|
||||
status: 'pending',
|
||||
},
|
||||
];
|
||||
230
src/widgets/cabinet/lib/modules.ts
Normal file
230
src/widgets/cabinet/lib/modules.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
// ─── Tag types ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export type ModuleTag =
|
||||
| 'Ilmiy'
|
||||
| 'AI tahlil'
|
||||
| 'Huquqiy'
|
||||
| 'Internet'
|
||||
| 'Bepul'
|
||||
| 'OAV'
|
||||
| 'Ichki baza'
|
||||
| 'Standartlar'
|
||||
| "Ta'lim"
|
||||
| 'Xalqaro'
|
||||
| 'Avtomatik';
|
||||
|
||||
export interface Module {
|
||||
name: string;
|
||||
desc: string;
|
||||
tags: ModuleTag[];
|
||||
}
|
||||
|
||||
export interface ModuleCategory {
|
||||
label: string;
|
||||
modules: Module[];
|
||||
}
|
||||
|
||||
// ─── Tag style map ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const TAG_STYLES: Record<ModuleTag, string> = {
|
||||
Ilmiy: 'bg-blue-50 text-blue-700',
|
||||
'AI tahlil': 'bg-violet-50 text-violet-700',
|
||||
Huquqiy: 'bg-amber-50 text-amber-700',
|
||||
Internet: 'bg-emerald-50 text-emerald-700',
|
||||
Bepul: 'bg-lime-50 text-lime-700',
|
||||
OAV: 'bg-stone-100 text-stone-600',
|
||||
'Ichki baza': 'bg-stone-100 text-stone-600',
|
||||
Standartlar: 'bg-amber-50 text-amber-700',
|
||||
"Ta'lim": 'bg-blue-50 text-blue-700',
|
||||
Xalqaro: 'bg-emerald-50 text-emerald-700',
|
||||
Avtomatik: 'bg-violet-50 text-violet-700',
|
||||
};
|
||||
|
||||
// ─── Module stats ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const MODULE_STATS = {
|
||||
total: 30,
|
||||
freeInternet: 10,
|
||||
aiModules: 5,
|
||||
categories: 4,
|
||||
} as const;
|
||||
|
||||
// ─── Module data ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const MODULE_CATEGORIES: ModuleCategory[] = [
|
||||
{
|
||||
label: "Ilmiy va ta'lim bazalari",
|
||||
modules: [
|
||||
{
|
||||
name: 'eLIBRARY.RU',
|
||||
desc: "Rus va xorijiy tillardagi ilmiy maqolalarning to'liq matnlari bazasi",
|
||||
tags: ['Ilmiy'],
|
||||
},
|
||||
{
|
||||
name: 'eLIBRARY nashrlari (tarjima va qayta bayon)',
|
||||
desc: 'Tarjima va parafraz qilingan maqolalarni aniqlash',
|
||||
tags: ['AI tahlil'],
|
||||
},
|
||||
{
|
||||
name: "RDK to'plami",
|
||||
desc: 'Rossiya Davlat kutubxonasidan dissertatsiya va avtoreferatlar',
|
||||
tags: ['Ilmiy'],
|
||||
},
|
||||
{
|
||||
name: 'BMK dissertatsiyalari',
|
||||
desc: 'Belarus milliy kutubxonasi dissertatsiyalari va avtoreferatlari',
|
||||
tags: ['Ilmiy'],
|
||||
},
|
||||
{
|
||||
name: 'IEEE',
|
||||
desc: 'Xalqaro elektrotexnika va elektronika muhandislari instituti bazasi',
|
||||
tags: ['Ilmiy'],
|
||||
},
|
||||
{
|
||||
name: 'IEEE parafraz moduli',
|
||||
desc: 'IEEE maqolalarining qayta bayon qilingan variantlarini aniqlash',
|
||||
tags: ['AI tahlil'],
|
||||
},
|
||||
{
|
||||
name: 'Elektron-kutubxona tizimlari',
|
||||
desc: 'Book.ru, Yurait, Lans, Aybuks va boshqa ELS bazalari',
|
||||
tags: ['Ilmiy'],
|
||||
},
|
||||
{
|
||||
name: 'OTMlar halqasi',
|
||||
desc: "O'zbekiston oliy ta'lim muassasalari birgalikdagi bazasi",
|
||||
tags: ['Ilmiy'],
|
||||
},
|
||||
{
|
||||
name: 'Kolleksiya NMU',
|
||||
desc: "O'zbekiston milliy kutubxonasi to'plami",
|
||||
tags: ['Ilmiy'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Huquqiy va normativ bazalar',
|
||||
modules: [
|
||||
{
|
||||
name: 'Patentlar',
|
||||
desc: "SSSR, O'zbekiston, Rossiya va MDH davlatlari patentlari bazasi",
|
||||
tags: ['Huquqiy'],
|
||||
},
|
||||
{
|
||||
name: 'NBS Adilex',
|
||||
desc: "O'zbekiston qonunchilik bazasi hujjatlari",
|
||||
tags: ['Huquqiy'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Internet tekshiruv modullari',
|
||||
modules: [
|
||||
{
|
||||
name: 'Internet PLUS moduli',
|
||||
desc: "Internet bo'ylab kengaytirilgan chuqur skanerlash",
|
||||
tags: ['Internet'],
|
||||
},
|
||||
{
|
||||
name: 'Internet RU – parafraz',
|
||||
desc: 'Rus internet segmentidagi qayta bayon qilingan qarzlar',
|
||||
tags: ['Internet', 'AI tahlil'],
|
||||
},
|
||||
{
|
||||
name: 'Internet EN – parafraz',
|
||||
desc: 'Ingliz internet segmentidagi qayta bayon qilingan qarzlar',
|
||||
tags: ['Internet', 'AI tahlil'],
|
||||
},
|
||||
{
|
||||
name: 'Internet RU – tarjima',
|
||||
desc: 'Rus internet segmentidagi tarjima qilingan qarzlar',
|
||||
tags: ['Internet', 'AI tahlil'],
|
||||
},
|
||||
{
|
||||
name: 'Internet EN – tarjima',
|
||||
desc: 'Ingliz internet segmentidagi tarjima qilingan qarzlar',
|
||||
tags: ['Internet', 'AI tahlil'],
|
||||
},
|
||||
{
|
||||
name: 'SMI Rossii va MDH',
|
||||
desc: 'Rossiya va MDH ommaviy axborot vositalari maqolalari',
|
||||
tags: ['OAV'],
|
||||
},
|
||||
{
|
||||
name: "Kompaniyaning ichki to'plami",
|
||||
desc: "Antiplag.uz ichki hujjatlar to'plami",
|
||||
tags: ['Ichki baza'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Yangi bepul internet manbalari',
|
||||
modules: [
|
||||
{
|
||||
name: 'consultant.ru',
|
||||
desc: 'Rossiya qonunchiligining elektron bazasi',
|
||||
tags: ['Bepul', 'Huquqiy'],
|
||||
},
|
||||
{
|
||||
name: 'kremlin.ru',
|
||||
desc: 'Rossiya prezidenti farmonlari va rasmiy qonunlar',
|
||||
tags: ['Bepul', 'Huquqiy'],
|
||||
},
|
||||
{
|
||||
name: 'pravo.gov.ru',
|
||||
desc: 'Rossiya rasmiy huquqiy hujjatlar nashriyoti portali',
|
||||
tags: ['Bepul', 'Huquqiy'],
|
||||
},
|
||||
{
|
||||
name: 'docs.cntd.ru',
|
||||
desc: 'Texnik normalar va standartlar hujjatlari bazasi',
|
||||
tags: ['Bepul', 'Standartlar'],
|
||||
},
|
||||
{
|
||||
name: 'rumc.mininuniver.ru',
|
||||
desc: "Minin universiteti adaptiv ta'lim dasturlari resursi",
|
||||
tags: ['Bepul', "Ta'lim"],
|
||||
},
|
||||
{
|
||||
name: 'moodle.kstu.ru',
|
||||
desc: "KSTU universiteti Moodle platformasi o'quv resurslari",
|
||||
tags: ['Bepul', "Ta'lim"],
|
||||
},
|
||||
{
|
||||
name: 'freereferats.ru',
|
||||
desc: "Dissertatsiya avtoreferatlari ochiq to'plami (PDF)",
|
||||
tags: ['Bepul', 'Ilmiy'],
|
||||
},
|
||||
{
|
||||
name: 'ktzszmoik.gov.by',
|
||||
desc: 'Belarus nogironlarni ijtimoiy himoya qilish davlat portali',
|
||||
tags: ['Bepul', 'Internet'],
|
||||
},
|
||||
{
|
||||
name: 'bizlog.ru',
|
||||
desc: "Iqtisodiy-boshqaruv terminologiyasi va izohli lug'at",
|
||||
tags: ['Bepul', 'Internet'],
|
||||
},
|
||||
{
|
||||
name: 'disabilityartsinternational.org',
|
||||
desc: "Nogironlik va madaniyat bo'yicha xalqaro resurs",
|
||||
tags: ['Bepul', 'Xalqaro'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Yordamchi modullar',
|
||||
modules: [
|
||||
{
|
||||
name: 'Shablon iboralar',
|
||||
desc: "Standart kirish so'zlari, universitet nomlari va klishe iboralarni aniqlash",
|
||||
tags: ['Avtomatik'],
|
||||
},
|
||||
{
|
||||
name: 'Iqtibos keltirish moduli',
|
||||
desc: "Hujjatda to'g'ri rasmiylashtirilgan iqtiboslarni avtomatik aniqlash",
|
||||
tags: ['Avtomatik'],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
55
src/widgets/cabinet/lib/types.ts
Normal file
55
src/widgets/cabinet/lib/types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// ─── Navigation ────────────────────────────────────────────────────────────────
|
||||
|
||||
export type CabinetSection =
|
||||
| 'dashboard'
|
||||
| 'plagiat'
|
||||
| 'si'
|
||||
| 'payments'
|
||||
| 'profile';
|
||||
|
||||
// ─── Domain ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface UserProfile {
|
||||
name: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export interface PlagiatCheck {
|
||||
id: number;
|
||||
file: string;
|
||||
type: string;
|
||||
percent: number;
|
||||
date: string;
|
||||
status: 'completed' | 'pending' | 'failed';
|
||||
downloadUrl?: string;
|
||||
}
|
||||
|
||||
export interface SiCheck {
|
||||
id: number;
|
||||
file: string;
|
||||
words: number;
|
||||
siPercent: number;
|
||||
date: string;
|
||||
status: 'completed' | 'pending' | 'failed';
|
||||
reportUrl?: string;
|
||||
}
|
||||
|
||||
export interface Payment {
|
||||
id: number;
|
||||
service: string;
|
||||
amount: number;
|
||||
discount: number;
|
||||
date: string;
|
||||
status: 'paid' | 'pending' | 'failed';
|
||||
}
|
||||
|
||||
export interface CabinetStats {
|
||||
total: number;
|
||||
thisMonth: number;
|
||||
discountUsed: number;
|
||||
discountTotal: number;
|
||||
balance: number;
|
||||
currency: string;
|
||||
}
|
||||
145
src/widgets/cabinet/ui/Sidebar.tsx
Normal file
145
src/widgets/cabinet/ui/Sidebar.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
FileSearch,
|
||||
BrainCircuit,
|
||||
CreditCard,
|
||||
User,
|
||||
Home,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Link } from '@/shared/config/i18n/navigation';
|
||||
import type { CabinetSection } from '../lib/types';
|
||||
|
||||
// ─── Nav items ─────────────────────────────────────────────────────────────────
|
||||
|
||||
type NavItemDef =
|
||||
| { id: CabinetSection; label: string; icon: React.ElementType; href?: never }
|
||||
| { id: 'home'; label: string; icon: React.ElementType; href: string };
|
||||
|
||||
const NAV_ITEMS: NavItemDef[] = [
|
||||
{ id: 'home', label: 'Bosh sahifa', icon: Home, href: '/' },
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ id: 'plagiat', label: 'Plagiat', icon: FileSearch },
|
||||
{ id: 'si', label: 'SI detektor', icon: BrainCircuit },
|
||||
{ id: 'payments', label: "To'lovlar tarixi", icon: CreditCard },
|
||||
{ id: 'profile', label: 'Profil', icon: User },
|
||||
];
|
||||
|
||||
// ─── Props ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface SidebarProps {
|
||||
active: CabinetSection;
|
||||
onNavigate: (section: CabinetSection) => void;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
// ─── Component ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({
|
||||
active,
|
||||
onNavigate,
|
||||
isOpen,
|
||||
onClose,
|
||||
userName,
|
||||
}) => (
|
||||
<>
|
||||
{/* Mobile backdrop */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-30 bg-black/30 backdrop-blur-sm lg:hidden"
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
<aside
|
||||
className={`
|
||||
fixed top-0 left-0 z-40 h-full w-60 bg-white border-r border-slate-100
|
||||
flex flex-col shadow-xl
|
||||
transition-transform duration-300 ease-out
|
||||
lg:sticky lg:top-0 lg:h-screen lg:translate-x-0 lg:shadow-none lg:z-auto
|
||||
${isOpen ? 'translate-x-0' : '-translate-x-full'}
|
||||
`}
|
||||
>
|
||||
{/* Brand */}
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-blue-500 flex items-center justify-center">
|
||||
<span className="text-white text-xs font-bold">P</span>
|
||||
</div>
|
||||
<span className="font-semibold text-slate-800 text-sm">Plagat</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="lg:hidden p-1.5 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors"
|
||||
aria-label="Yopish"
|
||||
>
|
||||
<X size={15} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* User pill */}
|
||||
<div className="px-3 pt-4 pb-2">
|
||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-slate-50 border border-slate-100">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center shrink-0">
|
||||
<span className="text-blue-600 text-xs font-semibold">
|
||||
{userName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-slate-800 truncate">
|
||||
{userName}
|
||||
</p>
|
||||
<p className="text-[11px] text-slate-400">Shaxsiy kabinet</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-2 space-y-0.5 overflow-y-auto">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
|
||||
if (item.id === 'home') {
|
||||
return (
|
||||
<Link
|
||||
key="home"
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium text-slate-500 hover:text-slate-800 hover:bg-slate-50 transition-all duration-150"
|
||||
>
|
||||
<Icon size={17} />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
const isActive = item.id === active;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onNavigate(item.id as CabinetSection)}
|
||||
className={`
|
||||
w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium
|
||||
transition-all duration-150
|
||||
${
|
||||
isActive
|
||||
? 'bg-blue-50 text-blue-600'
|
||||
: 'text-slate-500 hover:text-slate-800 hover:bg-slate-50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon size={17} />
|
||||
<span>{item.label}</span>
|
||||
{isActive && (
|
||||
<span className="ml-auto w-1.5 h-1.5 rounded-full bg-blue-500 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
49
src/widgets/cabinet/ui/dashboard/CtaCards.tsx
Normal file
49
src/widgets/cabinet/ui/dashboard/CtaCards.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { FileSearch, BrainCircuit, ArrowRight } from 'lucide-react';
|
||||
import type { CabinetSection } from '../../lib/types';
|
||||
|
||||
interface CtaCardsProps {
|
||||
onNavigate: (section: CabinetSection) => void;
|
||||
}
|
||||
|
||||
export const CtaCards: React.FC<CtaCardsProps> = ({ onNavigate }) => (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Plagiat */}
|
||||
<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'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>
|
||||
);
|
||||
128
src/widgets/cabinet/ui/dashboard/ModulesSection.tsx
Normal file
128
src/widgets/cabinet/ui/dashboard/ModulesSection.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
MODULE_CATEGORIES,
|
||||
MODULE_STATS,
|
||||
TAG_STYLES,
|
||||
type ModuleTag,
|
||||
} from '../../lib/modules';
|
||||
|
||||
// ─── Module stats mini-cards ───────────────────────────────────────────────────
|
||||
|
||||
const ModuleStats: React.FC = () => (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ value: MODULE_STATS.total, label: 'Jami modullar' },
|
||||
{ value: MODULE_STATS.freeInternet, label: 'Bepul internet manbalari' },
|
||||
{ value: MODULE_STATS.aiModules, label: 'AI tahlil modullari' },
|
||||
{ value: MODULE_STATS.categories, label: 'Kategoriya' },
|
||||
].map(({ value, label }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="bg-slate-50 border border-slate-100 rounded-xl px-4 py-3"
|
||||
>
|
||||
<p className="text-2xl font-bold text-slate-900 tabular-nums leading-none">
|
||||
{value}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">{label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Tag badge ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const Tag: React.FC<{ tag: ModuleTag }> = ({ tag }) => (
|
||||
<span
|
||||
className={`inline-block text-[10px] font-semibold px-2 py-0.5 rounded-full ${TAG_STYLES[tag]}`}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
);
|
||||
|
||||
// ─── Category divider ──────────────────────────────────────────────────────────
|
||||
|
||||
const CategoryHeader: React.FC<{ label: string }> = ({ label }) => (
|
||||
<div className="flex items-center gap-3 my-5">
|
||||
<div className="flex-1 h-px bg-slate-100" />
|
||||
<span className="text-[11px] font-semibold tracking-widest text-slate-400 uppercase whitespace-nowrap">
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-slate-100" />
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Single module card ────────────────────────────────────────────────────────
|
||||
|
||||
interface ModuleCardProps {
|
||||
name: string;
|
||||
desc: string;
|
||||
tags: ModuleTag[];
|
||||
index: number;
|
||||
}
|
||||
|
||||
const ModuleCard: React.FC<ModuleCardProps> = ({ name, desc, tags, index }) => (
|
||||
<div className="bg-white border border-slate-100 rounded-xl p-4 flex items-start gap-3 hover:border-slate-200 hover:shadow-sm transition-all duration-150">
|
||||
{/* Index number */}
|
||||
<span className="w-6 h-6 rounded-full bg-slate-100 flex items-center justify-center text-[10px] font-semibold text-slate-500 shrink-0 mt-0.5">
|
||||
{index}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-semibold text-slate-800 leading-snug mb-1 truncate">
|
||||
{name}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 leading-relaxed mb-2.5">{desc}</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{tags.map((tag) => (
|
||||
<Tag key={tag} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Modules section ───────────────────────────────────────────────────────────
|
||||
|
||||
export const ModulesSection: React.FC = () => {
|
||||
// running counter across all categories
|
||||
let counter = 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-800">
|
||||
Tekshiruv modullari
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-0.5">
|
||||
Plagiat aniqlashda foydalaniladigan barcha manbalar
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 bg-slate-100 px-2.5 py-1 rounded-lg font-medium">
|
||||
{MODULE_STATS.total} ta modul
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ModuleStats />
|
||||
|
||||
{MODULE_CATEGORIES.map((cat) => (
|
||||
<div key={cat.label}>
|
||||
<CategoryHeader label={cat.label} />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
{cat.modules.map((mod) => {
|
||||
counter += 1;
|
||||
return (
|
||||
<ModuleCard
|
||||
key={mod.name}
|
||||
name={mod.name}
|
||||
desc={mod.desc}
|
||||
tags={mod.tags}
|
||||
index={counter}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
81
src/widgets/cabinet/ui/dashboard/StatsCards.tsx
Normal file
81
src/widgets/cabinet/ui/dashboard/StatsCards.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import { TrendingUp, Calendar, Tag, Wallet } from 'lucide-react';
|
||||
import type { CabinetStats } from '../../lib/types';
|
||||
|
||||
// ─── Single stat card ──────────────────────────────────────────────────────────
|
||||
|
||||
interface StatCardProps {
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
value: string;
|
||||
sub?: string;
|
||||
iconColor: string;
|
||||
iconBg: string;
|
||||
}
|
||||
|
||||
const StatCard: React.FC<StatCardProps> = ({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
iconColor,
|
||||
iconBg,
|
||||
}) => (
|
||||
<div className="bg-white rounded-2xl border border-slate-100 p-5 shadow-sm hover:shadow-md transition-shadow duration-200">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-xl flex items-center justify-center mb-4 ${iconBg}`}
|
||||
>
|
||||
<Icon size={18} className={iconColor} />
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-slate-900 tabular-nums">{value}</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5">{label}</p>
|
||||
{sub && <p className="text-[11px] text-slate-400 mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Grid ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface StatsCardsProps {
|
||||
stats: CabinetStats;
|
||||
}
|
||||
|
||||
export const StatsCards: React.FC<StatsCardsProps> = ({ stats }) => {
|
||||
const discountPct = Math.round(
|
||||
(stats.discountUsed / stats.discountTotal) * 100,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
icon={TrendingUp}
|
||||
label="Jami tekshiruvlar"
|
||||
value={String(stats.total)}
|
||||
iconColor="text-blue-600"
|
||||
iconBg="bg-blue-50"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Calendar}
|
||||
label="Bu oy"
|
||||
value={String(stats.thisMonth)}
|
||||
sub={`${stats.discountUsed}/${stats.discountTotal} ta hujjat`}
|
||||
iconColor="text-emerald-600"
|
||||
iconBg="bg-emerald-50"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Tag}
|
||||
label="Chegirma holati"
|
||||
value={`${discountPct}%`}
|
||||
sub={`${stats.discountUsed}/${stats.discountTotal} ta ishlatilgan`}
|
||||
iconColor="text-amber-600"
|
||||
iconBg="bg-amber-50"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Wallet}
|
||||
label="Balans"
|
||||
value={`${stats.balance.toLocaleString()} ${stats.currency}`}
|
||||
iconColor="text-violet-600"
|
||||
iconBg="bg-violet-50"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
41
src/widgets/cabinet/ui/dashboard/index.tsx
Normal file
41
src/widgets/cabinet/ui/dashboard/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { CtaCards } from './CtaCards';
|
||||
import { StatsCards } from './StatsCards';
|
||||
import { ModulesSection } from './ModulesSection';
|
||||
import type { CabinetSection, CabinetStats } from '../../lib/types';
|
||||
|
||||
interface DashboardProps {
|
||||
stats: CabinetStats;
|
||||
onNavigate: (section: CabinetSection) => void;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export const Dashboard: React.FC<DashboardProps> = ({
|
||||
stats,
|
||||
onNavigate,
|
||||
userName,
|
||||
}) => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900">
|
||||
Xush kelibsiz, {userName} 👋
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 mt-0.5">
|
||||
Shaxsiy kabinetingizga xush kelibsiz
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<StatsCards stats={stats} />
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-widest mb-3">
|
||||
Tezkor harakatlar
|
||||
</h3>
|
||||
<CtaCards onNavigate={onNavigate} />
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-100 pt-6">
|
||||
<ModulesSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
114
src/widgets/cabinet/ui/index.tsx
Normal file
114
src/widgets/cabinet/ui/index.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { Dashboard } from './dashboard';
|
||||
import { useCabinet } from '../lib/hooks/useCabinet';
|
||||
import {
|
||||
MOCK_USER,
|
||||
MOCK_STATS,
|
||||
MOCK_PLAGIAT,
|
||||
MOCK_SI,
|
||||
MOCK_PAYMENTS,
|
||||
} from '../lib/mock';
|
||||
import type { CabinetSection } from '../lib/types';
|
||||
|
||||
// ─── Lazy sections (separate JS chunks) ───────────────────────────────────────
|
||||
|
||||
const Skeleton = () => (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="h-12 bg-slate-100 rounded-xl animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const PlagiatTable = dynamic(
|
||||
() =>
|
||||
import('./tables/PlagiatTable').then((m) => ({ default: m.PlagiatTable })),
|
||||
{ loading: Skeleton },
|
||||
);
|
||||
const SiTable = dynamic(
|
||||
() => import('./tables/SiTable').then((m) => ({ default: m.SiTable })),
|
||||
{ loading: Skeleton },
|
||||
);
|
||||
const PaymentsTable = dynamic(
|
||||
() =>
|
||||
import('./tables/PaymentsTable').then((m) => ({
|
||||
default: m.PaymentsTable,
|
||||
})),
|
||||
{ loading: Skeleton },
|
||||
);
|
||||
const ProfileSection = dynamic(
|
||||
() => import('./profile').then((m) => ({ default: m.ProfileSection })),
|
||||
{ loading: Skeleton },
|
||||
);
|
||||
|
||||
// ─── Section switcher ──────────────────────────────────────────────────────────
|
||||
|
||||
function SectionContent({
|
||||
section,
|
||||
onNavigate,
|
||||
}: {
|
||||
section: CabinetSection;
|
||||
onNavigate: (s: CabinetSection) => void;
|
||||
}) {
|
||||
switch (section) {
|
||||
case 'dashboard':
|
||||
return (
|
||||
<Dashboard
|
||||
stats={MOCK_STATS}
|
||||
onNavigate={onNavigate}
|
||||
userName={MOCK_USER.name}
|
||||
/>
|
||||
);
|
||||
case 'plagiat':
|
||||
return <PlagiatTable data={MOCK_PLAGIAT} />;
|
||||
case 'si':
|
||||
return <SiTable data={MOCK_SI} />;
|
||||
case 'payments':
|
||||
return <PaymentsTable data={MOCK_PAYMENTS} />;
|
||||
case 'profile':
|
||||
return <ProfileSection user={MOCK_USER} stats={MOCK_STATS} />;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Animation ────────────────────────────────────────────────────────────────
|
||||
|
||||
const FADE = {
|
||||
initial: { opacity: 0, y: 10 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: -6 },
|
||||
transition: { duration: 0.18, ease: 'easeOut' },
|
||||
} as const;
|
||||
|
||||
// ─── CabinetLayout ────────────────────────────────────────────────────────────
|
||||
|
||||
export const CabinetLayout: React.FC = () => {
|
||||
const { activeSection, navigate, isSidebarOpen, toggleSidebar } =
|
||||
useCabinet();
|
||||
const fullName = `${MOCK_USER.name} ${MOCK_USER.surname}`;
|
||||
|
||||
return (
|
||||
<div className="flex border-t min-h-screen">
|
||||
<Sidebar
|
||||
active={activeSection}
|
||||
onNavigate={navigate}
|
||||
isOpen={isSidebarOpen}
|
||||
onClose={toggleSidebar}
|
||||
userName={fullName}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<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} onNavigate={navigate} />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
52
src/widgets/cabinet/ui/profile/DiscountProgress.tsx
Normal file
52
src/widgets/cabinet/ui/profile/DiscountProgress.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { Tag } from 'lucide-react';
|
||||
import type { CabinetStats } from '../../lib/types';
|
||||
|
||||
interface DiscountProgressProps {
|
||||
stats: CabinetStats;
|
||||
}
|
||||
|
||||
export const DiscountProgress: React.FC<DiscountProgressProps> = ({
|
||||
stats,
|
||||
}) => {
|
||||
const pct = Math.round((stats.discountUsed / stats.discountTotal) * 100);
|
||||
const remaining = stats.discountTotal - stats.discountUsed;
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-amber-50 to-orange-50 border border-amber-100 rounded-2xl p-5">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-xl bg-amber-100 flex items-center justify-center">
|
||||
<Tag size={15} className="text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-amber-800">
|
||||
Bu oyda chegirma
|
||||
</h3>
|
||||
<p className="text-xs text-amber-600 mt-0.5">
|
||||
{remaining > 0
|
||||
? `${remaining} ta hujjatdan keyin chegirma tugaydi`
|
||||
: 'Bu oyda barcha chegirmalar ishlatildi'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-amber-700 tabular-nums">
|
||||
{stats.discountUsed}/{stats.discountTotal}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bar */}
|
||||
<div className="h-2 bg-amber-100 rounded-full overflow-hidden mb-2.5">
|
||||
<div
|
||||
className="h-full bg-amber-400 rounded-full transition-[width] duration-700 ease-out"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-[11px] text-amber-600">
|
||||
<span>{stats.discountUsed} ta ishlatildi</span>
|
||||
<span>{pct}%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
170
src/widgets/cabinet/ui/profile/ProfileForm.tsx
Normal file
170
src/widgets/cabinet/ui/profile/ProfileForm.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { User, Mail, Phone, Lock, Save, CheckCircle } from 'lucide-react';
|
||||
import { useProfile } from '../../lib/hooks/useProfile';
|
||||
import type { UserProfile } from '../../lib/types';
|
||||
|
||||
// ─── Input field ───────────────────────────────────────────────────────────────
|
||||
|
||||
interface InputFieldProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
type?: string;
|
||||
icon: React.ElementType;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const InputField: React.FC<InputFieldProps> = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
type = 'text',
|
||||
icon: Icon,
|
||||
placeholder,
|
||||
}) => (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1.5">
|
||||
{label}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Icon
|
||||
size={14}
|
||||
className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"
|
||||
/>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="
|
||||
w-full pl-9 pr-4 py-2.5 text-sm rounded-xl
|
||||
border border-slate-200 bg-white
|
||||
text-slate-800 placeholder:text-slate-300
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400
|
||||
transition-all duration-150
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Form ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface ProfileFormProps {
|
||||
initial: UserProfile;
|
||||
}
|
||||
|
||||
export const ProfileForm: React.FC<ProfileFormProps> = ({ initial }) => {
|
||||
const { form, isSaving, saved, handleChange, handleSave } =
|
||||
useProfile(initial);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Personal info */}
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
|
||||
<h3 className="text-sm font-semibold text-slate-800 mb-4">
|
||||
Shaxsiy ma'lumotlar
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<InputField
|
||||
label="Ism"
|
||||
value={form.name}
|
||||
onChange={(v) => handleChange('name', v)}
|
||||
icon={User}
|
||||
placeholder="Ali"
|
||||
/>
|
||||
<InputField
|
||||
label="Familiya"
|
||||
value={form.surname}
|
||||
onChange={(v) => handleChange('surname', v)}
|
||||
icon={User}
|
||||
placeholder="Karimov"
|
||||
/>
|
||||
<InputField
|
||||
label="Email"
|
||||
value={form.email}
|
||||
onChange={(v) => handleChange('email', v)}
|
||||
type="email"
|
||||
icon={Mail}
|
||||
placeholder="ali@example.com"
|
||||
/>
|
||||
<InputField
|
||||
label="Telefon"
|
||||
value={form.phone}
|
||||
onChange={(v) => handleChange('phone', v)}
|
||||
icon={Phone}
|
||||
placeholder="+998 90 123 45 67"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
|
||||
<h3 className="text-sm font-semibold text-slate-800 mb-4">
|
||||
Parol o'zgartirish
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<InputField
|
||||
label="Joriy parol"
|
||||
value={form.currentPassword}
|
||||
onChange={(v) => handleChange('currentPassword', v)}
|
||||
type="password"
|
||||
icon={Lock}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<InputField
|
||||
label="Yangi parol"
|
||||
value={form.newPassword}
|
||||
onChange={(v) => handleChange('newPassword', v)}
|
||||
type="password"
|
||||
icon={Lock}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<InputField
|
||||
label="Tasdiqlash"
|
||||
value={form.confirmPassword}
|
||||
onChange={(v) => handleChange('confirmPassword', v)}
|
||||
type="password"
|
||||
icon={Lock}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className={`
|
||||
flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-semibold text-white
|
||||
transition-all duration-200
|
||||
${
|
||||
isSaving
|
||||
? 'bg-blue-300 cursor-not-allowed'
|
||||
: saved
|
||||
? 'bg-emerald-500 hover:bg-emerald-600'
|
||||
: 'bg-blue-500 hover:bg-blue-600 active:scale-95 shadow-md hover:shadow-lg'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{saved ? (
|
||||
<>
|
||||
<CheckCircle size={15} /> Saqlandi
|
||||
</>
|
||||
) : isSaving ? (
|
||||
<>
|
||||
<span className="w-4 h-4 border-2 border-white/40 border-t-white rounded-full animate-spin" />
|
||||
Saqlanmoqda…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save size={15} /> Saqlash
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
26
src/widgets/cabinet/ui/profile/index.tsx
Normal file
26
src/widgets/cabinet/ui/profile/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { DiscountProgress } from './DiscountProgress';
|
||||
import { ProfileForm } from './ProfileForm';
|
||||
import type { CabinetStats, UserProfile } from '../../lib/types';
|
||||
|
||||
interface ProfileSectionProps {
|
||||
user: UserProfile;
|
||||
stats: CabinetStats;
|
||||
}
|
||||
|
||||
export const ProfileSection: React.FC<ProfileSectionProps> = ({
|
||||
user,
|
||||
stats,
|
||||
}) => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900">Profil</h2>
|
||||
<p className="text-sm text-slate-500 mt-0.5">
|
||||
Ma'lumotlaringizni boshqaring
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DiscountProgress stats={stats} />
|
||||
<ProfileForm initial={user} />
|
||||
</div>
|
||||
);
|
||||
101
src/widgets/cabinet/ui/tables/PaymentsTable.tsx
Normal file
101
src/widgets/cabinet/ui/tables/PaymentsTable.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import { CheckCircle, Clock, XCircle } from 'lucide-react';
|
||||
import type { Payment } from '../../lib/types';
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const STATUS_MAP = {
|
||||
paid: {
|
||||
label: "To'landi",
|
||||
icon: CheckCircle,
|
||||
cls: 'text-emerald-600 bg-emerald-50',
|
||||
},
|
||||
pending: {
|
||||
label: 'Kutilmoqda',
|
||||
icon: Clock,
|
||||
cls: 'text-amber-600 bg-amber-50',
|
||||
},
|
||||
failed: { label: 'Xato', icon: XCircle, cls: 'text-red-600 bg-red-50' },
|
||||
} as const;
|
||||
|
||||
// ─── Component ─────────────────────────────────────────────────────────────────
|
||||
|
||||
interface PaymentsTableProps {
|
||||
data: Payment[];
|
||||
}
|
||||
|
||||
export const PaymentsTable: React.FC<PaymentsTableProps> = ({ data }) => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900">
|
||||
To'lovlar tarixi
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 mt-0.5">
|
||||
{data.length} ta to'lov
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-100">
|
||||
{['#', 'Xizmat', 'Summa', 'Chegirma', 'Sana', 'Holat'].map(
|
||||
(h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
),
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{data.map((row) => {
|
||||
const s = STATUS_MAP[row.status];
|
||||
const Icon = s.icon;
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className="hover:bg-slate-50/60 transition-colors"
|
||||
>
|
||||
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
|
||||
{String(row.id).padStart(2, '0')}
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-slate-800 font-medium whitespace-nowrap">
|
||||
{row.service}
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-slate-800 font-semibold tabular-nums whitespace-nowrap">
|
||||
{row.amount.toLocaleString()} UZS
|
||||
</td>
|
||||
<td className="px-5 py-3.5 tabular-nums whitespace-nowrap">
|
||||
{row.discount > 0 ? (
|
||||
<span className="text-emerald-600 font-medium">
|
||||
-{row.discount.toLocaleString()} UZS
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-300">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-slate-500 whitespace-nowrap">
|
||||
{row.date}
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${s.cls}`}
|
||||
>
|
||||
<Icon size={12} />
|
||||
{s.label}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
128
src/widgets/cabinet/ui/tables/PlagiatTable.tsx
Normal file
128
src/widgets/cabinet/ui/tables/PlagiatTable.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import { Download, Clock, CheckCircle, XCircle } from 'lucide-react';
|
||||
import type { PlagiatCheck } from '../../lib/types';
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const STATUS_MAP = {
|
||||
completed: {
|
||||
label: 'Yakunlandi',
|
||||
icon: CheckCircle,
|
||||
cls: 'text-emerald-600 bg-emerald-50',
|
||||
},
|
||||
pending: {
|
||||
label: 'Kutilmoqda',
|
||||
icon: Clock,
|
||||
cls: 'text-amber-600 bg-amber-50',
|
||||
},
|
||||
failed: { label: 'Xato', icon: XCircle, cls: 'text-red-600 bg-red-50' },
|
||||
} as const;
|
||||
|
||||
const PercentBadge: React.FC<{ value: number }> = ({ value }) => {
|
||||
const cls =
|
||||
value < 15
|
||||
? 'text-emerald-700 bg-emerald-50'
|
||||
: value < 30
|
||||
? 'text-amber-700 bg-amber-50'
|
||||
: 'text-red-700 bg-red-50';
|
||||
return (
|
||||
<span
|
||||
className={`inline-block px-2 py-0.5 rounded-md text-xs font-semibold ${cls}`}
|
||||
>
|
||||
{value}%
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Component ─────────────────────────────────────────────────────────────────
|
||||
|
||||
interface PlagiatTableProps {
|
||||
data: PlagiatCheck[];
|
||||
}
|
||||
|
||||
export const PlagiatTable: React.FC<PlagiatTableProps> = ({ data }) => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900">Plagiat tekshiruvlar</h2>
|
||||
<p className="text-sm text-slate-500 mt-0.5">
|
||||
{data.length} ta tekshiruv
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-100">
|
||||
{['#', 'Fayl', 'Turi', '%', 'Sana', 'Holat', 'Yuklab olish'].map(
|
||||
(h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap last:text-center"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
),
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{data.map((row) => {
|
||||
const s = STATUS_MAP[row.status];
|
||||
const Icon = s.icon;
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className="hover:bg-slate-50/60 transition-colors"
|
||||
>
|
||||
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
|
||||
{String(row.id).padStart(2, '0')}
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
<span className="text-slate-800 font-medium max-w-[150px] truncate block">
|
||||
{row.file}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-slate-500 whitespace-nowrap">
|
||||
{row.type}
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
{row.status === 'pending' ? (
|
||||
<span className="text-slate-300 text-xs">—</span>
|
||||
) : (
|
||||
<PercentBadge value={row.percent} />
|
||||
)}
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-slate-500 whitespace-nowrap">
|
||||
{row.date}
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${s.cls}`}
|
||||
>
|
||||
<Icon size={12} />
|
||||
{s.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-center">
|
||||
{row.downloadUrl ? (
|
||||
<a
|
||||
href={row.downloadUrl}
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 transition-colors"
|
||||
aria-label="Yuklab olish"
|
||||
>
|
||||
<Download size={15} />
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-slate-200 text-xs">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
128
src/widgets/cabinet/ui/tables/SiTable.tsx
Normal file
128
src/widgets/cabinet/ui/tables/SiTable.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import { FileText, Clock, CheckCircle, XCircle } from 'lucide-react';
|
||||
import type { SiCheck } from '../../lib/types';
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const STATUS_MAP = {
|
||||
completed: {
|
||||
label: 'Yakunlandi',
|
||||
icon: CheckCircle,
|
||||
cls: 'text-emerald-600 bg-emerald-50',
|
||||
},
|
||||
pending: {
|
||||
label: 'Kutilmoqda',
|
||||
icon: Clock,
|
||||
cls: 'text-amber-600 bg-amber-50',
|
||||
},
|
||||
failed: { label: 'Xato', icon: XCircle, cls: 'text-red-600 bg-red-50' },
|
||||
} as const;
|
||||
|
||||
const SiPercentBadge: React.FC<{ value: number }> = ({ value }) => {
|
||||
const cls =
|
||||
value < 20
|
||||
? 'text-emerald-700 bg-emerald-50'
|
||||
: value < 40
|
||||
? 'text-amber-700 bg-amber-50'
|
||||
: 'text-red-700 bg-red-50';
|
||||
return (
|
||||
<span
|
||||
className={`inline-block px-2 py-0.5 rounded-md text-xs font-semibold ${cls}`}
|
||||
>
|
||||
{value}%
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Component ─────────────────────────────────────────────────────────────────
|
||||
|
||||
interface SiTableProps {
|
||||
data: SiCheck[];
|
||||
}
|
||||
|
||||
export const SiTable: React.FC<SiTableProps> = ({ data }) => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900">SI detektor</h2>
|
||||
<p className="text-sm text-slate-500 mt-0.5">
|
||||
{data.length} ta tekshiruv
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-100">
|
||||
{['#', 'Fayl', "So'z", 'SI%', 'Sana', 'Holat', 'Hisobot'].map(
|
||||
(h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap last:text-center"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
),
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{data.map((row) => {
|
||||
const s = STATUS_MAP[row.status];
|
||||
const Icon = s.icon;
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className="hover:bg-slate-50/60 transition-colors"
|
||||
>
|
||||
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
|
||||
{String(row.id).padStart(2, '0')}
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
<span className="text-slate-800 font-medium max-w-[150px] truncate block">
|
||||
{row.file}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-slate-500 tabular-nums">
|
||||
{row.words.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
{row.status === 'pending' ? (
|
||||
<span className="text-slate-300 text-xs">—</span>
|
||||
) : (
|
||||
<SiPercentBadge value={row.siPercent} />
|
||||
)}
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-slate-500 whitespace-nowrap">
|
||||
{row.date}
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${s.cls}`}
|
||||
>
|
||||
<Icon size={12} />
|
||||
{s.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-center">
|
||||
{row.reportUrl ? (
|
||||
<a
|
||||
href={row.reportUrl}
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg text-slate-400 hover:text-violet-600 hover:bg-violet-50 transition-colors"
|
||||
aria-label="Hisobot"
|
||||
>
|
||||
<FileText size={15} />
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-slate-200 text-xs">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -8,9 +8,9 @@ const Footer = () => {
|
||||
// { name: 'Contact', href: '/contact' },
|
||||
// ];
|
||||
return (
|
||||
<section className="py-10">
|
||||
<div className="custom-container">
|
||||
<div className=" flex flex-col justify-between gap-4 border-t pt-8 text-center text-sm font-medium text-muted-foreground lg:flex-row lg:items-center lg:text-left">
|
||||
<section className="py-5">
|
||||
<div className="max-w-6xl w-full mx-auto">
|
||||
<div className=" flex flex-col justify-between gap-4 text-center text-sm font-medium text-muted-foreground lg:flex-row lg:items-center lg:text-left">
|
||||
<p>{t('copyright', { year: new Date().getFullYear() })}</p>
|
||||
<ul className="flex justify-center gap-4 lg:justify-start">
|
||||
<li className="hover:text-primary">
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
} from '@/shared/ui/navigation-menu';
|
||||
import SubMenuLink from './SubMenuLink';
|
||||
@@ -30,7 +31,7 @@ function AuthButtons() {
|
||||
};
|
||||
|
||||
const userItem = [
|
||||
{ title: t('profile'), url: '/profile', icon: User },
|
||||
{ title: t('profile'), url: '/cabinet', icon: User },
|
||||
{ title: t('logout'), url: '/', icon: LogOut },
|
||||
];
|
||||
|
||||
@@ -64,23 +65,32 @@ function AuthButtons() {
|
||||
<div className="sm:flex hidden">
|
||||
<ChangeLang />
|
||||
</div>
|
||||
<NavigationMenu>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger className="text-xl">
|
||||
{localUser.name} {localUser.surname}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className="bg-popover text-popover-foreground">
|
||||
{userItem.map((subItem) => (
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
key={subItem.title}
|
||||
className="w-80"
|
||||
>
|
||||
<SubMenuLink logOut={clearTokens} item={subItem} />
|
||||
</NavigationMenuLink>
|
||||
))}
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenu viewport={true}>
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger className="text-lg">
|
||||
{localUser.name}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className="bg-popover text-popover-foreground">
|
||||
{userItem.map((subItem) => (
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
key={subItem.title}
|
||||
className="w-80"
|
||||
>
|
||||
<SubMenuLink
|
||||
logOut={() => {
|
||||
if (subItem.url !== '/cabinet') {
|
||||
clearTokens();
|
||||
}
|
||||
}}
|
||||
item={subItem}
|
||||
/>
|
||||
</NavigationMenuLink>
|
||||
))}
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ const Navbar = () => {
|
||||
const menu = getMenu(t);
|
||||
|
||||
return (
|
||||
<section className="py-4 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">
|
||||
{/* Desktop Menu */}
|
||||
<nav className="justify-between items-center flex max-sm:flex-col gap-5">
|
||||
@@ -34,10 +34,10 @@ const Navbar = () => {
|
||||
>
|
||||
<Image
|
||||
src={Logo_image}
|
||||
className="min-h-4"
|
||||
className="min-h-2"
|
||||
alt="Anti-Plagiat.uz"
|
||||
width={200}
|
||||
height={50}
|
||||
width={140}
|
||||
height={10}
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex sm:hidden items-center justify-center">
|
||||
|
||||
Reference in New Issue
Block a user