auto select and edit
This commit is contained in:
@@ -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)}"> </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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user