header + navigation and menu
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCategories } from "../service/ShareService";
|
||||
|
||||
export const useCategories = () => {
|
||||
return useQuery({
|
||||
queryKey: ["categories"],
|
||||
queryFn: getCategories,
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
|
||||
Reference in New Issue
Block a user