diff --git a/package-lock.json b/package-lock.json index 97b5c75..7939c16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "react-i18next": "^15.7.2", "react-multi-date-picker": "^4.5.2", "react-otp-input": "^3.1.1", + "react-quill-new": "^3.6.0", "react-router-dom": "^7.8.2", "react-spinners": "^0.17.0", "react-toastify": "^11.0.5", @@ -2838,9 +2839,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -3480,6 +3481,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3487,6 +3494,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "license": "Apache-2.0" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -4364,6 +4377,19 @@ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "license": "MIT" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4622,6 +4648,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parchment": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", + "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", + "license": "BSD-3-Clause" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4773,6 +4805,35 @@ ], "license": "MIT" }, + "node_modules/quill": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", + "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "license": "BSD-3-Clause", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash-es": "^4.17.21", + "parchment": "^3.0.0", + "quill-delta": "^5.1.0" + }, + "engines": { + "npm": ">=8.2.3" + } + }, + "node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "license": "MIT", + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/rc-rate": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", @@ -4927,6 +4988,21 @@ "react-dom": ">=16.8.6 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-quill-new": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.6.0.tgz", + "integrity": "sha512-weU6YfB2+7Cujw5Hjgmi0aN/qJd3B6ADWrxgUJMp2MO3tEvKX5kfB0sg3P0UdOVfU0z8icsKFzlnEIpeW1mLhw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21", + "quill": "~2.0.2" + }, + "peerDependencies": { + "quill-delta": "^5.1.0", + "react": "^16 || ^17 || ^18 || ^19", + "react-dom": "^16 || ^17 || ^18 || ^19" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -5324,13 +5400,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -5580,9 +5656,9 @@ } }, "node_modules/vite": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", - "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", + "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -5590,7 +5666,7 @@ "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", - "tinyglobby": "^0.2.14" + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" diff --git a/package.json b/package.json index 0974b69..b985702 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react-i18next": "^15.7.2", "react-multi-date-picker": "^4.5.2", "react-otp-input": "^3.1.1", + "react-quill-new": "^3.6.0", "react-router-dom": "^7.8.2", "react-spinners": "^0.17.0", "react-toastify": "^11.0.5", diff --git a/src/components/TextEditor.css b/src/components/TextEditor.css new file mode 100644 index 0000000..29a6bdd --- /dev/null +++ b/src/components/TextEditor.css @@ -0,0 +1,52 @@ +/* TextEditor RTL and Placeholder Styles */ + +[dir="rtl"] .ql-editor { + text-align: right; + direction: rtl; +} + +[dir="rtl"] .ql-toolbar { + direction: rtl; +} + +[dir="rtl"] .ql-toolbar .ql-picker-label { + text-align: right; +} + +[dir="rtl"] .ql-toolbar .ql-formats { + margin-left: 0; + margin-right: 15px; +} + +[dir="rtl"] .ql-toolbar .ql-formats:first-child { + margin-right: 0; +} + +/* Enhanced placeholder styles for RTL */ +.ql-editor.ql-blank::before { + color: #6b7280 !important; + content: var(--placeholder-text, "متن خود را وارد کنید...") !important; + font-style: normal !important; + pointer-events: none !important; + position: absolute !important; + right: 15px !important; + top: 12px !important; + left: auto !important; + font-size: 14px !important; + line-height: 1.5 !important; +} + +[dir="rtl"] .ql-editor.ql-blank::before { + right: 15px !important; + left: auto !important; +} + +/* Ensure placeholder shows when editor is focused but empty */ +.ql-editor.ql-blank.ql-has-focus::before { + display: none; +} + +/* Custom height for editor */ +.h-64 .ql-editor { + min-height: 160px; +} diff --git a/src/components/TextEditor.tsx b/src/components/TextEditor.tsx new file mode 100644 index 0000000..466bd2b --- /dev/null +++ b/src/components/TextEditor.tsx @@ -0,0 +1,65 @@ +import { type FC } from 'react' +import ReactQuill from 'react-quill-new' +import 'react-quill-new/dist/quill.snow.css' +import './TextEditor.css' + +interface TextEditorProps { + value: string + onChange: (value: string) => void + placeholder?: string + className?: string + height?: string +} + +const TextEditor: FC = ({ + value, + onChange, + placeholder = 'متن خود را وارد کنید...', + className = '', + height = 'h-[200px]' +}) => { + const modules = { + toolbar: [ + [{ 'header': [1, 2, 3, 4, 5, 6, false] }], + [{ 'font': [] }], + [{ 'size': [] }], + ['bold', 'italic', 'underline', 'strike', 'blockquote'], + [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'indent': '-1' }, { 'indent': '+1' }], + ['link', 'image', 'video'], + [{ 'color': [] }, { 'background': [] }], + [{ 'align': [] }], + ['clean'] + ], + clipboard: { + matchVisual: false, + }, + } + + const formats = [ + 'header', 'font', 'size', + 'bold', 'italic', 'underline', 'strike', 'blockquote', + 'list', 'bullet', 'indent', + 'link', 'image', 'video', + 'color', 'background', + 'align' + ] + + return ( +
+ +
+ ) +} + +export default TextEditor diff --git a/src/config/Pages.ts b/src/config/Pages.ts index df3ade3..86546bd 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -58,6 +58,7 @@ export const Pages = { faq: "/pages/faq", faqCreate: "/pages/faq/create", faqUpdate: "/pages/faq/update/", + shipment: "/pages/shipment", }, wallet: "/wallet", profile: "/profile", diff --git a/src/pages/setting/ShipmentPage.tsx b/src/pages/setting/ShipmentPage.tsx new file mode 100644 index 0000000..b9b2c28 --- /dev/null +++ b/src/pages/setting/ShipmentPage.tsx @@ -0,0 +1,107 @@ +import { type FC, useEffect, useState } from 'react' +import { useGetSiteSetting, useUpdateSiteSetting } from './hooks/useSettingData' +import { SiteSettingPageEnum } from './enum/Enum' +import { useFormik } from 'formik' +import type { UpdateSiteSetting } from './types/Types' +import { toast } from 'react-toastify' +import { Pages } from '@/config/Pages' +import * as Yup from 'yup' +import { useNavigate } from 'react-router-dom' +import Button from '@/components/Button' +import TextEditor from '@/components/TextEditor' +import PageLoading from '@/components/PageLoading' +import Error from '@/components/Error' +import { TickCircle } from 'iconsax-react' + +const ShipmentPage: FC = () => { + const navigate = useNavigate() + const { data: siteSetting, isLoading: siteSettingLoading, error: siteSettingError } = useGetSiteSetting(SiteSettingPageEnum.shipmentContent) + const updateSiteSettingMutation = useUpdateSiteSetting() + const [formData, setFormData] = useState({ shipmentContent: '' }) + + const formik = useFormik({ + initialValues: formData, + validationSchema: Yup.object({ + shipmentContent: Yup.string().required('روش ارسال الزامی است'), + }), + onSubmit: (values) => { + updateSiteSettingMutation.mutate(values, { + onSuccess: () => { + toast.success('روش ارسال با موفقیت ویرایش شد') + navigate(Pages.pages.shipment) + } + }) + }, + enableReinitialize: true + }) + + useEffect(() => { + if (siteSetting?.results?.siteSetting?.shipmentContent) { + const newData = { shipmentContent: siteSetting.results.siteSetting.shipmentContent } + setFormData(newData) + } + }, [siteSetting]) + + if (siteSettingLoading) { + return ( +
+ +
+ ) + } + + if (siteSettingError) { + return ( +
+ +
+ ) + } + + return ( +
+
+
+

ویرایش روش ارسال

+
+
+ +
+
+ +
+
+
+
+
+ متن روش ارسال +
+ formik.setFieldValue('shipmentContent', value)} + placeholder='متن روش ارسال را وارد کنید...' + /> + {formik.touched.shipmentContent && formik.errors.shipmentContent && ( +
+ {formik.errors.shipmentContent} +
+ )} +
+
+
+
+
+ ) +} + +export default ShipmentPage \ No newline at end of file diff --git a/src/pages/setting/enum/Enum.ts b/src/pages/setting/enum/Enum.ts index ce129a9..75775ce 100644 --- a/src/pages/setting/enum/Enum.ts +++ b/src/pages/setting/enum/Enum.ts @@ -7,3 +7,18 @@ export enum FaqPageEnum { ShipmentProcess = "ShipmentProcess", Policy = "Policy", } + +export enum SiteSettingPageEnum { + siteLogo = "siteLogo", + footerDescription = "footerDescription", + footerAddress = "footerAddress", + footerEmail = "footerEmail", + footerPhone = "footerPhone", + downloadAppDetails = "downloadAppDetails", + socialMediaLinks = "socialMediaLinks", + shipmentContent = "shipmentContent", + faqContent = "faqContent", + policyContent = "policyContent", + returnRulesContent = "returnRulesContent", + jobsContent = "jobsContent", +} diff --git a/src/pages/setting/hooks/useSettingData.ts b/src/pages/setting/hooks/useSettingData.ts index c789e29..e704f51 100644 --- a/src/pages/setting/hooks/useSettingData.ts +++ b/src/pages/setting/hooks/useSettingData.ts @@ -1,6 +1,6 @@ import * as api from "../service/SettingService"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { FaqPageEnum } from "../enum/Enum"; +import { FaqPageEnum, SiteSettingPageEnum } from "../enum/Enum"; import { type CreateFaqType } from "../types/Types"; export const useGetAboutUs = () => { @@ -57,4 +57,18 @@ export const useUpdateFaq = () => { queryClient.invalidateQueries({ queryKey: ["faq", params.page] }); }, }); +}; + +export const useGetSiteSetting = (page: SiteSettingPageEnum) => { + return useQuery({ + queryKey: ["siteSetting", page], + queryFn: () => api.getSiteSetting(page), + enabled: !!page, + }); +}; + +export const useUpdateSiteSetting = () => { + return useMutation({ + mutationFn: api.updateSiteSetting, + }); }; \ No newline at end of file diff --git a/src/pages/setting/service/SettingService.ts b/src/pages/setting/service/SettingService.ts index d975a61..e9811ab 100644 --- a/src/pages/setting/service/SettingService.ts +++ b/src/pages/setting/service/SettingService.ts @@ -5,8 +5,10 @@ import type { CreateFaqType, FaqDetailResponse, FaqResponse, + SiteSettingResponse, + UpdateSiteSetting, } from "../types/Types"; -import { FaqPageEnum } from "../enum/Enum"; +import { FaqPageEnum, SiteSettingPageEnum } from "../enum/Enum"; export const getAboutUs = async (): Promise => { const { data } = await axios.get("/about-us"); @@ -50,3 +52,15 @@ export const updateFaq = async (id: string, params: CreateFaqType) => { }); return data; }; + +export const getSiteSetting = async ( + page: SiteSettingPageEnum +): Promise => { + const { data } = await axios.get(`/site-setting?name=${page}`); + return data; +}; + +export const updateSiteSetting = async (params: UpdateSiteSetting) => { + const { data } = await axios.post(`/admin/site-setting/update`, params); + return data; +}; diff --git a/src/pages/setting/types/Types.ts b/src/pages/setting/types/Types.ts index a0b4f10..df94394 100644 --- a/src/pages/setting/types/Types.ts +++ b/src/pages/setting/types/Types.ts @@ -59,3 +59,56 @@ export interface FaqDetailResponse { success: boolean; results: FaqDetailResults; } + +export interface SiteSetting { + _id: string; + siteLogo?: string; + footerPhone?: string; + footerEmail?: string; + footerAddress?: string; + shipmentContent?: string; + faqContent?: string; + policyContent?: string; + returnRulesContent?: string; + jobsContent?: string; + footerDescription?: string; + downloadAppDetails?: { + pic: string; + url: string; + }[]; + socialMediaLinks?: { + icon: string; + url: string; + }[]; +} + +export interface SiteSettingResults { + siteSetting: SiteSetting; +} + +export interface SiteSettingResponse { + status: number; + success: boolean; + results: SiteSettingResults; +} + +export interface UpdateSiteSetting { + siteLogo?: string; + footerPhone?: string; + footerEmail?: string; + footerAddress?: string; + shipmentContent?: string; + faqContent?: string; + policyContent?: string; + returnRulesContent?: string; + jobsContent?: string; + footerDescription?: string; + downloadAppDetails?: { + pic: string; + url: string; + }[]; + socialMediaLinks?: { + icon: string; + url: string; + }[]; +} diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 078907e..83dcaab 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -49,6 +49,7 @@ import AboutUsCreate from '@/pages/setting/AboutUsCreate' import FaqList from '@/pages/setting/FaqList' import FaqCreate from '@/pages/setting/FaqCreate' import FaqUpdate from '@/pages/setting/FaqUpdate' +import ShipmentPage from '@/pages/setting/ShipmentPage' const MainRouter: FC = () => { @@ -123,6 +124,7 @@ const MainRouter: FC = () => { } /> } /> } /> + } /> diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index b30ce51..ec52c48 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -806,12 +806,12 @@ const PagesSubMenu: FC = () => {