search
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/Service";
|
import * as api from "../service/Service";
|
||||||
import { ProductsResponse } from "@/types/products.types";
|
import { ProductsResponse } from "@/types/products.types";
|
||||||
|
import { ProductSearchResponse } from "../types/Types";
|
||||||
|
|
||||||
interface UseProductsDataParams {
|
interface UseProductsDataParams {
|
||||||
page?: number;
|
page?: number;
|
||||||
@@ -35,6 +36,14 @@ export const useCategoryProductsData = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useProductSearch = (search: string) => {
|
||||||
|
return useQuery<ProductSearchResponse>({
|
||||||
|
queryKey: ["product-search", search],
|
||||||
|
queryFn: () => api.productSearch(search),
|
||||||
|
enabled: search.length > 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useAddWishlist = () => {
|
export const useAddWishlist = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from "@/config/axios";
|
import axios from "@/config/axios";
|
||||||
import { ProductsResponse } from "@/types/products.types";
|
import { ProductsResponse } from "@/types/products.types";
|
||||||
import { WishListType } from "../types/Types";
|
import { ProductSearchResponse, WishListType } from "../types/Types";
|
||||||
|
|
||||||
export const getProducts = async (params?: {
|
export const getProducts = async (params?: {
|
||||||
page?: number;
|
page?: number;
|
||||||
@@ -62,3 +62,10 @@ export const removeWishlist = async (params: WishListType) => {
|
|||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const productSearch = async (
|
||||||
|
search: string
|
||||||
|
): Promise<ProductSearchResponse> => {
|
||||||
|
const { data } = await axios.get(`/product/search?q=${search}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,3 +2,27 @@ export type WishListType = {
|
|||||||
productId: string;
|
productId: string;
|
||||||
variantId: string;
|
variantId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProductSearchItem = {
|
||||||
|
_id: number;
|
||||||
|
title_fa: string;
|
||||||
|
title_en: string;
|
||||||
|
model: string;
|
||||||
|
description: string;
|
||||||
|
variants: string[];
|
||||||
|
imagesUrl: {
|
||||||
|
cover: string;
|
||||||
|
list: string[];
|
||||||
|
};
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductSearchResults = {
|
||||||
|
products: ProductSearchItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductSearchResponse = {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: ProductSearchResults;
|
||||||
|
};
|
||||||
|
|||||||
+16
-44
@@ -1,9 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import Input from '@/components/Input'
|
|
||||||
import MenuItem from '@/components/MenuItem'
|
import MenuItem from '@/components/MenuItem'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { DocumentText, Home, InfoCircle, Messages1, SearchNormal1 } from 'iconsax-react'
|
import { DocumentText, Home, InfoCircle, Messages1 } from 'iconsax-react'
|
||||||
import { FC, Fragment, useEffect, useState } from 'react'
|
import { FC, Fragment, useEffect } from 'react'
|
||||||
import Menu from './components/Menu'
|
import Menu from './components/Menu'
|
||||||
import { useSharedStore } from './store/sharedStore'
|
import { useSharedStore } from './store/sharedStore'
|
||||||
import { getToken } from '@/config/func'
|
import { getToken } from '@/config/func'
|
||||||
@@ -11,10 +10,11 @@ import Account from './components/Account'
|
|||||||
import Cart from './components/Cart'
|
import Cart from './components/Cart'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import MobileSearch from './components/MobileSearch'
|
||||||
|
import DesktopSearch from './components/DesktopSearch'
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
|
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
|
||||||
const { setIsLogin } = useSharedStore()
|
const { setIsLogin } = useSharedStore()
|
||||||
|
|
||||||
const handleSetIsLogin = async () => {
|
const handleSetIsLogin = async () => {
|
||||||
@@ -31,41 +31,17 @@ const Header: FC = () => {
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<div className='fixed top-0 bg-white w-full z-[9998] border-b border-gray-100 max-w-full'>
|
<div className='fixed top-0 bg-white w-full z-[9998] border-b border-gray-100 max-w-full'>
|
||||||
{/* موبایل: لوگو و آیکون جستجو */}
|
{/* موبایل: لوگو و آیکون جستجو */}
|
||||||
<div className='md:hidden'>
|
<div className='md:hidden flex items-center justify-between relative'>
|
||||||
{!isSearchOpen ? (
|
<Link href='/' className='px-4 py-4'>
|
||||||
<div className='flex items-center justify-between px-4 py-4'>
|
<Image
|
||||||
<Link href='/'>
|
src='/images/logo.png'
|
||||||
<Image
|
alt='logo'
|
||||||
src='/images/logo.png'
|
width={120}
|
||||||
alt='logo'
|
height={50}
|
||||||
width={120}
|
className='max-w-[120px] w-[100px] max-h-[50px] object-contain'
|
||||||
height={50}
|
/>
|
||||||
className='max-w-[120px] w-[100px] max-h-[50px] object-contain'
|
</Link>
|
||||||
/>
|
<MobileSearch />
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsSearchOpen(true)}
|
|
||||||
className='p-2 rounded-lg hover:bg-gray-50'
|
|
||||||
>
|
|
||||||
<SearchNormal1 size={24} color='#8C90A3' />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='flex items-center gap-2 px-4 py-4'>
|
|
||||||
<Input
|
|
||||||
className='w-full'
|
|
||||||
variant='search'
|
|
||||||
placeholder='جستجو'
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsSearchOpen(false)}
|
|
||||||
className='text-sm text-gray-600 whitespace-nowrap px-2'
|
|
||||||
>
|
|
||||||
لغو
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* دسکتاپ: هدر کامل */}
|
{/* دسکتاپ: هدر کامل */}
|
||||||
@@ -80,11 +56,7 @@ const Header: FC = () => {
|
|||||||
className='max-w-[100px] w-[100px] max-h-[40px] object-contain'
|
className='max-w-[100px] w-[100px] max-h-[40px] object-contain'
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<Input
|
<DesktopSearch />
|
||||||
className='w-[300px] lg:w-[500px]'
|
|
||||||
variant='search'
|
|
||||||
placeholder='جستجو'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex items-center gap-6'>
|
<div className='flex items-center gap-6'>
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
'use client'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import { FC, useState, useRef, useEffect } from 'react'
|
||||||
|
import { useProductSearch } from '@/app/products/hooks/useProductsData'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
|
const DesktopSearch: FC = () => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const inputRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 })
|
||||||
|
|
||||||
|
const { data: searchResults, isLoading } = useProductSearch(searchQuery)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearchQuery(value)
|
||||||
|
setIsOpen(value.length > 0)
|
||||||
|
|
||||||
|
if (value.length > 0 && inputRef.current) {
|
||||||
|
const rect = inputRef.current.getBoundingClientRect()
|
||||||
|
setDropdownPosition({
|
||||||
|
top: rect.bottom + window.scrollY,
|
||||||
|
left: rect.left + window.scrollX,
|
||||||
|
width: rect.width
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProductClick = (url: string) => {
|
||||||
|
router.push(url)
|
||||||
|
setSearchQuery('')
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
if (inputRef.current && !inputRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
const rect = inputRef.current.getBoundingClientRect()
|
||||||
|
setDropdownPosition({
|
||||||
|
top: rect.bottom + window.scrollY,
|
||||||
|
left: rect.left + window.scrollX,
|
||||||
|
width: rect.width
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
window.addEventListener('scroll', updatePosition, true)
|
||||||
|
window.addEventListener('resize', updatePosition)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
window.removeEventListener('scroll', updatePosition, true)
|
||||||
|
window.removeEventListener('resize', updatePosition)
|
||||||
|
}
|
||||||
|
}, [isOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='relative w-[300px] lg:w-[500px]' ref={inputRef}>
|
||||||
|
<Input
|
||||||
|
className='w-full'
|
||||||
|
variant='search'
|
||||||
|
placeholder='جستجو'
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mounted && isOpen && searchQuery.length > 0 && createPortal(
|
||||||
|
<div
|
||||||
|
ref={dropdownRef}
|
||||||
|
className='fixed bg-white rounded-xl shadow-lg border border-gray-200 max-h-[400px] overflow-y-auto z-[99999]'
|
||||||
|
style={{
|
||||||
|
top: `${dropdownPosition.top + 8}px`,
|
||||||
|
left: `${dropdownPosition.left}px`,
|
||||||
|
width: `${dropdownPosition.width}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className='p-4 text-center text-gray-500'>
|
||||||
|
در حال جستجو...
|
||||||
|
</div>
|
||||||
|
) : searchResults?.results?.products?.length ? (
|
||||||
|
<div className='py-2'>
|
||||||
|
{searchResults.results.products.map((product) => (
|
||||||
|
<div
|
||||||
|
key={product._id}
|
||||||
|
onClick={() => handleProductClick(product.url)}
|
||||||
|
className='flex items-center gap-3 px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors'
|
||||||
|
>
|
||||||
|
<div className='w-[50px] h-[50px] flex-shrink-0'>
|
||||||
|
<Image
|
||||||
|
src={product.imagesUrl.cover}
|
||||||
|
alt={product.title_fa}
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
className='rounded-lg object-cover w-full h-full'
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex-1 min-w-0'>
|
||||||
|
<h3 className='text-sm font-medium text-gray-900 truncate'>
|
||||||
|
{product.title_fa}
|
||||||
|
</h3>
|
||||||
|
<p className='text-xs text-gray-500 truncate'>
|
||||||
|
{product.model}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='p-4 text-center text-gray-500'>
|
||||||
|
محصولی یافت نشد
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DesktopSearch
|
||||||
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
'use client'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import { SearchNormal1 } from 'iconsax-react'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useProductSearch } from '@/app/products/hooks/useProductsData'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
const MobileSearch: FC = () => {
|
||||||
|
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const { data: searchResults, isLoading } = useProductSearch(searchQuery)
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearchQuery(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsSearchOpen(false)
|
||||||
|
setSearchQuery('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProductClick = (url: string) => {
|
||||||
|
router.push(url)
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSearchOpen) {
|
||||||
|
return (
|
||||||
|
<div className='fixed inset-0 bg-white z-50 flex flex-col'>
|
||||||
|
<div className='flex items-center gap-2 px-4 py-4 border-b border-gray-200'>
|
||||||
|
<Input
|
||||||
|
className='w-full'
|
||||||
|
variant='search'
|
||||||
|
placeholder='جستجو'
|
||||||
|
autoFocus
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className='text-sm text-gray-600 whitespace-nowrap px-2'
|
||||||
|
>
|
||||||
|
لغو
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-1 overflow-y-auto'>
|
||||||
|
{searchQuery.length > 0 && (
|
||||||
|
<>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className='p-4 text-center text-gray-500'>
|
||||||
|
در حال جستجو...
|
||||||
|
</div>
|
||||||
|
) : searchResults?.results?.products?.length ? (
|
||||||
|
<div className='py-2'>
|
||||||
|
{searchResults.results.products.map((product) => (
|
||||||
|
<div
|
||||||
|
key={product._id}
|
||||||
|
onClick={() => handleProductClick(product.url)}
|
||||||
|
className='flex items-center gap-3 px-4 py-3 border-b border-gray-100 active:bg-gray-50'
|
||||||
|
>
|
||||||
|
<div className='w-[60px] h-[60px] flex-shrink-0'>
|
||||||
|
<Image
|
||||||
|
src={product.imagesUrl.cover}
|
||||||
|
alt={product.title_fa}
|
||||||
|
width={60}
|
||||||
|
height={60}
|
||||||
|
className='rounded-lg object-cover w-full h-full'
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex-1 min-w-0'>
|
||||||
|
<h3 className='text-sm font-medium text-gray-900 line-clamp-2'>
|
||||||
|
{product.title_fa}
|
||||||
|
</h3>
|
||||||
|
<p className='text-xs text-gray-500 truncate mt-1'>
|
||||||
|
{product.model}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='p-4 text-center text-gray-500'>
|
||||||
|
محصولی یافت نشد
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSearchOpen(true)}
|
||||||
|
className='p-2 rounded-lg hover:bg-gray-50'
|
||||||
|
>
|
||||||
|
<SearchNormal1 size={24} color='#8C90A3' />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MobileSearch
|
||||||
|
|
||||||
Reference in New Issue
Block a user