Files
dmail-admin/src/pages/setting/personality/utils/ExportHTML.ts
T
hamid zarghami d15a63a510 content text
2025-08-03 17:01:17 +03:30

596 lines
22 KiB
TypeScript

import {
PersonalityDataType,
HorizontalAlignment,
VerticalAlignment,
ButtonSize,
ImageSize,
TextType,
ButtonType,
ImageType,
SectionType,
SocialType,
SocialNetworkType,
SocialIconSize,
SocialIconStyle,
} from "../types/Types";
import {
getSocialIconPath,
hasPngIcon,
getSocialColor,
getSocialSvgIcon,
} from "./socialIcons";
export const exportPersonalityToHTML = async (
data: PersonalityDataType
): Promise<string> => {
console.log(
"🔥 [ExportHTML] Starting export with data:",
JSON.stringify(data, null, 2)
);
// 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[]) => {
console.log("🔥 [ExportHTML] renderTexts called with:", texts);
if (!texts || texts.length === 0) {
console.log("🔥 [ExportHTML] No texts to render");
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 || "right"}" 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[]) => {
console.log("🔥 [ExportHTML] renderButtons called with:", buttons);
if (!buttons || buttons.length === 0) {
console.log("🔥 [ExportHTML] No buttons to render");
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[]) => {
console.log("🔥 [ExportHTML] renderImages called with:", images);
if (!images || images.length === 0) {
console.log("🔥 [ExportHTML] No images to render");
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: right;">
<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("");
};
// Render SocialRenderer equivalent
const renderSocials = (socials: SocialType[]) => {
console.log("🔥 [ExportHTML] renderSocials called with:", socials);
if (!socials || socials.length === 0) {
console.log("🔥 [ExportHTML] No socials to render");
return "";
}
const getIconSize = (size: SocialIconSize) => {
switch (size) {
case SocialIconSize.SMALL:
return "24px";
case SocialIconSize.MEDIUM:
return "32px";
case SocialIconSize.LARGE:
return "40px";
default:
return "32px";
}
};
const getIconStyle = (social: SocialType) => {
const size = getIconSize(social.size);
const baseStyle = `width: ${size}; height: ${size}; display: inline-flex; align-items: center; justify-content: center; text-decoration: none; color: ${
social.color || getSocialColor(social.networkType)
}; margin: 2px;`;
switch (social.style) {
case SocialIconStyle.CIRCLE:
return `${baseStyle} border-radius: 50%;`;
case SocialIconStyle.SQUARE:
return `${baseStyle} border-radius: 0;`;
case SocialIconStyle.ROUNDED:
return `${baseStyle} border-radius: 8px;`;
default:
return baseStyle;
}
};
const getSocialIcon = (networkType: SocialNetworkType) => {
// Check if PNG icon is available, if so use it
if (hasPngIcon(networkType)) {
const iconPath = getSocialIconPath(networkType);
return `<img src="${iconPath}" alt="${networkType} icon" style="width: 100%; height: 100%; object-fit: contain; display: block;" />`;
}
// Use shared SVG function
return getSocialSvgIcon(networkType, false) as string;
};
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse;">
<tbody>
<tr>
<td align="${getHorizontalAlign(
socials[0]?.alignment
)}" valign="${getVerticalAlign(
socials[0]?.verticalAlignment
)}" style="padding: 4px 0;">
<div style="display: flex; justify-content: ${getHorizontalAlign(
socials[0]?.alignment
)}; gap: 4px; flex-wrap: wrap;">
${socials
.map(
(social) =>
`<a href="${social.link || "#"}" style="${getIconStyle(
social
)} text-decoration: none;">
${getSocialIcon(social.networkType)}
</a>`
)
.join("")}
</div>
</td>
</tr>
</tbody>
</table>`;
};
// 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 = ""
) => {
console.log(`🔥 [ExportHTML] Rendering section:`, {
columnsCount: section.columnsCount,
itemsLength: section.items?.length || 0,
sectionHeight,
items: section.items,
});
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: 0px; 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;
const hasImages =
item?.images && item.images.length > 0;
const hasSocials =
item?.socials && item.socials.length > 0;
console.log(
`🔥 [ExportHTML] Item ${index} content check:`,
{
hasButtons,
hasTexts,
hasImages,
hasSocials,
sectionHeight,
item: item,
}
);
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 ||
hasImages ||
hasSocials
) {
// اگر فقط دکمه داریم و متن نداریم
if (!hasTexts && hasButtons) {
result += `<tr>
<td width="100%" height="${sectionHeight}" align="${getButtonHorizontalAlign(
item.buttons[0]
)}" valign="${getButtonVerticalAlign(
item.buttons[0]
)}" style="padding: 8px;">
${renderImages(item?.images || [])}
${renderSocials(item?.socials || [])}
${
sectionHeight === "246px"
? '<div style="margin: 0 0 10px 0; text-align: right; color: #888; font-size: 14px;">{{content}}</div>'
: ""
}
${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}" style="padding: 20px; vertical-align: top; font-size: 15px; line-height: 1.8;">
${renderTexts(item?.texts || [])}
${renderImages(item?.images || [])}
${renderSocials(item?.socials || [])}
${
sectionHeight === "246px" &&
!hasTexts &&
!hasImages &&
!hasSocials
? '<div style="margin: 0; text-align: right; color: #888; font-size: 14px;">{{content}}</div>'
: ""
}
</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>`;
}
}
} else {
// اگر هیچ محتوایی نداریم و content section است
console.log(
"🔥 [ExportHTML] Empty content section detected, adding {{content}} placeholder"
);
if (sectionHeight === "246px") {
result += `<tr>
<td width="100%" height="230" style="padding: 20px; vertical-align: top; font-size: 15px; line-height: 1.8;">
<div style="margin: 0; text-align: right; color: #888; font-size: 14px;">{{content}}</div>
</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: right; vertical-align: middle; padding: 0px;">
<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(data.header, "123px")}
${renderSection(data.content, "246px", "")}
${renderSection(data.footer, "123px", "")}
</tbody>
</table>
</div>`;
console.log("🔥 [ExportHTML] Final HTML generated:", html);
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;
}
};