list foods

This commit is contained in:
hamid zarghami
2025-11-15 12:26:31 +03:30
parent b7562f911d
commit 59e2e62c56
8 changed files with 250 additions and 85 deletions
+39 -83
View File
@@ -1,10 +1,31 @@
import { type FC } from 'react'
import Table from '@/components/Table'
import { Add, Eye } from 'iconsax-react'
import Filters from '@/components/Filters'
import { Add } from 'iconsax-react'
import Button from '@/components/Button'
import { useGetFoods } from './hooks/useFoodData'
import { useFoodFilters } from './hooks/useFoodFilters'
import { getFoodTableColumns } from './components/FoodTableColumns'
import { useFoodFiltersFields } from './components/FoodFiltersFields'
const FoodsList: FC = () => {
const {
filters,
currentPage,
apiParams,
handleFiltersChange,
handlePageChange,
limit,
} = useFoodFilters()
const { data: foodsData, isLoading } = useGetFoods(apiParams)
const foods = foodsData?.data || []
const columns = getFoodTableColumns()
const filterFields = useFoodFiltersFields()
const totalPages = Math.ceil(foods.length / limit) || 1
return (
<div className='mt-5'>
<div className='flex justify-between items-center'>
@@ -18,89 +39,24 @@ const FoodsList: FC = () => {
</div>
<div className='mt-8'>
<Table
columns={[
{
key: 'image',
title: 'تصویر',
render: (item) => {
return (
<div className='w-10 h-10 rounded-full overflow-hidden'>
<img src={item.image} alt={item.name} className='w-full h-full object-cover' />
</div>
)
}
},
{
key: 'name',
title: 'نام غذا',
},
{
key: 'category',
title: 'دسته بندی',
},
{
key: 'price',
title: 'قیمت ',
},
{
key: 'preparationTime',
title: 'زمان آماده سازی',
},
{
key: 'status',
title: 'وضعیت',
render: (item) => {
return (
<div className='h-6 w-fit flex items-center bg-[#FFEDCA] text-[#FF7B00] rounded-full px-2.5 text-xs'>
{item.status}
</div>
)
}
},
{
key: 'view',
title: '',
render: () => {
return (
<div className='flex gap-2 items-center'>
<Eye size={20} color='#8C90A3' />
</div>
)
}
},
]}
data={[
{
id: 1,
image: 'https://picsum.photos/seed/food1/200',
name: 'کباب کوبیده',
category: 'غذای ایرانی',
price: '۱۵۰,۰۰۰ تومان',
preparationTime: '۳۰ دقیقه',
status: 'موجود',
},
{
id: 2,
image: 'https://picsum.photos/seed/food2/200',
name: 'پیتزا پپرونی',
category: 'فست فود',
price: '۲۰۰,۰۰۰ تومان',
preparationTime: '۴۵ دقیقه',
status: 'موجود',
},
{
id: 3,
image: 'https://picsum.photos/seed/food3/200',
name: 'سالاد سزار',
category: 'پیش غذا',
price: '۸۰,۰۰۰ تومان',
preparationTime: '۱۵ دقیقه',
status: 'ناموجود',
}
]}
<Filters
fields={filterFields}
onChange={handleFiltersChange}
initialValues={filters}
searchField="search"
/>
</div>
<Table
columns={columns}
data={foods}
isLoading={isLoading}
pagination={{
currentPage,
totalPages,
onPageChange: handlePageChange,
}}
/>
</div>
)
}
@@ -0,0 +1,13 @@
import { useMemo } from 'react'
import type { FieldType } from '@/components/Filters'
export const useFoodFiltersFields = (): FieldType[] => {
return useMemo(() => [
{
type: 'input',
name: 'search',
placeholder: 'جستجو',
},
], [])
}
@@ -0,0 +1,81 @@
import { Eye } from 'iconsax-react'
import type { ColumnType } from '@/components/types/TableTypes'
import type { Food } from '../types/Types'
import { formatPrice, formatTime } from '../utils/formatters'
export const getFoodTableColumns = (): ColumnType<Food>[] => {
return [
{
key: 'image',
title: 'تصویر',
render: (item: Food) => {
const imageUrl = item.images && item.images.length > 0 ? item.images[0] : ''
return (
<div className='w-10 h-10 rounded-full overflow-hidden bg-gray-200'>
{imageUrl ? (
<img src={imageUrl} alt={item.title} className='w-full h-full object-cover' />
) : (
<div className='w-full h-full flex items-center justify-center text-gray-400 text-xs'>
بدون تصویر
</div>
)}
</div>
)
}
},
{
key: 'title',
title: 'نام غذا',
},
{
key: 'categories',
title: 'دسته بندی',
render: (item: Food) => {
return item.categories && item.categories.length > 0
? item.categories.map(cat => cat.title).join(', ')
: '-'
}
},
{
key: 'price',
title: 'قیمت',
render: (item: Food) => {
return formatPrice(item.price)
}
},
{
key: 'prepareTime',
title: 'زمان آماده سازی',
render: (item: Food) => {
return formatTime(item.prepareTime)
}
},
{
key: 'isActive',
title: 'وضعیت',
render: (item: Food) => {
return (
<div className={`h-6 w-fit flex items-center rounded-full px-2.5 text-xs ${
item.isActive
? 'bg-[#FFEDCA] text-[#FF7B00]'
: 'bg-gray-200 text-gray-600'
}`}>
{item.isActive ? 'فعال' : 'غیرفعال'}
</div>
)
}
},
{
key: 'view',
title: '',
render: () => {
return (
<div className='flex gap-2 items-center'>
<Eye size={20} color='#8C90A3' />
</div>
)
}
},
]
}
+8
View File
@@ -1,5 +1,6 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/FoodService";
import type { GetFoodsParams } from "../service/FoodService";
export const useCreateFood = () => {
return useMutation({
@@ -7,6 +8,13 @@ export const useCreateFood = () => {
});
};
export const useGetFoods = (params?: GetFoodsParams) => {
return useQuery({
queryKey: ["foods", params],
queryFn: () => api.getFoods(params),
});
};
export const useGetCategories = () => {
return useQuery({
queryKey: ["categories"],
+44
View File
@@ -0,0 +1,44 @@
import { useState, useMemo } from 'react'
import type { FilterValues } from '@/components/Filters'
import type { GetFoodsParams } from '../service/FoodService'
const DEFAULT_PAGE = 1
const DEFAULT_LIMIT = 10
export const useFoodFilters = () => {
const [filters, setFilters] = useState<FilterValues>({})
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE)
const [limit] = useState(DEFAULT_LIMIT)
const apiParams = useMemo<GetFoodsParams>(() => {
const params: GetFoodsParams = {
page: currentPage,
limit: limit,
}
if (filters.search) {
params.search = filters.search
}
return params
}, [filters, currentPage, limit])
const handleFiltersChange = (newFilters: FilterValues) => {
setFilters(newFilters)
setCurrentPage(DEFAULT_PAGE)
}
const handlePageChange = (page: number) => {
setCurrentPage(page)
}
return {
filters,
currentPage,
apiParams,
handleFiltersChange,
handlePageChange,
limit,
}
}
+22 -1
View File
@@ -1,11 +1,32 @@
import axios from "@/config/axios";
import type { CreateFoodType, GetCategoriesResponseType } from "../types/Types";
import type {
CreateFoodType,
GetCategoriesResponseType,
GetFoodsResponseType,
} from "../types/Types";
export const createFood = async (params: CreateFoodType) => {
const { data } = await axios.post(`/foods`, params);
return data;
};
export type GetFoodsParams = {
page?: number;
limit?: number;
search?: string;
orderBy?: string;
order?: "asc" | "desc";
categoryId?: string;
isActive?: boolean;
};
export const getFoods = async (
params?: GetFoodsParams
): Promise<GetFoodsResponseType> => {
const { data } = await axios.get<GetFoodsResponseType>(`/foods`, { params });
return data;
};
export const getCategories = async (): Promise<GetCategoriesResponseType> => {
const { data } = await axios.get<GetCategoriesResponseType>(`/categories`);
return data;
+31 -1
View File
@@ -29,13 +29,43 @@ export type CreateFoodType = {
export type Category = {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
title: string;
isActive: boolean;
restId: string;
avatarUrl: string;
};
export type Food = {
id: string;
createdAt: string;
updatedAt: string;
foods: unknown[];
deletedAt: string | null;
title: string;
desc: string | null;
content: string[];
price: number;
points: number;
order: number | null;
prepareTime: number;
sat: boolean;
sun: boolean;
mon: boolean;
breakfast: boolean;
noon: boolean;
dinner: boolean;
stock: number;
stockDefault: number;
isActive: boolean;
images: string[];
inPlaceServe: boolean;
pickupServe: boolean;
rate: number;
discount: number;
categories: Category[];
};
export type GetCategoriesResponseType = IResponse<Category[]>;
export type GetFoodsResponseType = IResponse<Food[]>;
+12
View File
@@ -0,0 +1,12 @@
export const formatPrice = (price: number): string => {
return new Intl.NumberFormat('fa-IR', {
style: 'currency',
currency: 'IRR',
minimumFractionDigits: 0,
}).format(price)
}
export const formatTime = (minutes: number): string => {
return `${minutes} دقیقه`
}