diff --git a/package-lock.json b/package-lock.json index b39d982..236e9e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "axios": "^1.13.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "formik": "^2.4.9", "html2canvas": "^1.4.1", "i18next": "^25.6.2", "iconsax-react": "^0.0.8", @@ -32,6 +33,7 @@ "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.17", "use-image": "^1.1.4", + "yup": "^1.7.1", "zustand": "^5.0.8" }, "devDependencies": { @@ -2081,6 +2083,18 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2819,6 +2833,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3330,6 +3353,31 @@ "node": ">= 6" } }, + "node_modules/formik": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.9.tgz", + "integrity": "sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3500,6 +3548,15 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -4053,6 +4110,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4395,6 +4464,12 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -4510,6 +4585,12 @@ "react-dom": ">=16.8.0" } }, + "node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", + "license": "MIT" + }, "node_modules/react-i18next": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.0.tgz", @@ -4890,6 +4971,18 @@ "utrie": "^1.0.2" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -4948,6 +5041,12 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -4980,6 +5079,18 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -5252,6 +5363,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yup": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, "node_modules/zustand": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", diff --git a/package.json b/package.json index 5ae2424..21b5015 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "axios": "^1.13.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "formik": "^2.4.9", "html2canvas": "^1.4.1", "i18next": "^25.6.2", "iconsax-react": "^0.0.8", @@ -34,6 +35,7 @@ "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.17", "use-image": "^1.1.4", + "yup": "^1.7.1", "zustand": "^5.0.8" }, "devDependencies": { diff --git a/src/langs/fa.json b/src/langs/fa.json index d496ef1..85baa64 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -3,7 +3,8 @@ "menu": "منو", "home_page": "صفحه اصلی", "catalog": "کاتالوگ", - "logout": "خروج" + "logout": "خروج", + "request_design": "درخواست طراحی" }, "header": { "search": "جستجو", diff --git a/src/pages/designer/Request.tsx b/src/pages/designer/Request.tsx index 8c27eae..9f3b56d 100644 --- a/src/pages/designer/Request.tsx +++ b/src/pages/designer/Request.tsx @@ -1,11 +1,15 @@ import { usePageTitle } from '@/hooks/usePageTitle' -import { useMemo, useState, type FC } from 'react' +import { type FC } from 'react' +import { Form, Formik, type FormikProps } from 'formik' +import * as Yup from 'yup' import Button from '@/components/Button' import DatePicker from '@/components/DatePicker' import Input from '@/components/Input' import Select from '@/components/Select' import Textarea from '@/components/Textarea' import UploadBoxDraggble from '@/components/UploadBoxDraggble' +import Error from '@/components/Error' +import { useSingleUpload } from '@/pages/uploader/hooks/useUploaderData' const PRICE_PER_PAGE = 1000 @@ -17,90 +21,185 @@ const pageCountOptions = Array.from({ length: 20 }, (_, index) => { } }) +export type DesignerRequestFormValues = { + title: string + count: number + desc: string + expectedDate: string + attachments: string[] +} + +const initialValues: DesignerRequestFormValues = { + title: '', + count: 1, + desc: '', + expectedDate: '', + attachments: [], +} + +const validationSchema = Yup.object({ + title: Yup.string().trim().required('عنوان کاتالوگ الزامی است'), + count: Yup.number() + .min(1, 'حداقل تعداد صفحات ۱ است') + .max(20, 'حداکثر تعداد صفحات ۲۰ است') + .required('تعداد صفحات الزامی است'), + desc: Yup.string().trim(), + expectedDate: Yup.string().required('زمان تحویل الزامی است'), + attachments: Yup.array().of(Yup.string()), +}) + +type DesignerRequestFormFieldsProps = FormikProps & { + uploadFile: ReturnType['mutateAsync'] + isUploading: boolean +} + +const DesignerRequestFormFields: FC = ({ + values, + errors, + touched, + isSubmitting, + setFieldValue, + setFieldTouched, + handleChange, + handleBlur, + uploadFile, + isUploading, +}) => { + const totalPrice = values.count * PRICE_PER_PAGE + + const handleAttachmentChange = async (files: File[]) => { + if (files.length === 0) { + await setFieldValue('attachments', []) + setFieldTouched('attachments', true) + return + } + + try { + const response = await uploadFile(files[0]) + await setFieldValue('attachments', [response.data.url]) + } catch { + await setFieldValue('attachments', []) + } finally { + setFieldTouched('attachments', true) + } + } + + return ( +
+ + +
+ { + setFieldValue('expectedDate', date) + setFieldTouched('expectedDate', true) + }} + error_text={touched.expectedDate ? errors.expectedDate : undefined} + /> +