From fe5b7de9b1018bc48e9b580297383b208bf0f2a9 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 16 Jul 2025 12:24:51 +0330 Subject: [PATCH] screen shot test --- package.json | 4 + pnpm-lock.yaml | 66 +++++++ src/pages/setting/Setting.tsx | 93 +++++----- src/pages/setting/personality/Personality.tsx | 2 +- .../personality/hooks/usePersonalityData.ts | 6 + .../setting/personality/service/Service.ts | 10 + src/pages/setting/personality/types/Types.ts | 1 + .../personality/utils/screenshotHelper.ts | 174 ++++++++++++++++++ 8 files changed, 309 insertions(+), 47 deletions(-) create mode 100644 src/pages/setting/personality/utils/screenshotHelper.ts diff --git a/package.json b/package.json index 92f1890..7c49024 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "axios": "^1.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dom-to-image": "^2.6.0", + "html2canvas": "^1.4.1", "i18next": "^25.0.1", "iconsax-react": "^0.0.8", "js-cookie": "^3.0.5", @@ -51,6 +53,8 @@ }, "devDependencies": { "@eslint/js": "^9.22.0", + "@types/dom-to-image": "^2.6.7", + "@types/html2canvas": "^1.0.0", "@types/js-cookie": "^3.0.6", "@types/moment-jalaali": "^0.7.9", "@types/react": "^19.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6530d44..1a3b7b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,12 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + dom-to-image: + specifier: ^2.6.0 + version: 2.6.0 + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 i18next: specifier: ^25.0.1 version: 25.2.1(typescript@5.7.3) @@ -123,6 +129,12 @@ importers: '@eslint/js': specifier: ^9.22.0 version: 9.27.0 + '@types/dom-to-image': + specifier: ^2.6.7 + version: 2.6.7 + '@types/html2canvas': + specifier: ^1.0.0 + version: 1.0.0 '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 @@ -1070,6 +1082,9 @@ packages: '@types/babel__traverse@7.20.7': resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/dom-to-image@2.6.7': + resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==} + '@types/draco3d@1.4.10': resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} @@ -1082,6 +1097,10 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/html2canvas@1.0.0': + resolution: {integrity: sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==} + deprecated: This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed. + '@types/js-cookie@3.0.6': resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} @@ -1285,6 +1304,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1400,6 +1423,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1458,6 +1484,9 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dom-to-image@2.6.0: + resolution: {integrity: sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==} + draco3d@1.5.7: resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} @@ -1802,6 +1831,10 @@ packages: html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + http2-client@1.3.5: resolution: {integrity: sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==} @@ -2795,6 +2828,9 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + three-mesh-bvh@0.8.3: resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} peerDependencies: @@ -2960,6 +2996,9 @@ packages: resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} engines: {node: '>= 4'} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -4107,6 +4146,8 @@ snapshots: dependencies: '@babel/types': 7.27.3 + '@types/dom-to-image@2.6.7': {} + '@types/draco3d@1.4.10': {} '@types/es-aggregate-error@1.0.6': @@ -4119,6 +4160,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/html2canvas@1.0.0': + dependencies: + html2canvas: 1.4.1 + '@types/js-cookie@3.0.6': {} '@types/json-schema@7.0.15': {} @@ -4350,6 +4395,8 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: {} + base64-js@1.5.1: {} bidi-js@1.0.3: @@ -4463,6 +4510,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + csstype@3.1.3: {} data-view-buffer@1.0.2: @@ -4524,6 +4575,8 @@ snapshots: dependencies: path-type: 4.0.0 + dom-to-image@2.6.0: {} + draco3d@1.5.7: {} dunder-proto@1.0.1: @@ -4960,6 +5013,11 @@ snapshots: dependencies: void-elements: 3.1.0 + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + http2-client@1.3.5: {} human-signals@2.1.0: {} @@ -5968,6 +6026,10 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + three-mesh-bvh@0.8.3(three@0.177.0): dependencies: three: 0.177.0 @@ -6134,6 +6196,10 @@ snapshots: utility-types@3.11.0: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + uuid@11.1.0: {} validator@13.15.15: {} diff --git a/src/pages/setting/Setting.tsx b/src/pages/setting/Setting.tsx index c1e9b42..993a2fb 100644 --- a/src/pages/setting/Setting.tsx +++ b/src/pages/setting/Setting.tsx @@ -13,26 +13,28 @@ import Button from '@/components/Button' import { TickCircle } from 'iconsax-react' import { usePersonalityStore } from './personality/store/Store' import { exportPersonalityToHTML } from './personality/utils/ExportHTML' +import { capturePersonalityScreenshot } from './personality/utils/screenshotHelper' import Input from '@/components/Input' -import { useSaveTemplate, useUpdateTemplate, useGetTemplate } from './personality/hooks/usePersonalityData' +import { useSaveTemplate, useUpdateTemplate, useGetTemplate, useSingleUpload } from './personality/hooks/usePersonalityData' import { toast } from '@/components/Toast' const Setting: FC = () => { - const { t } = useTranslation() const { id } = useParams() const { data } = usePersonalityStore() const location = useLocation() const [activeTab, setActiveTab] = useState(SettingTabEnum.SETTING_DOMAIN) const [templateName, setTemplateName] = useState('') + const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false) + const { mutate: saveTemplate, isPending: isSaving } = useSaveTemplate() const { mutate: updateTemplate, isPending: isUpdating } = useUpdateTemplate() const { data: template } = useGetTemplate(id || '') + const { mutateAsync: singleUpload, isPending: isUploading } = useSingleUpload() const isEditMode = !!id - const isLoading = isSaving || isUpdating + const isLoading = isSaving || isUpdating || isUploading || isCapturingScreenshot - // پر کردن نام قالب در حالت edit useEffect(() => { if (isEditMode && template?.data?.template?.name) { setTemplateName(template.data.template.name) @@ -52,7 +54,6 @@ const Setting: FC = () => { case Paths.settingSignature: return SettingTabEnum.SETTING_SIGNATURE default: - // بررسی personality با id if (pathname.startsWith(Paths.settingPersonality + '/')) { return SettingTabEnum.SETTING_PERSONALITY } @@ -60,9 +61,6 @@ const Setting: FC = () => { } } - // Map setting tabs to URL paths - - // Update active tab when URL changes useEffect(() => { const tabFromPath = getTabFromPath(location.pathname) setActiveTab(tabFromPath) @@ -80,7 +78,7 @@ const Setting: FC = () => { return ; case SettingTabEnum.SETTING_PERSONALITY: return ( -
+
@@ -100,16 +98,31 @@ const Setting: FC = () => { return } + if (activeTab !== SettingTabEnum.SETTING_PERSONALITY) { + return + } + const html = await exportPersonalityToHTML(data) + setIsCapturingScreenshot(true) + const screenshotFile = await capturePersonalityScreenshot(templateName) + setIsCapturingScreenshot(false) + + if (!screenshotFile) { + toast('خطا در گرفتن اسکرین‌شات', 'error') + return + } + + const thumbnailUrl = await singleUpload(screenshotFile) + if (isEditMode) { - // حالت edit - استفاده از updateTemplate updateTemplate({ id: id!, params: { name: templateName, rawHtml: html, - structure: data + structure: data, + thumbnailUrl: thumbnailUrl?.data?.url } }, { onSuccess: (data) => { @@ -120,11 +133,11 @@ const Setting: FC = () => { } }) } else { - // حالت جدید - استفاده از saveTemplate saveTemplate({ name: templateName, rawHtml: html, - structure: data + structure: data, + thumbnailUrl: thumbnailUrl?.data?.url }, { onSuccess: (data) => { toast(data?.data?.message || 'قالب با موفقیت ایجاد شد', 'success') @@ -136,43 +149,31 @@ const Setting: FC = () => { } } - // const handleExportHTML = async () => { - // try { - // const html = await exportPersonalityToHTML(data) - // const success = await copyHTMLToClipboard(html) - - // if (success) { - // toast(t('export_success'), 'success') - // } else { - // toast(t('export_error'), 'error') - // } - // } catch { - // toast(t('export_error'), 'error') - // } - // } - return (

{t('setting.title')}

-
- setTemplateName(e.target.value)} - /> - -
+ { + activeTab === SettingTabEnum.SETTING_PERSONALITY && +
+ setTemplateName(e.target.value)} + /> + +
+ }
diff --git a/src/pages/setting/personality/Personality.tsx b/src/pages/setting/personality/Personality.tsx index c559113..78f2551 100644 --- a/src/pages/setting/personality/Personality.tsx +++ b/src/pages/setting/personality/Personality.tsx @@ -18,7 +18,7 @@ const Personality: FC = () => { }, [id, template, setData]) return ( -
+
diff --git a/src/pages/setting/personality/hooks/usePersonalityData.ts b/src/pages/setting/personality/hooks/usePersonalityData.ts index 7b73d9d..fd05bae 100644 --- a/src/pages/setting/personality/hooks/usePersonalityData.ts +++ b/src/pages/setting/personality/hooks/usePersonalityData.ts @@ -50,3 +50,9 @@ export const useDeleteTemplate = () => { }, }); }; + +export const useSingleUpload = () => { + return useMutation({ + mutationFn: (file: File) => api.singleUpload(file), + }); +}; diff --git a/src/pages/setting/personality/service/Service.ts b/src/pages/setting/personality/service/Service.ts index d5dd892..d8b7fbc 100644 --- a/src/pages/setting/personality/service/Service.ts +++ b/src/pages/setting/personality/service/Service.ts @@ -1,5 +1,6 @@ import axios from "@/config/axios"; import { TemplateType, TemplatesResponseType } from "../types/Types"; +import axiosDanak from "@/config/axiosDanak"; export const saveTemplate = async (params: TemplateType) => { const { data } = await axios.post("/templates", params); @@ -36,3 +37,12 @@ export const deleteTemplate = async (id: string) => { const { data } = await axios.delete(`/templates/${id}`); return data; }; + +export const singleUpload = async (file: File) => { + const formData = new FormData(); + formData.append("file", file); + + const { data } = await axiosDanak.post("/uploader/single-file", formData); + + return data; +}; diff --git a/src/pages/setting/personality/types/Types.ts b/src/pages/setting/personality/types/Types.ts index 61c8ce1..dcdd4fa 100644 --- a/src/pages/setting/personality/types/Types.ts +++ b/src/pages/setting/personality/types/Types.ts @@ -269,6 +269,7 @@ export type TemplateType = { name: string; rawHtml: string; structure: PersonalityDataType; + thumbnailUrl: string; }; export type GetTemplateResponse = { diff --git a/src/pages/setting/personality/utils/screenshotHelper.ts b/src/pages/setting/personality/utils/screenshotHelper.ts new file mode 100644 index 0000000..9052758 --- /dev/null +++ b/src/pages/setting/personality/utils/screenshotHelper.ts @@ -0,0 +1,174 @@ +import domtoimage from "dom-to-image"; + +/** + * گرفتن اسکرین‌شات با dom-to-image و برگرداندن فایل برای آپلود + */ +export const captureScreenshotAsFile = async ( + templateName: string +): Promise => { + try { + console.log("📸 Starting screenshot capture for upload..."); + + const element = await findAndValidateElement(); + if (!element) { + console.error("❌ Element not found"); + return null; + } + + // آماده‌سازی element + const htmlElement = element as HTMLElement; + const originalTransform = htmlElement.style.transform; + const originalPosition = htmlElement.style.position; + + htmlElement.style.transform = "scale(1)"; + htmlElement.style.position = "relative"; + + // صبر برای render کامل + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const options = { + quality: 1, + bgcolor: "#ffffff", + width: element.offsetWidth, + height: element.offsetHeight, + style: { + transform: "scale(1)", + transformOrigin: "top left", + }, + }; + + const dataUrl = await domtoimage.toPng(element, options); + + // بازگردانی استایل‌های اصلی + htmlElement.style.transform = originalTransform; + htmlElement.style.position = originalPosition; + + // تبدیل dataURL به File + const response = await fetch(dataUrl); + const blob = await response.blob(); + const file = new File([blob], `${templateName}-template.png`, { + type: "image/png", + }); + + console.log("✅ Screenshot file created successfully:", file); + return file; + } catch (error) { + console.error("❌ Screenshot capture failed:", error); + return null; + } +}; + +/** + * تابع اصلی - استفاده از dom-to-image و برگرداندن فایل + */ +export const capturePersonalityScreenshot = async ( + templateName: string +): Promise => { + console.log("🎯 Starting screenshot capture..."); + + try { + const screenshotFile = await captureScreenshotAsFile(templateName); + if (screenshotFile) { + console.log("✅ Screenshot captured successfully"); + return screenshotFile; + } else { + console.error("❌ Screenshot capture failed"); + return null; + } + } catch (error) { + console.error("❌ Screenshot error:", error); + return null; + } +}; + +/** + * پیدا کردن و بررسی element + */ +const findAndValidateElement = async (): Promise => { + console.log("🔍 Looking for personality element..."); + + const selectors = [ + ".personality-template", + '[class*="personality-template"]', + ".flex-1.bg-white.rounded-4xl.p-8", + ".bg-white.rounded-4xl.p-8", + 'div[class*="bg-white"][class*="rounded-4xl"]', + ]; + + for (let i = 0; i < selectors.length; i++) { + const selector = selectors[i]; + console.log(`🔍 Trying selector ${i + 1}/${selectors.length}: ${selector}`); + + const elements = document.querySelectorAll(selector); + console.log(` Found ${elements.length} elements`); + + for (let j = 0; j < elements.length; j++) { + const element = elements[j] as HTMLElement; + + const rect = element.getBoundingClientRect(); + const isVisible = rect.width > 0 && rect.height > 0; + const hasContent = element.innerHTML.trim().length > 0; + + console.log(` Element ${j + 1}:`, { + visible: isVisible, + hasContent, + dimensions: `${rect.width}x${rect.height}`, + className: element.className.slice(0, 50) + "...", + }); + + if (isVisible && hasContent) { + console.log(`✅ Found valid element with selector: ${selector}`); + return element; + } + } + } + + console.log("❌ No valid elements found"); + return null; +}; + +/** + * تابع debug برای بررسی element + */ +export const debugPersonalityElement = () => { + console.log("🔍 === DEBUG: Personality Element Detection ==="); + + const selectors = [ + ".personality-template", + '[class*="personality-template"]', + ".flex-1.bg-white.rounded-4xl.p-8", + ".bg-white.rounded-4xl", + "table", + ]; + + let foundElements = 0; + + selectors.forEach((selector, index) => { + const elements = document.querySelectorAll(selector); + console.log(`${index + 1}. "${selector}": ${elements.length} elements`); + + elements.forEach((el, elIndex) => { + const htmlEl = el as HTMLElement; + const rect = htmlEl.getBoundingClientRect(); + + console.log(` [${elIndex + 1}]`, { + visible: rect.width > 0 && rect.height > 0, + size: `${rect.width}x${rect.height}`, + hasContent: htmlEl.innerHTML.trim().length > 0, + className: htmlEl.className.slice(0, 40) + "...", + }); + + if (rect.width > 0 && rect.height > 0) { + foundElements++; + } + }); + }); + + console.log(`📊 Summary: ${foundElements} visible elements found`); + console.log("========================================="); + + return { + found: foundElements > 0, + totalElements: foundElements, + }; +};