From 5dbe901ac734ba2e7cbd1a0bd6f5a38fa4d14158 Mon Sep 17 00:00:00 2001 From: hossein taromi Date: Sun, 27 Jul 2025 14:45:43 +0330 Subject: [PATCH] feat(categories): implement product categories management system - Add categories models, requests and hooks with full CRUD support - Implement categories list page with search, filtering and pagination - Add category form page for creating and editing categories - Include responsive design for mobile and desktop views - Add real-time preview and validation for better user experience --- .../categories-list/CategoriesListPage.tsx | 295 ++++++++++++++++++ .../category-form/CategoryFormPage.tsx | 191 ++++++++++++ src/pages/categories/core/_hooks.ts | 84 +++++ src/pages/categories/core/_models.ts | 49 +++ src/pages/categories/core/_requests.ts | 79 +++++ 5 files changed, 698 insertions(+) create mode 100644 src/pages/categories/categories-list/CategoriesListPage.tsx create mode 100644 src/pages/categories/category-form/CategoryFormPage.tsx create mode 100644 src/pages/categories/core/_hooks.ts create mode 100644 src/pages/categories/core/_models.ts create mode 100644 src/pages/categories/core/_requests.ts diff --git a/src/pages/categories/categories-list/CategoriesListPage.tsx b/src/pages/categories/categories-list/CategoriesListPage.tsx new file mode 100644 index 0000000..ae7c366 --- /dev/null +++ b/src/pages/categories/categories-list/CategoriesListPage.tsx @@ -0,0 +1,295 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useCategories, useDeleteCategory } from '../core/_hooks'; +import { Category } from '../core/_models'; +import { Button } from "@/components/ui/Button"; +import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; +import { Trash2, Edit3, Plus, FolderOpen, Folder } from "lucide-react"; +import { Modal } from "@/components/ui/Modal"; + +const CategoriesTableSkeleton = () => ( +
+
+
+ + + + + + + + + + + {[...Array(5)].map((_, i) => ( + + + + + + + ))} + +
+ نام دسته‌بندی + + توضیحات + + تاریخ ایجاد + + عملیات +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); + +const CategoriesListPage = () => { + const navigate = useNavigate(); + const [deleteCategoryId, setDeleteCategoryId] = useState(null); + const [filters, setFilters] = useState({ + search: '' + }); + + const { data: categories, isLoading, error } = useCategories(filters); + const { mutate: deleteCategory, isPending: isDeleting } = useDeleteCategory(); + + const handleCreate = () => { + navigate('/categories/create'); + }; + + const handleEdit = (categoryId: number) => { + navigate(`/categories/${categoryId}/edit`); + }; + + const handleDeleteConfirm = () => { + if (deleteCategoryId) { + deleteCategory(deleteCategoryId, { + onSuccess: () => { + setDeleteCategoryId(null); + } + }); + } + }; + + const handleSearchChange = (e: React.ChangeEvent) => { + setFilters(prev => ({ ...prev, search: e.target.value })); + }; + + if (error) { + return ( +
+
+

خطا در بارگذاری دسته‌بندی‌ها

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

+ + مدیریت دسته‌بندی‌ها +

+

+ مدیریت دسته‌بندی‌های محصولات +

+
+ +
+ + {/* Filters */} +
+
+
+ + +
+
+
+ + {/* Categories Table */} + {isLoading ? ( + + ) : ( +
+ {/* Desktop Table */} +
+
+ + + + + + + + + + + {(categories || []).map((category: Category) => ( + + + + + + + ))} + +
+ نام دسته‌بندی + + توضیحات + + تاریخ ایجاد + + عملیات +
+
+ + {category.name} +
+
+
+ {category.description || 'بدون توضیحات'} +
+
+ {new Date(category.created_at).toLocaleDateString('fa-IR')} + +
+ + +
+
+
+
+ + {/* Mobile Cards */} +
+ {(categories || []).map((category: Category) => ( +
+
+
+

+ + {category.name} +

+

+ {category.description || 'بدون توضیحات'} +

+
+
+
+ تاریخ ایجاد: {new Date(category.created_at).toLocaleDateString('fa-IR')} +
+
+ + +
+
+ ))} +
+ + {/* Empty State */} + {(!categories || categories.length === 0) && !isLoading && ( +
+ +

+ دسته‌بندی‌ای موجود نیست +

+

+ برای شروع، اولین دسته‌بندی محصولات خود را ایجاد کنید. +

+
+ +
+
+ )} +
+ )} + + {/* Delete Confirmation Modal */} + setDeleteCategoryId(null)} + title="حذف دسته‌بندی" + > +
+

+ آیا از حذف این دسته‌بندی اطمینان دارید؟ این عمل قابل بازگشت نیست و ممکن است بر محصولاتی که در این دسته‌بندی قرار دارند تأثیر بگذارد. +

+
+ + +
+
+
+
+ ); +}; + +export default CategoriesListPage; \ No newline at end of file diff --git a/src/pages/categories/category-form/CategoryFormPage.tsx b/src/pages/categories/category-form/CategoryFormPage.tsx new file mode 100644 index 0000000..533feee --- /dev/null +++ b/src/pages/categories/category-form/CategoryFormPage.tsx @@ -0,0 +1,191 @@ +import React, { useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; +import { useCategory, useCreateCategory, useUpdateCategory } from '../core/_hooks'; +import { CategoryFormData } from '../core/_models'; +import { Button } from "@/components/ui/Button"; +import { Input } from "@/components/ui/Input"; +import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; +import { ArrowRight, FolderOpen } from "lucide-react"; + +const categorySchema = yup.object({ + name: yup.string().required('نام دسته‌بندی الزامی است').min(2, 'نام دسته‌بندی باید حداقل 2 کاراکتر باشد'), + description: yup.string().optional(), +}); + +const CategoryFormPage = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const isEdit = !!id; + + const { data: category, isLoading: isLoadingCategory } = useCategory(id || '', isEdit); + const { mutate: createCategory, isPending: isCreating } = useCreateCategory(); + const { mutate: updateCategory, isPending: isUpdating } = useUpdateCategory(); + + const isLoading = isCreating || isUpdating; + + const { + register, + handleSubmit, + formState: { errors, isValid, isDirty }, + setValue, + watch + } = useForm({ + resolver: yupResolver(categorySchema) as any, + mode: 'onChange', + defaultValues: { + name: '', + description: '' + } + }); + + const formValues = watch(); + + useEffect(() => { + if (isEdit && category) { + setValue('name', category.name, { shouldValidate: true }); + setValue('description', category.description || '', { shouldValidate: true }); + } + }, [isEdit, category, setValue]); + + const onSubmit = (data: CategoryFormData) => { + if (isEdit && id) { + updateCategory({ + id: parseInt(id), + name: data.name, + description: data.description || undefined + }, { + onSuccess: () => { + navigate('/categories'); + } + }); + } else { + createCategory({ + name: data.name, + description: data.description || undefined + }, { + onSuccess: () => { + navigate('/categories'); + } + }); + } + }; + + const handleBack = () => { + navigate('/categories'); + }; + + if (isEdit && isLoadingCategory) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+ +
+

+ + {isEdit ? 'ویرایش دسته‌بندی' : 'ایجاد دسته‌بندی جدید'} +

+

+ {isEdit ? 'ویرایش اطلاعات دسته‌بندی' : 'اطلاعات دسته‌بندی جدید را وارد کنید'} +

+
+
+ + {/* Form */} +
+
+ + +
+ +