This commit is contained in:
Generated
+21
@@ -20,6 +20,7 @@
|
|||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"rc-rate": "^2.13.1",
|
"rc-rate": "^2.13.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/moment-jalaali": "^0.7.9",
|
"@types/moment-jalaali": "^0.7.9",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/qrcode.react": "^1.0.5",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
@@ -2383,6 +2385,16 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qrcode.react": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qrcode.react/-/qrcode.react-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-BghPtnlwvrvq8QkGa1H25YnN+5OIgCKFuQruncGWLGJYOzeSKiix/4+B9BtfKF2wf5ja8yfyWYA3OXju995G8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.11",
|
"version": "19.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz",
|
||||||
@@ -4987,6 +4999,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode.react": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"rc-rate": "^2.13.1",
|
"rc-rate": "^2.13.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/moment-jalaali": "^0.7.9",
|
"@types/moment-jalaali": "^0.7.9",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/qrcode.react": "^1.0.5",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ const validationSchema = yup.object({
|
|||||||
.min(1, 'حداقل یک شماره تماس الزامی است')
|
.min(1, 'حداقل یک شماره تماس الزامی است')
|
||||||
.of(yup.string().required('شماره تماس الزامی است')),
|
.of(yup.string().required('شماره تماس الزامی است')),
|
||||||
instagram: yup.string().url('لینک اینستاگرام معتبر نیست').nullable(),
|
instagram: yup.string().url('لینک اینستاگرام معتبر نیست').nullable(),
|
||||||
|
domain: yup
|
||||||
|
.string()
|
||||||
|
.nullable()
|
||||||
|
.test('domain-or-url', 'آدرس وبسایت معتبر نیست', (value) => {
|
||||||
|
if (!value) return true
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (/^https?:\/\//i.test(trimmed)) {
|
||||||
|
try {
|
||||||
|
new URL(trimmed)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(\/.*)?$/.test(trimmed)
|
||||||
|
}),
|
||||||
address: yup.string().required('آدرس الزامی است'),
|
address: yup.string().required('آدرس الزامی است'),
|
||||||
latitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
|
latitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
|
||||||
longitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
|
longitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
|
||||||
@@ -52,6 +68,7 @@ const GeneralSettings: FC = () => {
|
|||||||
menuColor: '#000000',
|
menuColor: '#000000',
|
||||||
phones: [''],
|
phones: [''],
|
||||||
instagram: '',
|
instagram: '',
|
||||||
|
domain: '',
|
||||||
address: '',
|
address: '',
|
||||||
latitude: 0,
|
latitude: 0,
|
||||||
longitude: 0,
|
longitude: 0,
|
||||||
@@ -112,6 +129,7 @@ const GeneralSettings: FC = () => {
|
|||||||
? [restaurantData.phone]
|
? [restaurantData.phone]
|
||||||
: [''],
|
: [''],
|
||||||
instagram: restaurantData.instagram || '',
|
instagram: restaurantData.instagram || '',
|
||||||
|
domain: restaurantData.domain || '',
|
||||||
address: restaurantData.address || '',
|
address: restaurantData.address || '',
|
||||||
latitude: restaurantData.latitude || 0,
|
latitude: restaurantData.latitude || 0,
|
||||||
longitude: restaurantData.longitude || 0,
|
longitude: restaurantData.longitude || 0,
|
||||||
@@ -206,6 +224,22 @@ const GeneralSettings: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8'>
|
||||||
|
<Input
|
||||||
|
label='آدرس وبسایت (دامنه)'
|
||||||
|
name='domain'
|
||||||
|
value={formik.values.domain || ''}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
error_text={formik.touched.domain && formik.errors.domain ? formik.errors.domain : undefined}
|
||||||
|
placeholder='example.com یا https://example.com'
|
||||||
|
isNotRequired
|
||||||
|
/>
|
||||||
|
<p className='mt-2 text-xs text-description'>
|
||||||
|
در صورتی که دامنه خریداری کرده اید آدرس دامنه خریداری شده رو وارد کنید
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<Textarea
|
<Textarea
|
||||||
label='آدرس'
|
label='آدرس'
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import Tabs from "@/components/Tabs";
|
import Tabs from "@/components/Tabs";
|
||||||
import { Brush2, Money2, NotificationStatus, Setting4 } from "iconsax-react";
|
import { Brush2, Money2, NotificationStatus, ScanBarcode, Setting4 } from "iconsax-react";
|
||||||
import { useState, type FC } from "react";
|
import { useState, type FC } from "react";
|
||||||
import NotificationsList from "../notifications/List";
|
import NotificationsList from "../notifications/List";
|
||||||
import DesignSettings from "./components/DesignSettings";
|
import DesignSettings from "./components/DesignSettings";
|
||||||
|
import QrCodeSettings from "./components/QrCodeSettings";
|
||||||
import ScoreSetting from "./components/ScoreSetting";
|
import ScoreSetting from "./components/ScoreSetting";
|
||||||
import { SettingTabEnum } from "./enum/Enum";
|
import { SettingTabEnum } from "./enum/Enum";
|
||||||
import GeneralSettings from "./GeneralSettings";
|
import GeneralSettings from "./GeneralSettings";
|
||||||
@@ -37,6 +38,11 @@ const Setting: FC = () => {
|
|||||||
label: "تنظیمات طراحی",
|
label: "تنظیمات طراحی",
|
||||||
value: SettingTabEnum.DESIGN,
|
value: SettingTabEnum.DESIGN,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <ScanBarcode color={activeTab === SettingTabEnum.QR_CODE ? "black" : "#8C90A3"} size={24} />,
|
||||||
|
label: "QR کد",
|
||||||
|
value: SettingTabEnum.QR_CODE,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onChange={(value: string) => setActiveTab(value as SettingTabEnum)}
|
onChange={(value: string) => setActiveTab(value as SettingTabEnum)}
|
||||||
active={activeTab}
|
active={activeTab}
|
||||||
@@ -48,6 +54,7 @@ const Setting: FC = () => {
|
|||||||
{activeTab === SettingTabEnum.FINANCIAL && <ScoreSetting />}
|
{activeTab === SettingTabEnum.FINANCIAL && <ScoreSetting />}
|
||||||
{activeTab === SettingTabEnum.NOTIFICATION && <NotificationsList />}
|
{activeTab === SettingTabEnum.NOTIFICATION && <NotificationsList />}
|
||||||
{activeTab === SettingTabEnum.DESIGN && <DesignSettings />}
|
{activeTab === SettingTabEnum.DESIGN && <DesignSettings />}
|
||||||
|
{activeTab === SettingTabEnum.QR_CODE && <QrCodeSettings />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import Button from "@/components/Button";
|
||||||
|
import { MENU_SITE_ORIGIN } from "@/config/menuPreview";
|
||||||
|
import { DocumentDownload } from "iconsax-react";
|
||||||
|
import { QRCodeCanvas } from "qrcode.react";
|
||||||
|
import { useMemo, useRef, type FC } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useGetRestaurant } from "../hooks/useSettingData";
|
||||||
|
|
||||||
|
const normalizeMenuUrl = (domain?: string | null, slug?: string | null): string => {
|
||||||
|
const trimmedDomain = domain?.trim();
|
||||||
|
|
||||||
|
if (trimmedDomain) {
|
||||||
|
if (/^https?:\/\//i.test(trimmedDomain)) {
|
||||||
|
return trimmedDomain.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
return `https://${trimmedDomain.replace(/\/+$/, "")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slug) {
|
||||||
|
return `${MENU_SITE_ORIGIN}/${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const QrCodeSettings: FC = () => {
|
||||||
|
const { data: restaurant, isLoading } = useGetRestaurant();
|
||||||
|
const canvasWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const menuUrl = useMemo(
|
||||||
|
() => normalizeMenuUrl(restaurant?.data?.domain, restaurant?.data?.slug),
|
||||||
|
[restaurant?.data?.domain, restaurant?.data?.slug],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
const canvas = canvasWrapperRef.current?.querySelector("canvas");
|
||||||
|
if (!canvas) {
|
||||||
|
toast.error("QR کد آماده نیست");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
const fileName = restaurant?.data?.slug || restaurant?.data?.name || "menu";
|
||||||
|
link.download = `qr-${fileName}.png`;
|
||||||
|
link.href = canvas.toDataURL("image/png");
|
||||||
|
link.click();
|
||||||
|
toast.success("QR کد با موفقیت دانلود شد");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-4xl p-8">
|
||||||
|
<div className="text-sm text-gray-500">در حال بارگذاری...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!menuUrl) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-4xl p-8">
|
||||||
|
<div className="text-lg font-light">QR کد منو</div>
|
||||||
|
<p className="mt-4 text-sm text-gray-500">
|
||||||
|
برای ساخت QR کد، ابتدا دامنه وبسایت را در تنظیمات عمومی وارد کنید یا از اسلاگ رستوران استفاده شود.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-4xl p-8">
|
||||||
|
<div className="text-lg font-light">QR کد منو</div>
|
||||||
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
|
این QR کد به آدرس منوی دیجیتال رستوران شما لینک میشود. میتوانید آن را دانلود و چاپ کنید.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col items-center gap-6">
|
||||||
|
<div
|
||||||
|
ref={canvasWrapperRef}
|
||||||
|
className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm"
|
||||||
|
>
|
||||||
|
<QRCodeCanvas
|
||||||
|
value={menuUrl}
|
||||||
|
size={256}
|
||||||
|
level="H"
|
||||||
|
includeMargin
|
||||||
|
bgColor="#FFFFFF"
|
||||||
|
fgColor="#000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-md text-center">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">آدرس منو</div>
|
||||||
|
<a
|
||||||
|
href={menuUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm text-primary break-all hover:underline"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
|
{menuUrl}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="button" className="w-fit px-6" onClick={handleDownload}>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<DocumentDownload size={20} color="#fff" />
|
||||||
|
<span>دانلود QR کد</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QrCodeSettings;
|
||||||
@@ -5,4 +5,5 @@ export const enum SettingTabEnum {
|
|||||||
DELIVERY = "delivery",
|
DELIVERY = "delivery",
|
||||||
NOTIFICATION = "notification",
|
NOTIFICATION = "notification",
|
||||||
DESIGN = "design",
|
DESIGN = "design",
|
||||||
|
QR_CODE = "qr_code",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type UpdateRestaurantType = {
|
|||||||
longitude?: number;
|
longitude?: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
establishedYear?: number | null;
|
establishedYear?: number | null;
|
||||||
|
domain?: string;
|
||||||
serviceArea?: GeoJSONPolygon;
|
serviceArea?: GeoJSONPolygon;
|
||||||
enableAiChat?: boolean;
|
enableAiChat?: boolean;
|
||||||
score?: {
|
score?: {
|
||||||
|
|||||||
Reference in New Issue
Block a user