diff --git a/.env b/.env index 9585291..3ce601b 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ VITE_BASE_URL = 'https://api.shinan.ir' VITE_TOKEN_NAME = 'sh_admin_token' -VITE_REFRESH_TOKEN_NAME = 'sh_admin_token' \ No newline at end of file +VITE_REFRESH_TOKEN_NAME = 'sh_admin_refresh_token' \ No newline at end of file diff --git a/src/components/Button.tsx b/src/components/Button.tsx index db81bce..d558a4a 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -6,24 +6,42 @@ import { clx } from '../helpers/utils' type Props = { className?: string; isLoading?: boolean, + variant?: 'primary' | 'outline' | 'ghost' | 'danger'; + size?: 'sm' | 'md' | 'lg'; } & ButtonHTMLAttributes & XOR<{ children: ReactNode }, { label: string }>; -const Button: FC = memo((props: Props) => { +const Button: FC = memo((props) => { + const { variant = 'primary', size = 'md', className, ...restProps } = props; const buttonClass = clx( - 'flex rounded-xl items-center justify-center text-center h-10 text-sm bg-primary text-white w-full', - props.disabled && 'cursor-not-allowed opacity-60', - props.className + 'flex rounded-xl items-center justify-center text-center font-medium transition-colors', + // Variant styles + variant === 'primary' && 'bg-primary text-white hover:bg-primary/90', + variant === 'outline' && 'bg-transparent border border-gray-300 text-gray-700 hover:bg-gray-50 hover:border-gray-400', + variant === 'ghost' && 'bg-transparent text-gray-700 hover:bg-gray-100', + variant === 'danger' && 'bg-red-500 text-white hover:bg-red-600', + // Size styles + size === 'sm' && 'h-8 px-3 text-xs', + size === 'md' && 'h-10 px-4 text-sm', + size === 'lg' && 'h-12 px-6 text-base', + // Disabled state + restProps.disabled && 'cursor-not-allowed opacity-60', + // Default width + 'w-full' ); return ( - ) diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx new file mode 100644 index 0000000..d5dbf8b --- /dev/null +++ b/src/components/Switch.tsx @@ -0,0 +1,50 @@ +import { Fragment } from 'react' +import { Switch } from '@headlessui/react' +import ReactLoading from 'react-spinners/MoonLoader' + +interface Props { + active: boolean, + onChange: (value: boolean) => void, + isLoading?: boolean, + label?: string, +} + +const SwitchComponent = (props: Props) => { + + const handleChange = (value: boolean) => { + props.onChange(value) + } + + if (props.isLoading) { + return ( + + ) + } + + return ( +
+ + {({ checked }) => ( + + )} + + { + props.label && +
+ : {props.label} +
+ } +
+ ) +} + +export default SwitchComponent diff --git a/src/index.css b/src/index.css index bcacdbd..cd8e80e 100644 --- a/src/index.css +++ b/src/index.css @@ -39,3 +39,24 @@ textarea::placeholder { --width-sidebar: 300px; --border-radius-2.5: 10px; } + +.dltr { + direction: ltr; +} + +.rowTwoInput { + @apply flex flex-col sm:flex-row sm:gap-5 gap-5; +} + +.rowTwoInput { + @apply flex flex-col sm:flex-row sm:gap-5 gap-5; +} +.td { + @apply px-6 py-4 whitespace-nowrap text-xs; +} +.thead { + @apply h-[69px] bg-white text-sm text-[#8C90A3] rounded-3xl overflow-hidden; +} +.tr { + @apply w-full h-[74px] bg-white border-t border-[#EAEDF5] hover:bg-secondary; +} diff --git a/src/pages/category/Create.tsx b/src/pages/category/Create.tsx new file mode 100644 index 0000000..cd22524 --- /dev/null +++ b/src/pages/category/Create.tsx @@ -0,0 +1,318 @@ + +import { type FC, useState } from 'react' +import Button from '../../components/Button' +import { TickCircle, ArrowLeft } from 'iconsax-react' +import SwitchComponent from '../../components/Switch' +import Input from '../../components/Input' +import Textarea from '../../components/Textarea' +import UploadBox from '../../components/UploadBox' +import { type CategoryType, type CategoryTreeNode } from './types/Types' +import { useFormik } from 'formik' +import * as Yup from 'yup' +import { useCreateCategory, useGetCategories, useSingleUpload } from './hooks/useCategoryData' +import { useNavigate } from 'react-router-dom' +import { toast } from 'react-toastify' + +const CreateCategory: FC = () => { + + const navigate = useNavigate() + const [iconFile, setIconFile] = useState() + const [imageFile, setImageFile] = useState() + const [searchTerm, setSearchTerm] = useState('') + const singleUpload = useSingleUpload() + const [isActive, setIsActive] = useState(true) + + const createCategoryMutation = useCreateCategory() + const getCategories = useGetCategories() + + const formik = useFormik({ + initialValues: { + title_fa: '', + title_en: '', + icon: '', + imageUrl: '', + description: '', + parent: '', + }, + validationSchema: Yup.object({ + title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'), + title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'), + description: Yup.string().required('توضیحات دسته الزامی است'), + parent: Yup.string().optional(), + }), + onSubmit: async (values) => { + + if (!iconFile || !imageFile) { + toast.error('آیکون و تصویر دسته الزامی است') + return + } + + if (iconFile) { + const res = await singleUpload.mutateAsync(iconFile) + values.icon = res.results?.url?.url + + } + if (imageFile) { + const response = await singleUpload.mutateAsync(imageFile) + values.imageUrl = response.results?.url?.url + } + + createCategoryMutation.mutate(values, { + onSuccess: () => { + navigate('/admin/categories') + }, + }) + }, + }) + + // تبدیل داده‌های API به فرمت مورد نیاز + const categoriesData: CategoryTreeNode[] = getCategories.data?.results?.data || [] + + // فیلتر کردن دسته‌ها بر اساس جستجو + const filteredCategories = categoriesData.filter(cat => + cat.title_fa.toLowerCase().includes(searchTerm.toLowerCase()) || + cat.title_en.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + const handleParentSelect = (category: CategoryTreeNode) => { + formik.setFieldValue('parent', category._id) + } + + return ( +
+
+
+ +

ساخت دسته جدید

+
+
+ +
+
+ +
+ {/* اطلاعات اصلی */} +
+

اطلاعات اصلی

+ +
+
+ وضعیت دسته + +
+ +
+ + +
+ +