auto select and edit

This commit is contained in:
hamid zarghami
2025-07-15 12:40:39 +03:30
parent 66b1dc15d8
commit 558d03533b
38 changed files with 5269 additions and 296 deletions
@@ -0,0 +1,511 @@
import {
PersonalityDataType,
HorizontalAlignment,
VerticalAlignment,
ButtonSize,
ImageSize,
TextType,
ButtonType,
ImageType,
SectionType,
} from "../types/Types";
// تابع برای تبدیل تصویر به base64
const imageToBase64 = async (imageUrl: string): Promise<string> => {
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (error) {
console.error("Error converting image to base64:", error);
return imageUrl; // در صورت خطا، URL اصلی را برگردان
}
};
// تابع برای تبدیل همه تصاویر در داده‌ها به base64
const convertImagesToBase64 = async (
data: PersonalityDataType
): Promise<PersonalityDataType> => {
const convertSectionImages = async (
section: SectionType
): Promise<SectionType> => {
const items = await Promise.all(
section.items.map(async (item) => {
// تبدیل تصاویر در images array
const images = await Promise.all(
item.images.map(async (image) => ({
...image,
src: await imageToBase64(image.src),
}))
);
// تبدیل background image
let backgroundImage = item.backgroundImage;
if (backgroundImage) {
backgroundImage = await imageToBase64(backgroundImage);
}
return {
...item,
images,
backgroundImage,
};
})
);
return {
...section,
items,
};
};
const [header, content, footer] = await Promise.all([
convertSectionImages(data.header),
convertSectionImages(data.content),
convertSectionImages(data.footer),
]);
return {
header,
content,
footer,
};
};
export const exportPersonalityToHTML = async (
data: PersonalityDataType
): Promise<string> => {
// تبدیل تصاویر به base64
const dataWithBase64Images = await convertImagesToBase64(data);
// Helper functions
const getHorizontalAlign = (alignment?: HorizontalAlignment) => {
switch (alignment) {
case HorizontalAlignment.LEFT:
return "left";
case HorizontalAlignment.RIGHT:
return "right";
case HorizontalAlignment.CENTER:
default:
return "center";
}
};
const getVerticalAlign = (verticalAlignment?: VerticalAlignment) => {
switch (verticalAlignment) {
case VerticalAlignment.TOP:
return "top";
case VerticalAlignment.BOTTOM:
return "bottom";
case VerticalAlignment.MIDDLE:
default:
return "middle";
}
};
// Render TextRenderer equivalent
const renderTexts = (texts: TextType[]) => {
if (!texts || texts.length === 0) return "";
return texts
.map(
(textItem, textIndex) =>
`<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse;">
<tbody>
<tr>
<td align="${textItem.alignment || "center"}" style="color: ${
textItem.color || "#000000"
}; font-size: ${
textItem.fontSize || 14
}px; line-height: 1.4; padding-bottom: ${
textIndex < (texts?.length || 0) - 1 ? "4px" : "0"
};">
${textItem.text}
</td>
</tr>
</tbody>
</table>`
)
.join("");
};
// Render ButtonRenderer equivalent
const renderButtons = (buttons: ButtonType[]) => {
if (!buttons || buttons.length === 0) return "";
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse;">
<tbody>
<tr>
<td align="${getHorizontalAlign(
buttons[0]?.alignment
)}" valign="${getVerticalAlign(
buttons[0]?.verticalAlignment
)}" style="width: 100%;">
<table cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse;">
<tbody>
<tr>
${buttons
.slice(0, 2)
.map(
(button, buttonIndex) =>
`<td style="padding-right: ${
buttonIndex === 0 && buttons.length > 1 ? "4px" : "0"
};">
<table cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse; background-color: ${
button.backgroundColor
}; border: ${
button.isBorder
? `${button.borderSize}px solid ${button.borderColor}`
: "none"
}; border-radius: 4px;">
<tbody>
<tr>
<td align="center" valign="middle" style="padding: ${
button.size === ButtonSize.SMALL
? "2px 4px"
: button.size === ButtonSize.MEDIUM
? "4px 8px"
: "8px 12px"
}; font-size: ${
button.size === ButtonSize.SMALL
? "11px"
: button.size === ButtonSize.MEDIUM
? "13px"
: "15px"
}; color: ${
button.textColor
}; text-decoration: none; white-space: nowrap; max-width: fit-content; overflow: hidden; text-overflow: ellipsis;">
<a href="${button.link || "#"}" style="color: ${
button.textColor
}; text-decoration: none;">
${button.text}
</a>
</td>
</tr>
</tbody>
</table>
</td>`
)
.join("")}
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>`;
};
// Render ImageRenderer equivalent
const renderImages = (images: ImageType[]) => {
if (!images || images.length === 0) return "";
const shouldUseBackgroundImage = (imageSize: ImageSize) => {
return (
imageSize === ImageSize.REPEAT ||
imageSize === ImageSize.REPEAT_X ||
imageSize === ImageSize.REPEAT_Y
);
};
const getBackgroundStyle = (image: ImageType) => {
let backgroundRepeat = "no-repeat";
switch (image.size) {
case ImageSize.REPEAT:
backgroundRepeat = "repeat";
break;
case ImageSize.REPEAT_X:
backgroundRepeat = "repeat-x";
break;
case ImageSize.REPEAT_Y:
backgroundRepeat = "repeat-y";
break;
default:
backgroundRepeat = "no-repeat";
break;
}
const horizontal =
image.alignment === HorizontalAlignment.LEFT
? "left"
: image.alignment === HorizontalAlignment.RIGHT
? "right"
: "center";
const vertical =
image.verticalAlignment === VerticalAlignment.TOP
? "top"
: image.verticalAlignment === VerticalAlignment.BOTTOM
? "bottom"
: "center";
return `background-image: url(${
image.src
}); background-position: ${horizontal} ${vertical}; background-repeat: ${backgroundRepeat}; width: ${
image.width || "100%"
}; height: ${image.height || "100px"};`;
};
const renderImage = (image: ImageType) => {
if (shouldUseBackgroundImage(image.size)) {
return `<div style="${getBackgroundStyle(image)}">&nbsp;</div>`;
}
if (image.size === ImageSize.CONTAIN) {
return `<table width="${
image.width || "100%"
}" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td align="center" valign="middle" style="text-align: center;">
<img src="${image.src}" alt="${
image.alt || "تصویر آپلود شده"
}" width="${image.width || "100%"}" height="${
image.height || "auto"
}" style="border: 0;" />
</td>
</tr>
</tbody>
</table>`;
}
return `<img src="${image.src}" alt="${
image.alt || "تصویر آپلود شده"
}" width="${image.width || "100%"}" height="${
image.height || "auto"
}" style="border: 0;" />`;
};
return images
.map(
(image, imageIndex) =>
`<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td align="${getHorizontalAlign(
image.alignment
)}" valign="${getVerticalAlign(
image.verticalAlignment
)}" style="padding-bottom: ${
imageIndex < images.length - 1 ? "4px" : "0"
};">
${renderImage(image)}
</td>
</tr>
</tbody>
</table>`
)
.join("");
};
// Helper functions for button alignment (same as in React components)
const getButtonHorizontalAlign = (button?: {
alignment?: HorizontalAlignment;
}) => {
if (!button || !button.alignment) return "center";
switch (button.alignment) {
case HorizontalAlignment.LEFT:
return "left";
case HorizontalAlignment.RIGHT:
return "right";
case HorizontalAlignment.CENTER:
default:
return "center";
}
};
const getButtonVerticalAlign = (button?: {
verticalAlignment?: VerticalAlignment;
}) => {
if (!button || !button.verticalAlignment) return "middle";
switch (button.verticalAlignment) {
case VerticalAlignment.TOP:
return "top";
case VerticalAlignment.BOTTOM:
return "bottom";
case VerticalAlignment.MIDDLE:
default:
return "middle";
}
};
// Render section equivalent (HeaderSection, ContentSection, FooterSection)
const renderSection = (
section: SectionType,
sectionHeight: string,
paddingTop = ""
) => {
if (section.columnsCount > 0) {
return `<tr>
${paddingTop ? `<td style="padding-top: ${paddingTop};">` : "<td>"}
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td style="padding: 10px; border-radius: 24px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="table-layout: fixed;">
<tbody>
<tr>
${Array.from(
{ length: section.columnsCount },
(_, index) => {
const item = section.items[index];
const hasButtons =
item?.buttons && item.buttons.length > 0;
const hasTexts =
item?.texts && item.texts.length > 0;
let result = `<td style="width: ${
100 / section.columnsCount
}%; height: ${sectionHeight}; background-color: ${
item?.backgroundColor || "#ffffff"
}; ${
item?.backgroundImage
? `background-image: url(${
item.backgroundImage
}); background-size: ${
item?.style?.backgroundSize || "cover"
}; background-repeat: ${
item?.style?.backgroundRepeat || "no-repeat"
}; background-position: ${
item?.style?.backgroundPosition || "center"
};`
: ""
} padding: 0; vertical-align: top; border-radius: 8px; position: relative;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse; height: ${sectionHeight};">
<tbody>`;
// اگر فقط دکمه داریم و متن نداریم
if (!hasTexts && hasButtons) {
result += `<tr>
<td width="100%" height="${sectionHeight}" align="${getButtonHorizontalAlign(
item.buttons[0]
)}" valign="${getButtonVerticalAlign(
item.buttons[0]
)}" style="padding: 8px;">
${renderButtons(item.buttons)}
</td>
</tr>`;
} else {
// ردیف اصلی برای متن و تصاویر
const mainHeight = hasButtons
? sectionHeight === "123px"
? "60"
: sectionHeight === "246px"
? "203"
: "40"
: sectionHeight === "123px"
? "87"
: sectionHeight === "246px"
? "230"
: "67";
result += `<tr>
<td width="100%" height="${mainHeight}" align="${
item?.texts?.[0]?.alignment || "center"
}" valign="${
item?.texts?.[0]?.verticalAlignment ===
VerticalAlignment.TOP
? "top"
: item?.texts?.[0]?.verticalAlignment ===
VerticalAlignment.BOTTOM
? "bottom"
: "middle"
}" style="padding: 8px; font-size: 14px; line-height: 1.4;">
${renderTexts(item?.texts || [])}
${renderImages(item?.images || [])}
</td>
</tr>`;
// ردیف دکمه‌ها
if (hasButtons) {
result += `<tr>
<td width="100%" height="27" align="${getButtonHorizontalAlign(
item.buttons[0]
)}" valign="${getButtonVerticalAlign(
item.buttons[0]
)}" style="padding: 0 8px 8px 8px;">
${renderButtons(item.buttons)}
</td>
</tr>`;
}
}
result += `</tbody>
</table>
</td>`;
// فاصله بین ستون‌ها
if (index < section.columnsCount - 1) {
result += `<td style="width: 16px;"></td>`;
}
return result;
}
).join("")}
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>`;
} else {
// حالت خالی
const emptyMessages = {
"123px": "سربرگ ایمیل",
"246px": "محتوای ایمیل",
};
const emptyMessage =
emptyMessages[sectionHeight as keyof typeof emptyMessages] ||
"فوتر ایمیل";
return `<tr>
${paddingTop ? `<td style="padding-top: ${paddingTop};">` : "<td>"}
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td style="height: ${
sectionHeight === "246px" ? "286px" : sectionHeight
}; border-radius: 24px; text-align: center; vertical-align: middle; padding: 10px;">
<div style="color: #888; font-size: 14px; margin-bottom: 8px;">
${emptyMessage}
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>`;
}
};
// Generate complete HTML exactly like Personality.tsx structure
const html = `<div style="max-width: 600px; margin: 0 auto; direction: rtl;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse;">
<tbody>
${renderSection(dataWithBase64Images.header, "123px")}
${renderSection(dataWithBase64Images.content, "246px", "16px")}
${renderSection(dataWithBase64Images.footer, "123px", "16px")}
</tbody>
</table>
</div>`;
return html;
};
export const copyHTMLToClipboard = async (html: string): Promise<boolean> => {
try {
await navigator.clipboard.writeText(html);
return true;
} catch (error) {
console.error("Failed to copy HTML to clipboard:", error);
return false;
}
};