category attibute

This commit is contained in:
hamid zarghami
2025-09-21 15:43:48 +03:30
parent f731f8f73d
commit a18c52358b
12 changed files with 557 additions and 4 deletions
+214
View File
@@ -0,0 +1,214 @@
import { type FC } from 'react'
import { useParams } from 'react-router-dom'
import TitleLine from '../../components/TitleLine'
import Button from '../../components/Button'
import { AttributesModal } from './components'
import { useAttributesLogic } from './hooks/useAttributesLogic'
import { useGetCategoryAttributes } from './hooks/useCategoryData'
import { type CategoryAttributeType } from './types/Types'
import { Add, Edit, Trash } from 'iconsax-react'
const Attributes: FC = () => {
const { id } = useParams()
const { data: attributesData, isLoading: attributesLoading } = useGetCategoryAttributes(id!)
const {
category,
isLoading,
isModalOpen,
newAttribute,
openModal,
closeModal,
setNewAttribute,
addAttribute,
addValueToAttribute,
removeValueFromAttribute,
isAddingLoading
} = useAttributesLogic({ id })
const attributes = attributesData?.results?.data?.category?.attributes || []
if (isLoading) {
return <div>در حال بارگذاری...</div>
}
if (!category) {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto p-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
<TitleLine title="مدیریت ویژگی‌های دسته‌بندی" />
<div className="mt-4 space-y-2">
<h1 className="text-2xl font-bold text-gray-900">
دستهبندی یافت نشد
</h1>
</div>
</div>
</div>
</div>
)
}
if (!category.leaf) {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto p-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
<TitleLine title="مدیریت ویژگی‌های دسته‌بندی" />
<div className="mt-4 space-y-2">
<h1 className="text-2xl font-bold text-gray-900">
{category.title_fa}
</h1>
<p className="text-gray-600">
این دستهبندی امکان داشتن ویژگی ندارد
</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex flex-col items-center justify-center py-12 px-6 border-2 border-dashed border-gray-300 rounded-lg bg-gray-50">
<div className="text-center space-y-4">
<div className="w-16 h-16 mx-auto bg-gray-200 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div>
<h3 className="text-lg font-medium text-gray-900 mb-1">امکان ساخت ویژگی وجود ندارد</h3>
<p className="text-gray-500">ویژگیها فقط برای دستهبندیهای نهایی (leaf) قابل تعریف هستند</p>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto p-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
<TitleLine title="مدیریت ویژگی‌های دسته‌بندی" />
<div className="mt-4 space-y-2">
<h1 className="text-2xl font-bold text-gray-900">
{category.title_fa}
</h1>
<p className="text-gray-600">
مدیریت ویژگیهای دستهبندی
</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-gray-900">
ویژگیها ({attributes.length})
</h3>
<Button
onClick={openModal}
className="flex w-fit items-center gap-2"
>
<Add color='#fff' size={16} />
اضافه کردن ویژگی
</Button>
</div>
{attributesLoading ? (
<div className="text-center py-12">
<div className="text-gray-500">در حال بارگذاری ویژگیها...</div>
</div>
) : attributes.length === 0 ? (
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto bg-gray-200 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-1">هنوز ویژگیای اضافه نشده</h3>
<p className="text-gray-500 mb-4">ویژگیهای دستهبندی به شما کمک میکنند محصولات را بهتر توصیف کنید</p>
<Button
onClick={openModal}
variant="outline"
className="flex items-center gap-2 mx-auto"
>
<Add size={16} />
اضافه کردن اولین ویژگی
</Button>
</div>
) : (
<div className="space-y-4">
{attributes.map((attribute: CategoryAttributeType) => (
<div key={attribute._id} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<h4 className="font-medium text-gray-900">{attribute.title}</h4>
<span className={`px-2 py-1 text-xs rounded-full ${attribute.type === 'text' ? 'bg-blue-100 text-blue-800' :
attribute.type === 'number' ? 'bg-green-100 text-green-800' :
attribute.type === 'select' ? 'bg-purple-100 text-purple-800' :
'bg-gray-100 text-gray-800'
}`}>
{attribute.type === 'text' ? 'متن' :
attribute.type === 'number' ? 'عدد' :
attribute.type === 'select' ? 'انتخابی' :
attribute.type === 'boolean' ? 'بله/خیر' : attribute.type}
</span>
{attribute.required && (
<span className="px-2 py-1 text-xs bg-red-100 text-red-800 rounded-full">
اجباری
</span>
)}
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
className="text-blue-500 hover:text-blue-700"
>
<Edit size={16} />
</Button>
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-700"
>
<Trash size={16} />
</Button>
</div>
</div>
{attribute.hint && (
<p className="text-sm text-gray-600 mb-2">{attribute.hint}</p>
)}
{attribute.type === 'select' && attribute.values.length > 0 && (
<div className="flex flex-wrap gap-1">
{attribute.values.map((value, index) => (
<span
key={index}
className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded"
>
{value.text}
</span>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
<AttributesModal
isOpen={isModalOpen}
onClose={closeModal}
newAttribute={newAttribute}
onAttributeChange={setNewAttribute}
onAdd={addAttribute}
onAddValue={addValueToAttribute}
onRemoveValue={removeValueFromAttribute}
isLoading={isAddingLoading}
/>
</div>
)
}
export default Attributes
+8
View File
@@ -68,6 +68,13 @@ const CategoryList: FC = () => {
<Button className='h-7 text-xs w-fit px-4' label='افزودن تنوع' />
</Link> : <Lock size={20} color='#8C90A3' />}
</Td>
<Td text="">
<div className="flex items-center gap-2">
{category.leaf ? <Link to={`${Pages.category.attributes}${category._id}`}>
<Button className='h-7 text-xs w-fit px-4' label='ویژگی‌ها' variant='outline' />
</Link> : <Lock size={20} color='#8C90A3' />}
</div>
</Td>
<Td text="">
<div className="flex items-center gap-2">
<Link to={`${Pages.category.update}${category._id}`} state={{ from: 'list' }}>
@@ -127,6 +134,7 @@ const CategoryList: FC = () => {
<Td text={'تصویر'} />
<Td text={'توضیحات'} />
<Td text={'تنوع'} />
<Td text={'ویژگی‌ها'} />
<Td text={'عملیات'} />
</tr>
</thead>
@@ -0,0 +1,162 @@
import { type FC, useState } from 'react'
import Button from '../../../components/Button'
import DefaulModal from '../../../components/DefaulModal'
import Input from '../../../components/Input'
import Textarea from '../../../components/Textarea'
import RadioGroup from '../../../components/RadioGroup'
import Switch from '../../../components/Switch'
import { type CreateCategoryAttributeType } from '../types/Types'
import { Add, Trash } from 'iconsax-react'
interface AttributesModalProps {
isOpen: boolean
onClose: () => void
newAttribute: CreateCategoryAttributeType
onAttributeChange: (field: keyof CreateCategoryAttributeType, value: string | boolean | { text: string }[]) => void
onAdd: () => void
onAddValue: (text: string) => void
onRemoveValue: (index: number) => void
isLoading?: boolean
}
const AttributesModal: FC<AttributesModalProps> = ({
isOpen,
onClose,
newAttribute,
onAttributeChange,
onAdd,
onAddValue,
onRemoveValue,
isLoading = false
}) => {
const [newValue, setNewValue] = useState('')
const attributeTypes = [
{ value: 'text', label: 'متن' },
{ value: 'number', label: 'عدد' },
{ value: 'select', label: 'انتخابی' },
{ value: 'boolean', label: 'بله/خیر' }
]
const handleAddValue = () => {
if (newValue.trim()) {
onAddValue(newValue.trim())
setNewValue('')
}
}
const isFormValid = () => {
return !!newAttribute.title.trim() &&
(newAttribute.type !== 'select' || newAttribute.values.length > 0)
}
return (
<DefaulModal
open={isOpen}
close={onClose}
isHeader={true}
title_header="اضافه کردن ویژگی جدید"
width={600}
>
<div className="space-y-6">
<Input
label="عنوان ویژگی"
placeholder="مثال: وزن, رنگ, سایز"
value={newAttribute.title}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onAttributeChange('title', e.target.value)}
required
/>
<div className="space-y-2">
<label className="text-sm font-medium">نوع ویژگی</label>
<RadioGroup
items={attributeTypes}
selected={newAttribute.type}
onChange={(value) => onAttributeChange('type', value)}
/>
</div>
<Textarea
label="راهنما (اختیاری)"
placeholder="توضیحات برای کاربر در مورد این ویژگی"
value={newAttribute.hint}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onAttributeChange('hint', e.target.value)}
rows={3}
/>
<div className="flex items-center justify-between">
<div className="space-y-1">
<label className="text-sm font-medium">اجباری</label>
<p className="text-xs text-gray-500">آیا این ویژگی اجباری است؟</p>
</div>
<Switch
active={newAttribute.required}
onChange={(checked) => onAttributeChange('required', checked)}
/>
</div>
{newAttribute.type === 'select' && (
<div className="space-y-3">
<label className="text-sm font-medium">مقادیر قابل انتخاب</label>
<div className="flex gap-2 items-center">
<Input
placeholder="مقدار جدید اضافه کنید"
value={newValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddValue()}
/>
<Button
onClick={handleAddValue}
variant="outline"
// size="sm"
disabled={!newValue.trim()}
className='w-fit mt-1'
>
<Add size={16} color='#000' />
</Button>
</div>
{newAttribute.values.length > 0 && (
<div className="space-y-2 flex gap-3 items-center flex-wrap max-h-32 overflow-y-auto">
{newAttribute.values.map((value, index) => (
<div key={index} className="flex min-w-[30%] flex-1 items-center justify-between bg-gray-50 p-2 rounded">
<div className="text-sm">{value.text}</div>
<Button
variant="ghost"
size="sm"
onClick={() => onRemoveValue(index)}
className="text-red-500 w-fit hover:text-red-700"
>
<Trash color='red' size={20} />
</Button>
</div>
))}
<div className='min-w-[30%] flex-1 px-2'></div>
<div className='min-w-[30%] flex-1 px-2'></div>
</div>
)}
</div>
)}
<div className="flex gap-3 pt-4 border-t border-border">
<Button
variant="outline"
className="flex-1"
onClick={onClose}
>
انصراف
</Button>
<Button
variant="primary"
className="flex-1"
onClick={onAdd}
disabled={!isFormValid() || isLoading}
>
{isLoading ? 'در حال اضافه کردن...' : 'اضافه کردن'}
</Button>
</div>
</div>
</DefaulModal>
)
}
export default AttributesModal
+1
View File
@@ -8,3 +8,4 @@ export { default as VariantSizesList } from "./VariantSizesList";
export { default as VariantColorsList } from "./VariantColorsList";
export { default as VariantMeteragesList } from "./VariantMeteragesList";
export { default as VariantModal } from "./VariantModal";
export { default as AttributesModal } from "./AttributesModal";
@@ -0,0 +1,102 @@
import { useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { useGetCategoryById, useCreateCategoryAttribute } from './useCategoryData'
import { type CreateCategoryAttributeType } from '../types/Types'
import { toast } from 'react-toastify'
import { extractErrorMessage } from '../../../helpers/utils'
interface UseAttributesLogicProps {
id: string | undefined
}
export const useAttributesLogic = ({ id }: UseAttributesLogicProps) => {
const queryClient = useQueryClient()
const { data, isLoading } = useGetCategoryById(id!)
const createAttributeMutation = useCreateCategoryAttribute()
const [isModalOpen, setIsModalOpen] = useState(false)
const [newAttribute, setNewAttribute] = useState<CreateCategoryAttributeType>({
title: '',
type: 'text',
required: false,
values: []
})
const categoryData = data?.results?.data
const category = categoryData
const resetModal = () => {
setIsModalOpen(false)
setNewAttribute({
title: '',
type: 'text',
required: false,
values: []
})
}
const openModal = () => setIsModalOpen(true)
const closeModal = () => resetModal()
const addAttribute = async () => {
if (!id || !newAttribute.title.trim()) return
createAttributeMutation.mutate(
{ id, data: newAttribute },
{
onSuccess: () => {
toast.success('ویژگی با موفقیت اضافه شد')
resetModal()
queryClient.invalidateQueries({ queryKey: ['category', id] })
queryClient.invalidateQueries({ queryKey: ['category', 'attributes', id] })
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
}
)
}
const updateNewAttribute = (field: keyof CreateCategoryAttributeType, value: any) => {
setNewAttribute(prev => ({
...prev,
[field]: value
}))
}
const addValueToAttribute = (text: string) => {
if (!text.trim()) return
setNewAttribute(prev => ({
...prev,
values: [...prev.values, { text: text.trim() }]
}))
}
const removeValueFromAttribute = (index: number) => {
setNewAttribute(prev => ({
...prev,
values: prev.values.filter((_, i) => i !== index)
}))
}
return {
// Data
category,
isLoading,
// Modal state
isModalOpen,
newAttribute,
// Actions
openModal,
closeModal,
setNewAttribute: updateNewAttribute,
addAttribute,
addValueToAttribute,
removeValueFromAttribute,
// Loading states
isAddingLoading: createAttributeMutation.isPending
}
}
+16 -1
View File
@@ -1,6 +1,6 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import * as api from '../service/CategoryService';
import { type CategoryType } from '../types/Types';
import { type CategoryType, type CreateCategoryAttributeType, type CategoryAttributesResponseType } from '../types/Types';
export const useCreateCategory = () => {
return useMutation({
@@ -66,4 +66,19 @@ export const useDeleteCategoryVariant = () => {
return useMutation({
mutationFn: api.deleteCategoryVariant,
});
};
export const useCreateCategoryAttribute = () => {
return useMutation({
mutationFn: ({ id, data }: { id: string; data: CreateCategoryAttributeType }) =>
api.createCategoryAttribute(id, data),
});
};
export const useGetCategoryAttributes = (id: string) => {
return useQuery<CategoryAttributesResponseType>({
queryKey: ['category', 'attributes', id],
queryFn: () => api.getCategoryAttributes(id),
enabled: !!id,
});
};
+1
View File
@@ -1,4 +1,5 @@
export { default as CreateCategory } from "./Create";
export { default as Attributes } from "./Attributes";
export * from "./types/Types";
export * from "./hooks/useCategoryData";
export * from "./store/CategoryStore";
+11 -1
View File
@@ -1,5 +1,5 @@
import axios from "../../../config/axios";
import { type CategoryType, type SingleUploadResponseType, type CategoriesResponseType, type CategoryByIdResponseType, type CategoryVariantsResponseType, type CategoryVariantsType } from "../types/Types";
import { type CategoryType, type SingleUploadResponseType, type CategoriesResponseType, type CategoryByIdResponseType, type CategoryVariantsResponseType, type CategoryVariantsType, type CreateCategoryAttributeType, type CategoryAttributesResponseType } from "../types/Types";
export const createCategory = async (params: CategoryType) => {
const { data } = await axios.post(`/admin/category`, params);
@@ -56,4 +56,14 @@ export const deleteCategoryVariant = async (params: { id: string; variantType: '
}
});
return data;
};
export const createCategoryAttribute = async (id: string, params: CreateCategoryAttributeType) => {
const { data } = await axios.post(`/admin/category/${id}/attributes`, params);
return data;
};
export const getCategoryAttributes = async (id: string): Promise<CategoryAttributesResponseType> => {
const { data } = await axios.get(`/category/${id}/attributes`);
return data;
};
+37
View File
@@ -136,3 +136,40 @@ export type CategoryVariantsResponseType = {
};
};
};
export type CreateCategoryAttributeType = {
title: string;
type: string;
multiple?: boolean;
hint?: string;
required: boolean;
values: {
text: string;
}[];
};
export type CategoryAttributeType = {
_id: number;
title: string;
type: string;
multiple: boolean;
hint: string;
required: boolean;
values: {
_id: number;
text: string;
}[];
};
export type CategoryAttributesResponseType = {
status: number;
success: boolean;
results: {
data: {
category: {
category_title_fa: string[];
attributes: CategoryAttributeType[];
};
};
};
};