generate qr code
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-07-27 10:08:49 +03:30
parent f30ee4f6e3
commit e283d4a5c4
7 changed files with 182 additions and 1 deletions
+21
View File
@@ -20,6 +20,7 @@
"iconsax-react": "^0.0.8",
"leaflet": "^1.9.4",
"moment-jalaali": "^0.10.4",
"qrcode.react": "^4.2.0",
"rc-rate": "^2.13.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
@@ -46,6 +47,7 @@
"@types/leaflet": "^1.9.21",
"@types/moment-jalaali": "^0.7.9",
"@types/node": "^24.10.1",
"@types/qrcode.react": "^1.0.5",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
@@ -2383,6 +2385,16 @@
"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": {
"version": "19.1.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz",
@@ -4987,6 +4999,15 @@
"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": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+2
View File
@@ -22,6 +22,7 @@
"iconsax-react": "^0.0.8",
"leaflet": "^1.9.4",
"moment-jalaali": "^0.10.4",
"qrcode.react": "^4.2.0",
"rc-rate": "^2.13.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
@@ -48,6 +49,7 @@
"@types/leaflet": "^1.9.21",
"@types/moment-jalaali": "^0.7.9",
"@types/node": "^24.10.1",
"@types/qrcode.react": "^1.0.5",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
+34
View File
@@ -28,6 +28,22 @@ const validationSchema = yup.object({
.min(1, 'حداقل یک شماره تماس الزامی است')
.of(yup.string().required('شماره تماس الزامی است')),
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('آدرس الزامی است'),
latitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
longitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
@@ -52,6 +68,7 @@ const GeneralSettings: FC = () => {
menuColor: '#000000',
phones: [''],
instagram: '',
domain: '',
address: '',
latitude: 0,
longitude: 0,
@@ -112,6 +129,7 @@ const GeneralSettings: FC = () => {
? [restaurantData.phone]
: [''],
instagram: restaurantData.instagram || '',
domain: restaurantData.domain || '',
address: restaurantData.address || '',
latitude: restaurantData.latitude || 0,
longitude: restaurantData.longitude || 0,
@@ -206,6 +224,22 @@ const GeneralSettings: FC = () => {
/>
</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'>
<Textarea
label='آدرس'
+8 -1
View File
@@ -1,8 +1,9 @@
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 NotificationsList from "../notifications/List";
import DesignSettings from "./components/DesignSettings";
import QrCodeSettings from "./components/QrCodeSettings";
import ScoreSetting from "./components/ScoreSetting";
import { SettingTabEnum } from "./enum/Enum";
import GeneralSettings from "./GeneralSettings";
@@ -37,6 +38,11 @@ const Setting: FC = () => {
label: "تنظیمات طراحی",
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)}
active={activeTab}
@@ -48,6 +54,7 @@ const Setting: FC = () => {
{activeTab === SettingTabEnum.FINANCIAL && <ScoreSetting />}
{activeTab === SettingTabEnum.NOTIFICATION && <NotificationsList />}
{activeTab === SettingTabEnum.DESIGN && <DesignSettings />}
{activeTab === SettingTabEnum.QR_CODE && <QrCodeSettings />}
</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;
+1
View File
@@ -5,4 +5,5 @@ export const enum SettingTabEnum {
DELIVERY = "delivery",
NOTIFICATION = "notification",
DESIGN = "design",
QR_CODE = "qr_code",
}
+1
View File
@@ -18,6 +18,7 @@ export type UpdateRestaurantType = {
longitude?: number;
description?: string;
establishedYear?: number | null;
domain?: string;
serviceArea?: GeoJSONPolygon;
enableAiChat?: boolean;
score?: {