create tabs component + create filter component + my orders page
This commit is contained in:
Generated
+81
@@ -14,10 +14,12 @@
|
||||
"@tanstack/react-query": "^5.90.3",
|
||||
"axios": "^1.12.2",
|
||||
"iconsax-react": "^0.0.8",
|
||||
"moment-jalaali": "^0.10.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-loading-skeleton": "^3.5.0",
|
||||
"react-multi-date-picker": "^4.5.2",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"react-spinners": "^0.17.0",
|
||||
"swiper": "^12.0.2",
|
||||
@@ -28,6 +30,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@types/moment-jalaali": "^0.7.9",
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
@@ -2105,6 +2108,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/moment-jalaali": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz",
|
||||
"integrity": "sha512-gsDOoAzRnCfQTbfdlUrCvX6R0wIto6CvwfvV2C3j4qJLK+DEiTK8Rl/xlOCBO9C6qeUfX8oyZ2UfjnXJTOvHSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": ">=2.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
|
||||
@@ -3537,6 +3550,12 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jalaali-js": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz",
|
||||
"integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
@@ -4006,6 +4025,38 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-jalaali": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.10.4.tgz",
|
||||
"integrity": "sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jalaali-js": "^1.2.7",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.46"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.48",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
|
||||
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -4250,6 +4301,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-date-object": {
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz",
|
||||
"integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
@@ -4262,6 +4319,16 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-element-popper": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/react-element-popper/-/react-element-popper-2.1.7.tgz",
|
||||
"integrity": "sha512-tuM2OxKlW32h+6uFSK6EENHPeZ2OGgOipHfOAl+VLWEv9/j3QkSGbD+ADX3A9uJlmq24i37n28RjJmAbGTfpEg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-infinite-scroll-component": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
|
||||
@@ -4289,6 +4356,20 @@
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-multi-date-picker": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-multi-date-picker/-/react-multi-date-picker-4.5.2.tgz",
|
||||
"integrity": "sha512-FgWjZB3Z6IA6XpcWiLPk85PwcRUhOiYhKK42o5k672gD/n2I6rzPfQ8bUrldOIiF/Z7FfOCdH7a6FeubzqteLg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-date-object": "^2.1.8",
|
||||
"react-element-popper": "^2.1.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
"@tanstack/react-query": "^5.90.3",
|
||||
"axios": "^1.12.2",
|
||||
"iconsax-react": "^0.0.8",
|
||||
"moment-jalaali": "^0.10.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-loading-skeleton": "^3.5.0",
|
||||
"react-multi-date-picker": "^4.5.2",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"react-spinners": "^0.17.0",
|
||||
"swiper": "^12.0.2",
|
||||
@@ -30,6 +32,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@types/moment-jalaali": "^0.7.9",
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useState, useEffect, type FC } from 'react';
|
||||
import DatePicker from 'react-multi-date-picker';
|
||||
import persian from 'react-date-object/calendars/persian';
|
||||
import persian_fa from 'react-date-object/locales/persian_fa';
|
||||
import DateObject from 'react-date-object';
|
||||
import { Calendar } from 'iconsax-react';
|
||||
import { clx } from '@/helpers/utils';
|
||||
|
||||
type Props = {
|
||||
onChange: (date: string) => void;
|
||||
defaultValue?: string;
|
||||
error_text?: string;
|
||||
placeholder: string;
|
||||
reset?: boolean;
|
||||
isDateTime?: boolean;
|
||||
className?: string;
|
||||
label?: string,
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
const DatePickerComponent: FC<Props> = (props) => {
|
||||
const [value, setValue] = useState<DateObject | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.reset) {
|
||||
setValue(null);
|
||||
}
|
||||
}, [props.reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const formattedDate = `${value.year}/${value.month.number}/${value.day}`;
|
||||
props.onChange(formattedDate);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.defaultValue && !value) {
|
||||
const defaultDate = new DateObject({
|
||||
date: props.defaultValue,
|
||||
calendar: persian,
|
||||
locale: persian_fa,
|
||||
});
|
||||
setValue(defaultDate);
|
||||
}
|
||||
}, [props.defaultValue, value]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{
|
||||
props.label &&
|
||||
<div className='text-sm text-foreground font-medium mb-2'>{props.label}</div>
|
||||
}
|
||||
<div className={clx(
|
||||
'relative',
|
||||
props.readOnly && 'readOny',
|
||||
props.label && 'mt-1'
|
||||
)}>
|
||||
<DatePicker
|
||||
placeholder={props.placeholder}
|
||||
value={value}
|
||||
onChange={(date) => setValue(date as DateObject)}
|
||||
calendar={persian}
|
||||
locale={persian_fa}
|
||||
calendarPosition="bottom-right"
|
||||
className={props.className}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
{props.error_text && props.error_text !== '' && (
|
||||
<div className="text-xs text-right text-red-600 mt-2 mr-2 font-medium">
|
||||
{props.error_text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Calendar
|
||||
size={20}
|
||||
color='#8c90a3'
|
||||
className='absolute top-0 bottom-0 my-auto left-2 pointer-events-none'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePickerComponent;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { type FC, Fragment, type ReactNode, useEffect } from 'react'
|
||||
import HeaderModal from './HeaderModal'
|
||||
|
||||
interface Props {
|
||||
open: boolean,
|
||||
close: () => void,
|
||||
children: ReactNode,
|
||||
isHeader?: boolean,
|
||||
title_header?: string,
|
||||
width?: number
|
||||
}
|
||||
|
||||
const DefaulModal: FC<Props> = (props: Props) => {
|
||||
useEffect(() => {
|
||||
if (props.open) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = 'auto'
|
||||
}
|
||||
}, [props.open])
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
props.open && (
|
||||
<Fragment>
|
||||
<div style={{ maxWidth: props.width }} className='xl:justify-center xl:items-center items-end flex overflow-x-hidden overflow-y-auto fixed inset-0 z-[9999999] h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto'>
|
||||
<div className='relative xl:h-full max-h-[80%] bottom-0 left-0 flex xl:items-center sm:h-auto w-full xl:my-6 xl:p-2'>
|
||||
<div className='h-auto p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full outline-none focus:outline-none bg-card border border-border'>
|
||||
|
||||
{
|
||||
props.isHeader && props.title_header &&
|
||||
<div onClick={props.close} className='pb-6 border-b border-border border-opacity-20'>
|
||||
<div className={`h-[5px] w-[200px] mx-auto rounded-full mb-4 xl:hidden bg-[#D1D3D7]'
|
||||
}`}></div>
|
||||
<HeaderModal close={props.close} label={props.title_header} />
|
||||
</div>
|
||||
}
|
||||
|
||||
{props.children}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onClick={props.close} className='fixed size-full bg-black/65 top-0 bottom-0 right-0 inset-0 z-50 '></div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default DefaulModal
|
||||
@@ -0,0 +1,229 @@
|
||||
import { type FC, useEffect, useState, type ChangeEvent, useRef } from 'react';
|
||||
import { Filter } from 'iconsax-react';
|
||||
import DatePicker from './DatePicker';
|
||||
import Input from './Input';
|
||||
import Select, { type ItemsSelectType } from './Select';
|
||||
import DefaulModal from './DefaulModal';
|
||||
import moment from 'moment-jalaali'
|
||||
import Button from './Button';
|
||||
|
||||
export type DateFieldType = {
|
||||
type: 'date';
|
||||
name: string;
|
||||
placeholder: string;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
export type SelectFieldType = {
|
||||
type: 'select';
|
||||
name: string;
|
||||
placeholder: string;
|
||||
options: ItemsSelectType[];
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
export type InputFieldType = {
|
||||
type: 'input';
|
||||
name: string;
|
||||
placeholder: string;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
export type FieldType = DateFieldType | SelectFieldType | InputFieldType;
|
||||
|
||||
export type FilterValues = Record<string, string | null>;
|
||||
|
||||
interface FiltersProps {
|
||||
fields: FieldType[];
|
||||
onChange: (filters: FilterValues) => void;
|
||||
initialValues?: FilterValues;
|
||||
className?: string;
|
||||
fieldClassName?: string;
|
||||
searchField?: string;
|
||||
}
|
||||
|
||||
const Filters: FC<FiltersProps> = ({
|
||||
fields,
|
||||
onChange,
|
||||
initialValues = {},
|
||||
className = "flex flex-col md:flex-row justify-between items-start md:items-center gap-4",
|
||||
fieldClassName = "flex flex-col sm:flex-row gap-3 md:gap-4 w-full md:w-auto",
|
||||
searchField = "search"
|
||||
}) => {
|
||||
const [filters, setFilters] = useState<FilterValues>({});
|
||||
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
|
||||
const [inputValues, setInputValues] = useState<Record<string, string>>({});
|
||||
const onChangeRef = useRef(onChange);
|
||||
|
||||
useEffect(() => {
|
||||
onChangeRef.current = onChange;
|
||||
}, [onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(initialValues).length > 0) {
|
||||
setFilters(initialValues);
|
||||
const inputFields: Record<string, string> = {};
|
||||
fields.forEach(field => {
|
||||
if (field.type === 'input' && initialValues[field.name]) {
|
||||
inputFields[field.name] = initialValues[field.name] as string;
|
||||
}
|
||||
});
|
||||
setInputValues(inputFields);
|
||||
} else {
|
||||
const defaultValues: FilterValues = {};
|
||||
const defaultInputs: Record<string, string> = {};
|
||||
fields.forEach(field => {
|
||||
if ('defaultValue' in field && field.defaultValue !== undefined) {
|
||||
defaultValues[field.name] = field.defaultValue;
|
||||
if (field.type === 'input') {
|
||||
defaultInputs[field.name] = field.defaultValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(defaultValues).length > 0) {
|
||||
setFilters(defaultValues);
|
||||
setInputValues(defaultInputs);
|
||||
onChange(defaultValues);
|
||||
}
|
||||
}
|
||||
}, [fields, initialValues, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeouts: Record<string, number> = {};
|
||||
|
||||
Object.keys(inputValues).forEach(fieldName => {
|
||||
timeouts[fieldName] = setTimeout(() => {
|
||||
setFilters(currentFilters => {
|
||||
const currentFilter = currentFilters[fieldName];
|
||||
const newValue = inputValues[fieldName];
|
||||
|
||||
if (currentFilter !== newValue) {
|
||||
const newFilters = { ...currentFilters, [fieldName]: newValue || null };
|
||||
onChangeRef.current(newFilters);
|
||||
return newFilters;
|
||||
}
|
||||
return currentFilters;
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
return () => {
|
||||
Object.values(timeouts).forEach(timeout => clearTimeout(timeout));
|
||||
};
|
||||
}, [inputValues]);
|
||||
|
||||
const handleChange = (name: string, value: string | null) => {
|
||||
const newFilters = { ...filters, [name]: value };
|
||||
setFilters(newFilters);
|
||||
onChange(newFilters);
|
||||
};
|
||||
|
||||
const handleInputChange = (name: string, event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
setInputValues(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const renderField = (field: FieldType) => {
|
||||
const currentValue = field.type === 'input' ? inputValues[field.name] : filters[field.name];
|
||||
|
||||
switch (field.type) {
|
||||
case 'date':
|
||||
return (
|
||||
<DatePicker
|
||||
key={field.name}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(value) => handleChange(field.name, moment(value, 'jYYYY/jMM/jDD').format('YYYY-MM-DD'))}
|
||||
defaultValue={currentValue || field.defaultValue || ''}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<Select
|
||||
key={field.name}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
||||
defaultValue={currentValue || field.defaultValue || ''}
|
||||
items={field.options}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'input':
|
||||
return (
|
||||
<Input
|
||||
key={field.name}
|
||||
placeholder={field.placeholder}
|
||||
variant="search"
|
||||
className="w-full md:max-w-[200px]"
|
||||
value={currentValue || field.defaultValue || ''}
|
||||
onChange={(e) => handleInputChange(field.name, e)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const searchFieldObj = fields.find(field => field.name === searchField);
|
||||
const otherFields = fields.filter(field => field.name !== searchField);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex md:hidden items-center justify-between w-full mb-2">
|
||||
<button
|
||||
onClick={() => setIsFiltersOpen(!isFiltersOpen)}
|
||||
className='flex items-center gap-2 px-3 py-2 rounded-lg transition-colors bg-card text-card-foreground border border-border hover:bg-secondary hover:bg-opacity-80'
|
||||
>
|
||||
<Filter
|
||||
size={18}
|
||||
color='#000000'
|
||||
/>
|
||||
<span className="text-sm">فیلترها</span>
|
||||
</button>
|
||||
{searchFieldObj && (
|
||||
<div className="flex-1 mr-4">
|
||||
{renderField(searchFieldObj)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex md:flex-row justify-between items-center gap-4 w-full">
|
||||
<div className={fieldClassName}>
|
||||
{otherFields.map(renderField)}
|
||||
</div>
|
||||
{searchFieldObj && (
|
||||
<div className="w-full md:w-auto">
|
||||
{renderField(searchFieldObj)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DefaulModal
|
||||
open={isFiltersOpen}
|
||||
close={() => setIsFiltersOpen(false)}
|
||||
isHeader={true}
|
||||
title_header="فیلتر کردن نتایج"
|
||||
>
|
||||
<div className="pb-5 gap-4 flex flex-col-reverse mt-3">
|
||||
{otherFields.map(field => (
|
||||
<div key={field.name} className="w-full">
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
{field.placeholder}
|
||||
</label>
|
||||
<div className="w-full">
|
||||
{renderField(field)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='mt-6'>
|
||||
<Button
|
||||
label='بستن'
|
||||
onClick={() => setIsFiltersOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Filters;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { type FC } from 'react'
|
||||
import { CloseCircle } from 'iconsax-react'
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
close: () => void,
|
||||
}
|
||||
|
||||
const HeaderModal: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='text-sm text-foreground font-medium'>{props.label}</div>
|
||||
<div className='size-7 rounded-full flex justify-center items-center transition-colors bg-white bg-opacity-35 hover:bg-opacity-50'>
|
||||
<CloseCircle
|
||||
size={20}
|
||||
color='#000000'
|
||||
onClick={props.close}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderModal
|
||||
@@ -33,9 +33,9 @@ const Input: FC<Props> = (props: Props) => {
|
||||
const [search, setSearch] = useState<string>('')
|
||||
|
||||
const inputClass = clx(
|
||||
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
||||
'w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
||||
props.readOnly && 'bg-gray-100 border-0 text-description',
|
||||
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
|
||||
props.variant === 'search' && 'border-0 ps-10 mt-0',
|
||||
props.className
|
||||
);
|
||||
|
||||
@@ -84,7 +84,7 @@ const Input: FC<Props> = (props: Props) => {
|
||||
{props.label}
|
||||
</label>
|
||||
|
||||
<div className='w-full relative mt-1'>
|
||||
<div className={clx('w-full relative', props.label && 'mt-1')}>
|
||||
<input {...props} onChange={(e) => {
|
||||
setSearch(e.target.value)
|
||||
handleInputChange(e)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { FC, SelectHTMLAttributes } from 'react'
|
||||
import { clx } from '../helpers/utils'
|
||||
import { ArrowDown2 } from 'iconsax-react'
|
||||
|
||||
export type ItemsSelectType = {
|
||||
value: string,
|
||||
label: string,
|
||||
}
|
||||
type Props = {
|
||||
className?: string,
|
||||
items: ItemsSelectType[],
|
||||
error_text?: string,
|
||||
placeholder?: string,
|
||||
label?: string,
|
||||
readOnly?: boolean,
|
||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||
|
||||
const Select: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{
|
||||
props.label &&
|
||||
<label className='text-sm text-primary-content'>
|
||||
{props.label}
|
||||
</label>
|
||||
}
|
||||
<div className='relative'>
|
||||
<select {...props} className={clx(
|
||||
'w-full bg-white input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-[10px] transition-colors',
|
||||
props.readOnly && 'bg-muted border-0 text-muted-foreground',
|
||||
props.className,
|
||||
props.label && 'mt-1'
|
||||
)}>
|
||||
{
|
||||
props.placeholder &&
|
||||
<option value="" disabled selected>{props.placeholder}</option>
|
||||
}
|
||||
{
|
||||
props.items?.map((item) => {
|
||||
return (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
</select>
|
||||
<ArrowDown2 size={16} color='#8c90a3' className='absolute z-0 top-3 left-2 pointer-events-none'
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
props.error_text && props.error_text !== '' ?
|
||||
<div className='text-xs text-right text-red-600 mt-2 mr-2 font-medium'>
|
||||
{props.error_text}
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Select
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import React, { Fragment, useState, memo } from 'react';
|
||||
import DefaultTableSkeleton from '@/components/DefaultTableSkeleton';
|
||||
import Td from '@/components/Td';
|
||||
import type { TableProps, RowDataType, ColumnType } from '@/components/types/TableTypes';
|
||||
@@ -321,7 +321,7 @@ const Table = <T extends RowDataType>({
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
export default memo(Table) as <T extends RowDataType>(props: TableProps<T>) => React.ReactElement;
|
||||
|
||||
/*
|
||||
مثال استفاده با pagination:
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { clx } from '@/helpers/utils'
|
||||
import { type FC } from 'react'
|
||||
|
||||
type TabItem = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
items: TabItem[]
|
||||
activeTab: string
|
||||
onTabChange: (tab: string) => void
|
||||
}
|
||||
|
||||
const Tabs: FC<Props> = (props) => {
|
||||
|
||||
const { items, activeTab, onTabChange } = props
|
||||
|
||||
|
||||
return (
|
||||
<div className='flex justify-center text-sm'>
|
||||
<div className='flex gap-4 h-[52px] rounded-full bg-white px-4 items-center'>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<div onClick={() => onTabChange(item.value)} key={item.value} className={clx(
|
||||
'h-[32px] flex items-center justify-center w-[122px] cursor-pointer rounded-full',
|
||||
activeTab === item.value && 'bg-primary'
|
||||
)}>
|
||||
{item.label}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tabs
|
||||
@@ -49,3 +49,23 @@ textarea::placeholder {
|
||||
tbody tr:nth-child(odd) {
|
||||
background-color: #f5f7fc;
|
||||
}
|
||||
|
||||
.rmdp-input {
|
||||
min-height: 40px;
|
||||
background-color: white;
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid oklch(0.922 0 0) !important;
|
||||
font-size: 12px !important;
|
||||
width: 100% !important;
|
||||
margin: 0px;
|
||||
padding-right: 16px !important;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
.readOny .rmdp-input {
|
||||
background-color: #f5f5f5 !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.rmdp-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import Tabs from '@/components/Tabs'
|
||||
import { useState, type FC } from 'react'
|
||||
import { TabMyOrdersEnum } from './enum/OrderEnum'
|
||||
import Filters from '@/components/Filters'
|
||||
import Button from '@/components/Button'
|
||||
import { AddSquare } from 'iconsax-react'
|
||||
import Table from '@/components/Table'
|
||||
|
||||
const MyOrders: FC = () => {
|
||||
|
||||
const [activeTab, setActiveTab] = useState<TabMyOrdersEnum>(TabMyOrdersEnum.ALL)
|
||||
return (
|
||||
<div className='mt-5'>
|
||||
<h1>
|
||||
سفارشات من
|
||||
</h1>
|
||||
|
||||
<div className='mt-10'>
|
||||
<Tabs
|
||||
items={[
|
||||
{ label: 'همه', value: TabMyOrdersEnum.ALL },
|
||||
{ label: 'در حال انجام', value: TabMyOrdersEnum.IN_PROGRESS },
|
||||
{ label: 'تایید شده', value: TabMyOrdersEnum.CONFIRMED },
|
||||
{ label: 'تحویل داده شده', value: TabMyOrdersEnum.DELIVERED },
|
||||
{ label: 'کنسل شده', value: TabMyOrdersEnum.CANCELLED },
|
||||
]}
|
||||
activeTab={activeTab}
|
||||
onTabChange={(tab) => setActiveTab(tab as TabMyOrdersEnum)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-16 flex justify-between items-center'>
|
||||
<Filters
|
||||
fields={[
|
||||
{ type: 'input', name: 'search', placeholder: 'جستجو' },
|
||||
{ type: 'date', name: 'date', placeholder: 'تاریخ' },
|
||||
{
|
||||
type: 'select', name: 'status', placeholder: 'وضعیت', options: [
|
||||
{ label: 'همه', value: 'all' },
|
||||
{ label: 'در حال انجام', value: 'in_progress' },
|
||||
{ label: 'تایید شده', value: 'confirmed' },
|
||||
{ label: 'تحویل داده شده', value: 'delivered' },
|
||||
{ label: 'کنسل شده', value: 'cancelled' },
|
||||
]
|
||||
},
|
||||
]}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className='w-fit px-5'
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<AddSquare
|
||||
size={16}
|
||||
color='#000000'
|
||||
/>
|
||||
<span>
|
||||
سفارش جدید
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='mt-10'>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
key: 'id',
|
||||
title: 'شماره',
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
title: 'عنوان',
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
title: 'زمان ثبت سفارش',
|
||||
},
|
||||
{
|
||||
key: 'designer',
|
||||
title: 'طراح',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
title: 'نوع سفارش',
|
||||
},
|
||||
{
|
||||
key: 'itemsCount',
|
||||
title: 'تعداد اقلام',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: 'وضعیت سفارش',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: '',
|
||||
}
|
||||
]}
|
||||
data={[
|
||||
{
|
||||
id: 1,
|
||||
title: 'سفارش 1',
|
||||
createdAt: '2021-01-01',
|
||||
designer: 'طراح 1',
|
||||
type: 'نوع 1',
|
||||
itemsCount: 10,
|
||||
status: 'در حال انجام',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyOrders
|
||||
@@ -0,0 +1,7 @@
|
||||
export const enum TabMyOrdersEnum {
|
||||
ALL = "all",
|
||||
IN_PROGRESS = "in_progress",
|
||||
CONFIRMED = "confirmed",
|
||||
DELIVERED = "delivered",
|
||||
CANCELLED = "cancelled",
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import Home from '../pages/home/Home'
|
||||
import SideBar from '../shared/Sidebar'
|
||||
import Header from '../shared/Header'
|
||||
import { Paths } from '@/config/Paths'
|
||||
import MyOrders from '@/pages/order/MyOrders'
|
||||
|
||||
const MainRouter: FC = () => {
|
||||
return (
|
||||
@@ -17,6 +18,7 @@ const MainRouter: FC = () => {
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path={Paths.home} element={<Home />} />
|
||||
<Route path={Paths.myOrders} element={<MyOrders />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"erasableSyntaxOnly": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": "./",
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"erasableSyntaxOnly": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": "./",
|
||||
|
||||
Reference in New Issue
Block a user