header + navigation and menu

This commit is contained in:
hamid zarghami
2025-08-03 15:53:06 +03:30
parent 61a934074c
commit 76500fb337
46 changed files with 1042 additions and 154 deletions
+46
View File
@@ -0,0 +1,46 @@
import Input from '@/components/Input'
import { Separator } from '@/components/ui/separator'
import { DocumentText, Home, Profile, ShoppingCart } from 'iconsax-react'
import { FC } from 'react'
import Menu from './components/Menu'
const Header: FC = () => {
return (
<div className='fixed top-0 bg-white w-full'>
<div className='flex items-center justify-between px-6 py-[18px]'>
<div className='flex gap-[30px] items-center'>
<div>LOGO</div>
<Input className='w-[500px]' variant='search' placeholder='جستجو' />
</div>
<div className='flex items-center gap-6'>
<div className='h-10 border border-border py-3 px-4 rounded-[12px] flex items-center gap-2.5'>
<Profile size={20} color='#333333' />
<div className='text-sm text-[#333333]'>حساب کاربری</div>
</div>
<Separator style={{ height: 25 }} orientation='vertical' />
<div className='h-10 p-3 rounded-[12px] border border-border flex items-center gap-2.5'>
<ShoppingCart size={20} color='#333333' />
</div>
</div>
</div>
<div className='mt-7 text-sm px-6 flex items-center gap-7'>
<Menu />
<Separator style={{ height: 20 }} orientation='vertical' />
<div className='flex items-center gap-2.5'>
<Home size={20} color='#333333' />
<div className='text-[#333333]'>صفحه نخست</div>
</div>
<div className='flex items-center gap-2.5'>
<DocumentText size={20} color='#333333' />
<div className='text-[#333333]'>مجله</div>
</div>
</div>
</div>
)
}
export default Header
+117
View File
@@ -0,0 +1,117 @@
'use client'
import { HambergerMenu, ArrowLeft2 } from 'iconsax-react'
import { FC, useState, useRef, useEffect } from 'react'
import Image from 'next/image'
import { useCategories } from '../hooks/useShareData'
import { Category } from '../types/SharedTypes'
const Menu: FC = () => {
const [isOpen, setIsOpen] = useState(false)
const [hoveredPath, setHoveredPath] = useState<Category[]>([])
const { data, isLoading } = useCategories()
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false)
setHoveredPath([])
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const parentCategories = data?.results?.data?.filter(cat => !cat.parent) || []
const handleCategoryHover = (category: Category, level: number) => {
const newPath = [...hoveredPath.slice(0, level), category]
setHoveredPath(newPath)
}
const renderCategoryLevel = (categories: Category[], level: number) => {
return (
<div className='w-64 border-l border-gray-200 last:border-l-0 flex flex-col min-h-0'>
<div className='p-2 overflow-y-auto max-h-[calc(100vh-200px)]'>
{categories.map((category) => (
<div
key={category._id}
className={`p-3 rounded-lg cursor-pointer transition-colors hover:bg-gray-50 ${hoveredPath[level]?._id === category._id ? 'bg-gray-50' : ''
}`}
onMouseEnter={() => handleCategoryHover(category, level)}
>
<div className='flex items-center gap-3'>
{category.icon && (
<Image
src={category.icon}
alt={category.title_fa}
width={20}
height={20}
className='w-5 h-5 object-contain'
/>
)}
<span className='text-sm text-gray-800 flex-1'>{category.title_fa}</span>
{category.children?.length > 0 && (
<ArrowLeft2 size={16} color='#666' />
)}
</div>
</div>
))}
</div>
</div>
)
}
return (
<div className='relative' ref={menuRef}>
<div
className='flex items-center gap-2.5 cursor-pointer'
onClick={() => setIsOpen(!isOpen)}
onMouseEnter={() => setIsOpen(true)}
>
<HambergerMenu size={24} color='#333333' />
<div className='text-[#333333]'>دسته بندی کالاها</div>
</div>
{isOpen && (
<div
className='absolute top-full right-0 mt-2 bg-white rounded-xl shadow-lg border border-gray-200 z-50 flex max-w-[90vw] max-h-screen overflow-hidden'
onMouseLeave={() => {
setIsOpen(false)
setHoveredPath([])
}}
>
{/* دسته‌های اصلی (سطح اول) */}
{isLoading ? (
<div className='w-64 border-l border-gray-200 flex items-center justify-center h-40'>
<div className='text-gray-500'>در حال بارگذاری...</div>
</div>
) : parentCategories.length > 0 ? (
<>
{renderCategoryLevel(parentCategories, 0)}
{/* سطوح بعدی (recursive) */}
{hoveredPath.map((category, index) => {
if (category.children && category.children.length > 0) {
return (
<div key={`level-${index + 1}-${category._id}`}>
{renderCategoryLevel(category.children, index + 1)}
</div>
)
}
return null
})}
</>
) : (
<div className='w-64 border-l border-gray-200 flex items-center justify-center h-40'>
<div className='text-gray-500'>دستهبندیای یافت نشد</div>
</div>
)}
</div>
)}
</div>
)
}
export default Menu
+9
View File
@@ -0,0 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import { getCategories } from "../service/ShareService";
export const useCategories = () => {
return useQuery({
queryKey: ["categories"],
queryFn: getCategories,
});
};
+7
View File
@@ -0,0 +1,7 @@
import axios from "@/config/axios";
import { CategoriesResponseType } from "../types/SharedTypes";
export const getCategories = async () => {
const { data } = await axios.get<CategoriesResponseType>("/category");
return data;
};
+33 -3
View File
@@ -7,11 +7,42 @@ export type SharedStoreType = {
};
export interface ApiResponse<T> {
statusCode: number;
status: number;
success: boolean;
data: T;
results: T;
}
export type Category = {
_id: string;
title_fa: string;
title_en: string;
icon: string;
imageUrl: string;
description: string;
variants: number[];
hierarchy: string[];
leaf: boolean;
parent: string | null;
deleted: boolean;
createdAt: string;
updatedAt: string;
url: string;
children: Category[];
theme?: string;
};
export type CategoriesResponseType = ApiResponse<{
data: Category[];
}>;
export type CategoryResponseType = ApiResponse<{
id: string;
name: string;
parentId: string | null;
createdAt: string;
updatedAt: string;
}>;
export type UserMeResponseType = ApiResponse<{
user: {
id: string;
@@ -31,7 +62,6 @@ export type UserMeResponseType = ApiResponse<{
id: string;
createdAt: string;
name: string;
// Add other role properties as needed
}>;
};
}>;