ajout d'export et import
suppression du bouton base64 pour l'image correction de bug au niveau du texte et puce de numéro
This commit is contained in:
425
src/App.tsx
425
src/App.tsx
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { Download, Type, Image as ImageIcon, Layout, MousePointer2, Trash2, Code, AlignLeft, AlignCenter, AlignRight, Loader2, Link as LinkIcon, Smile, Minus, Heading, Bold, Italic, Underline, List, ListOrdered, Upload, Save, RotateCcw, FileX } from 'lucide-react';
|
import { Download, Type, Image as ImageIcon, Layout, MousePointer2, Trash2, Code, AlignLeft, AlignCenter, AlignRight, Loader2, Link as LinkIcon, Smile, Minus, Heading, Bold, Italic, Underline, List, ListOrdered, Upload, Save, RotateCcw, FileX, Palette } from 'lucide-react';
|
||||||
|
|
||||||
// --- CONFIGURATION & CONSTANTES ---
|
// --- CONFIGURATION & CONSTANTES ---
|
||||||
const DEFAULT_WIDTH = "600";
|
const DEFAULT_WIDTH = "600";
|
||||||
@@ -63,7 +63,7 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
|||||||
bgColor: '#eeeeee',
|
bgColor: '#eeeeee',
|
||||||
backgroundImage: '',
|
backgroundImage: '',
|
||||||
url: '', // Lien de redirection
|
url: '', // Lien de redirection
|
||||||
exportAsImage: true, // Nouveau: Détermine si l'en-tête est converti en Data Base64
|
exportAsImage: false, // Nouveau: Détermine si l'en-tête est converti en Data Base64
|
||||||
textColor: PURPLE_COLOR,
|
textColor: PURPLE_COLOR,
|
||||||
subtitleColor: ORANGE_COLOR,
|
subtitleColor: ORANGE_COLOR,
|
||||||
padding: '30',
|
padding: '30',
|
||||||
@@ -179,6 +179,7 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
|||||||
// --- COMPOSANT MINI RTE (Rich Text Editor) ---
|
// --- COMPOSANT MINI RTE (Rich Text Editor) ---
|
||||||
const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string) => void }) => {
|
const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string) => void }) => {
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const savedSelectionRef = useRef<Range | null>(null);
|
||||||
|
|
||||||
const execCmd = (command: string, value: string | undefined = undefined) => {
|
const execCmd = (command: string, value: string | undefined = undefined) => {
|
||||||
document.execCommand(command, false, value);
|
document.execCommand(command, false, value);
|
||||||
@@ -187,6 +188,30 @@ const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveSelection = () => {
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (sel && sel.rangeCount > 0) {
|
||||||
|
const range = sel.getRangeAt(0);
|
||||||
|
if (editorRef.current && editorRef.current.contains(range.commonAncestorContainer)) {
|
||||||
|
savedSelectionRef.current = range.cloneRange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColorChange = (color: string) => {
|
||||||
|
if (savedSelectionRef.current) {
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (sel) {
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(savedSelectionRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.execCommand('foreColor', false, color);
|
||||||
|
if (editorRef.current) {
|
||||||
|
onChange(editorRef.current.innerHTML);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorRef.current && editorRef.current.innerHTML !== value) {
|
if (editorRef.current && editorRef.current.innerHTML !== value) {
|
||||||
if (document.activeElement !== editorRef.current) {
|
if (document.activeElement !== editorRef.current) {
|
||||||
@@ -204,14 +229,29 @@ const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string)
|
|||||||
<div className="w-px h-4 bg-gray-300 mx-1"></div>
|
<div className="w-px h-4 bg-gray-300 mx-1"></div>
|
||||||
<RteButton onClick={() => execCmd('insertUnorderedList')} icon={<List size={14} />} title="Liste à puces" />
|
<RteButton onClick={() => execCmd('insertUnorderedList')} icon={<List size={14} />} title="Liste à puces" />
|
||||||
<RteButton onClick={() => execCmd('insertOrderedList')} icon={<ListOrdered size={14} />} title="Liste numérotée" />
|
<RteButton onClick={() => execCmd('insertOrderedList')} icon={<ListOrdered size={14} />} title="Liste numérotée" />
|
||||||
|
<div className="w-px h-4 bg-gray-300 mx-1"></div>
|
||||||
|
<Tooltip text="Couleur du texte sélectionné" className="w-auto">
|
||||||
|
<div className="flex items-center gap-1 p-1 hover:bg-gray-200 rounded cursor-pointer">
|
||||||
|
<Palette size={14} className="text-gray-700" />
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
onChange={(e) => handleColorChange(e.target.value)}
|
||||||
|
onFocus={saveSelection}
|
||||||
|
onMouseDown={saveSelection}
|
||||||
|
className="w-4 h-4 p-0 border-0 cursor-pointer bg-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
contentEditable
|
contentEditable
|
||||||
className="p-3 min-h-[150px] text-sm focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-200"
|
className="wysiwyg p-3 min-h-[150px] text-sm focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-200"
|
||||||
onInput={(e) => onChange(e.currentTarget.innerHTML)}
|
onInput={(e) => { saveSelection(); onChange(e.currentTarget.innerHTML); }}
|
||||||
onBlur={(e) => onChange(e.currentTarget.innerHTML)}
|
onBlur={(e) => { saveSelection(); onChange(e.currentTarget.innerHTML); }}
|
||||||
|
onMouseUp={saveSelection}
|
||||||
|
onKeyUp={saveSelection}
|
||||||
style={{ fontFamily: 'Helvetica, Arial, sans-serif' }}
|
style={{ fontFamily: 'Helvetica, Arial, sans-serif' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,6 +276,7 @@ export default function NewsletterBuilder() {
|
|||||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
||||||
const [globalBg, setGlobalBg] = useState('#f3f4f6');
|
const [globalBg, setGlobalBg] = useState('#f3f4f6');
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
|
const [includeMetadata, setIncludeMetadata] = useState(false);
|
||||||
|
|
||||||
const hasHeader = blocks.some(b => b.type === 'header');
|
const hasHeader = blocks.some(b => b.type === 'header');
|
||||||
const hasFooter = blocks.some(b => b.type === 'footer');
|
const hasFooter = blocks.some(b => b.type === 'footer');
|
||||||
@@ -485,7 +526,7 @@ export default function NewsletterBuilder() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateHTML = (blocksToRender: any[]) => {
|
const generateHTML = (blocksToRender: any[], originalBlocks?: any[]) => {
|
||||||
const renderBlockHTML = (block: any) => {
|
const renderBlockHTML = (block: any) => {
|
||||||
const fontFamily = block.fontFamily || FONT_OPTIONS.sans.value;
|
const fontFamily = block.fontFamily || FONT_OPTIONS.sans.value;
|
||||||
const commonStyle = `font-family: ${fontFamily}; box-sizing: border-box;`;
|
const commonStyle = `font-family: ${fontFamily}; box-sizing: border-box;`;
|
||||||
@@ -584,6 +625,7 @@ export default function NewsletterBuilder() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const builderMetadata = originalBlocks ? JSON.stringify({ blocks: originalBlocks, globalBg }) : '';
|
||||||
const rows = blocksToRender.map(renderBlockHTML).join('');
|
const rows = blocksToRender.map(renderBlockHTML).join('');
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
@@ -605,6 +647,7 @@ p { margin-top: 0; margin-bottom: 10px; }
|
|||||||
${rows}
|
${rows}
|
||||||
</table>
|
</table>
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
|
<!-- NEWSLETTER_BUILDER_DATA: ${builderMetadata} -->
|
||||||
</body></html>`;
|
</body></html>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -631,7 +674,7 @@ p { margin-top: 0; margin-bottom: 10px; }
|
|||||||
}
|
}
|
||||||
return block;
|
return block;
|
||||||
}));
|
}));
|
||||||
const html = generateHTML(processedBlocks);
|
const html = generateHTML(processedBlocks, includeMetadata ? blocks : undefined);
|
||||||
const blob = new Blob([html], { type: 'text/html' });
|
const blob = new Blob([html], { type: 'text/html' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -644,6 +687,338 @@ p { margin-top: 0; margin-bottom: 10px; }
|
|||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const importHTML = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const text = event.target?.result as string;
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
let parsedData: any = null;
|
||||||
|
|
||||||
|
// 1. Essai de lecture du commentaire HTML (Reconstruction parfaite)
|
||||||
|
const commentMatch = text.match(/<!-- NEWSLETTER_BUILDER_DATA:\s*({.*?})\s*-->/s);
|
||||||
|
if (commentMatch) {
|
||||||
|
try {
|
||||||
|
parsedData = JSON.parse(commentMatch[1]);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Échec parsing commentaire JSON", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback : Analyse du DOM HTML pur (Reconstruction par structure)
|
||||||
|
if (!parsedData) {
|
||||||
|
try {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(text, 'text/html');
|
||||||
|
const bodyBg = doc.body.style.backgroundColor || "#f3f4f6";
|
||||||
|
const innerTable = doc.querySelector('table table');
|
||||||
|
|
||||||
|
if (innerTable) {
|
||||||
|
const rows = Array.from(innerTable.querySelectorAll('tr'));
|
||||||
|
const blocksList: any[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const tr = rows[i];
|
||||||
|
|
||||||
|
// Ignorer les lignes imbriquées dans des sous-tables
|
||||||
|
const parentTable = tr.parentElement?.closest('table');
|
||||||
|
if (parentTable && parentTable !== innerTable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const td = tr.querySelector('td');
|
||||||
|
if (!td) continue;
|
||||||
|
|
||||||
|
const bgColor = td.style.backgroundColor || "#ffffff";
|
||||||
|
let padding = "10";
|
||||||
|
if (td.style.padding) {
|
||||||
|
padding = td.style.padding.replace("px", "").split(" ")[0];
|
||||||
|
}
|
||||||
|
const align = td.getAttribute("align") || "left";
|
||||||
|
|
||||||
|
// A. Séparateur / Divider
|
||||||
|
const dividerDiv = td.querySelector('div[style*="border-top"]');
|
||||||
|
if (dividerDiv) {
|
||||||
|
const styleStr = dividerDiv.getAttribute("style") || "";
|
||||||
|
const thicknessMatch = styleStr.match(/border-top:\s*(\d+)px/);
|
||||||
|
const styleMatch = styleStr.match(/border-top:\s*\d+px\s+(\w+)/);
|
||||||
|
const colorMatch = styleStr.match(/border-top:\s*\d+px\s+\w+\s+(#[a-fA-F0-9]+|rgb.*|hsl.*)/);
|
||||||
|
|
||||||
|
blocksList.push({
|
||||||
|
id: `imported-divider-${Date.now()}-${i}`,
|
||||||
|
type: 'divider',
|
||||||
|
lineStyle: styleMatch ? styleMatch[1] : 'solid',
|
||||||
|
lineColor: colorMatch ? colorMatch[1] : '#800080',
|
||||||
|
thickness: thicknessMatch ? thicknessMatch[1] : '2',
|
||||||
|
bgColor,
|
||||||
|
padding
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B. Bouton
|
||||||
|
const btnTable = td.querySelector('table');
|
||||||
|
if (btnTable) {
|
||||||
|
const btnLink = btnTable.querySelector('a');
|
||||||
|
if (btnLink) {
|
||||||
|
const url = btnLink.getAttribute("href") || "#";
|
||||||
|
const btnColor = btnTable.querySelector('td')?.getAttribute("bgcolor") || "#f37037";
|
||||||
|
const textColor = btnLink.style.color || "#ffffff";
|
||||||
|
const borderRadius = btnTable.querySelector('td')?.style.borderRadius?.replace("px", "") || "4";
|
||||||
|
|
||||||
|
blocksList.push({
|
||||||
|
id: `imported-button-${Date.now()}-${i}`,
|
||||||
|
type: 'button',
|
||||||
|
content: btnLink.textContent?.trim() || 'BOUTON',
|
||||||
|
url,
|
||||||
|
btnColor,
|
||||||
|
textColor,
|
||||||
|
bgColor,
|
||||||
|
padding,
|
||||||
|
align,
|
||||||
|
borderRadius,
|
||||||
|
width: btnLink.style.display === 'block' ? 'full' : 'auto',
|
||||||
|
fontFamily: btnLink.style.fontFamily || 'Helvetica, Arial, sans-serif'
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C. Lien simple
|
||||||
|
const simpleLink = td.querySelector('a');
|
||||||
|
const h2Tag = td.querySelector('h2');
|
||||||
|
const h1Tag = td.querySelector('h1');
|
||||||
|
|
||||||
|
if (simpleLink && !h2Tag && !h1Tag) {
|
||||||
|
const imgInside = simpleLink.querySelector('img');
|
||||||
|
if (imgInside) {
|
||||||
|
blocksList.push({
|
||||||
|
id: `imported-image-${Date.now()}-${i}`,
|
||||||
|
type: 'image',
|
||||||
|
src: imgInside.getAttribute("src") || "",
|
||||||
|
url: simpleLink.getAttribute("href") || "",
|
||||||
|
alt: imgInside.getAttribute("alt") || "",
|
||||||
|
bgColor,
|
||||||
|
padding,
|
||||||
|
align,
|
||||||
|
width: imgInside.getAttribute("width") || "100%"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
blocksList.push({
|
||||||
|
id: `imported-link-${Date.now()}-${i}`,
|
||||||
|
type: 'link',
|
||||||
|
content: simpleLink.textContent?.trim() || "",
|
||||||
|
url: simpleLink.getAttribute("href") || "",
|
||||||
|
textColor: simpleLink.style.color || "#f37037",
|
||||||
|
bgColor,
|
||||||
|
padding,
|
||||||
|
align,
|
||||||
|
fontSize: simpleLink.style.fontSize?.replace("px", "") || "14",
|
||||||
|
fontFamily: simpleLink.style.fontFamily || 'Helvetica, Arial, sans-serif'
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// D. Image sans lien
|
||||||
|
const imgTag = td.querySelector('img');
|
||||||
|
if (imgTag && !td.textContent?.includes("Pour adhérer") && !tr.innerHTML.includes("slogan")) {
|
||||||
|
blocksList.push({
|
||||||
|
id: `imported-image-${Date.now()}-${i}`,
|
||||||
|
type: 'image',
|
||||||
|
src: imgTag.getAttribute("src") || "",
|
||||||
|
url: "",
|
||||||
|
alt: imgTag.getAttribute("alt") || "",
|
||||||
|
bgColor,
|
||||||
|
padding,
|
||||||
|
align,
|
||||||
|
width: imgTag.getAttribute("width") || "100%"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E. Espace (Spacer)
|
||||||
|
if (td.style.height && td.style.height !== "1px" && !td.textContent?.trim()) {
|
||||||
|
blocksList.push({
|
||||||
|
id: `imported-spacer-${Date.now()}-${i}`,
|
||||||
|
type: 'spacer',
|
||||||
|
height: td.style.height.replace("px", ""),
|
||||||
|
bgColor
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// F. En-tête / Header (HTML non converti en image)
|
||||||
|
if (h1Tag) {
|
||||||
|
const subtitleDiv = td.querySelector('div');
|
||||||
|
const bgImgAttr = td.style.backgroundImage || "";
|
||||||
|
let bgImgUrl = "";
|
||||||
|
const bgMatch = bgImgAttr.match(/url\(['"]?(.*?)['"]?\)/);
|
||||||
|
if (bgMatch) bgImgUrl = bgMatch[1];
|
||||||
|
|
||||||
|
blocksList.push({
|
||||||
|
id: `imported-header-${Date.now()}-${i}`,
|
||||||
|
type: 'header',
|
||||||
|
content: h1Tag.innerHTML.replace(/<br>/g, "\n"),
|
||||||
|
subtitle: subtitleDiv ? subtitleDiv.textContent || "" : "",
|
||||||
|
bgColor,
|
||||||
|
backgroundImage: bgImgUrl,
|
||||||
|
url: simpleLink ? simpleLink.getAttribute("href") || "" : "",
|
||||||
|
exportAsImage: false,
|
||||||
|
textColor: h1Tag.style.color || "#800080",
|
||||||
|
subtitleColor: subtitleDiv ? subtitleDiv.style.color || "#f37037" : "#f37037",
|
||||||
|
padding,
|
||||||
|
align,
|
||||||
|
fontSize: h1Tag.style.fontSize?.replace("px", "") || "42",
|
||||||
|
subtitleFontSize: subtitleDiv ? subtitleDiv.style.fontSize?.replace("px", "") || "24" : "24",
|
||||||
|
fontFamily: h1Tag.style.fontFamily || 'Helvetica, Arial, sans-serif'
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// G. Titre
|
||||||
|
if (h2Tag) {
|
||||||
|
blocksList.push({
|
||||||
|
id: `imported-title-${Date.now()}-${i}`,
|
||||||
|
type: 'title',
|
||||||
|
content: h2Tag.innerHTML.replace(/<br>/g, "\n"),
|
||||||
|
bgColor,
|
||||||
|
textColor: h2Tag.style.color || "#800080",
|
||||||
|
padding,
|
||||||
|
align,
|
||||||
|
fontSize: h2Tag.style.fontSize?.replace("px", "") || "20",
|
||||||
|
fontFamily: h2Tag.style.fontFamily || 'Helvetica, Arial, sans-serif'
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// H. Pied de page / Footer (Identifié sur la première ligne)
|
||||||
|
if (td.textContent?.includes("Pour adhérer") || tr.innerHTML.includes("slogan")) {
|
||||||
|
let titleTop = td.textContent?.trim() || "Pour adhérer";
|
||||||
|
let btn1Text = "CONTACTER TA SECTION";
|
||||||
|
let btn1Url = "";
|
||||||
|
let btn1Color = "#800040";
|
||||||
|
let btn2Text = "COTISER EN LIGNE";
|
||||||
|
let btn2Url = "";
|
||||||
|
let btn2Color = "#22c55e";
|
||||||
|
let logoSrc = "";
|
||||||
|
let logoWidth = "80";
|
||||||
|
let slogan = "";
|
||||||
|
let address = "";
|
||||||
|
let legalText = "";
|
||||||
|
|
||||||
|
// Chercher les détails dans les lignes suivantes (le footer en génère plusieurs)
|
||||||
|
for (let j = i + 1; j < Math.min(i + 8, rows.length); j++) {
|
||||||
|
const nextTd = rows[j].querySelector('td');
|
||||||
|
if (!nextTd) continue;
|
||||||
|
|
||||||
|
const links = nextTd.querySelectorAll('a');
|
||||||
|
if (links.length >= 2) {
|
||||||
|
btn1Text = links[0].textContent?.trim() || btn1Text;
|
||||||
|
btn1Url = links[0].getAttribute("href") || btn1Url;
|
||||||
|
const path1 = links[0].closest('td');
|
||||||
|
btn1Color = path1?.getAttribute("bgcolor") || btn1Color;
|
||||||
|
|
||||||
|
btn2Text = links[1].textContent?.trim() || btn2Text;
|
||||||
|
btn2Url = links[1].getAttribute("href") || btn2Url;
|
||||||
|
const path2 = links[1].closest('td');
|
||||||
|
btn2Color = path2?.getAttribute("bgcolor") || btn2Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const footerImg = nextTd.querySelector('img');
|
||||||
|
if (footerImg) {
|
||||||
|
logoSrc = footerImg.getAttribute("src") || logoSrc;
|
||||||
|
logoWidth = footerImg.getAttribute("width") || logoWidth;
|
||||||
|
|
||||||
|
const sloganDiv = nextTd.querySelector('div');
|
||||||
|
if (sloganDiv) {
|
||||||
|
slogan = sloganDiv.textContent?.trim() || slogan;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addrDiv = nextTd.querySelector('div[style*="color: #333"]');
|
||||||
|
if (addrDiv) {
|
||||||
|
address = addrDiv.innerHTML.replace(/<br>/g, "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bgCheck = nextTd.getAttribute("bgcolor") || nextTd.style.backgroundColor;
|
||||||
|
if (bgCheck && (bgCheck.includes("f9f9f9") || bgCheck.includes("249"))) {
|
||||||
|
legalText = nextTd.textContent?.trim() || legalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blocksList.push({
|
||||||
|
id: `imported-footer-${Date.now()}-${i}`,
|
||||||
|
type: 'footer',
|
||||||
|
titleTop,
|
||||||
|
btn1Text,
|
||||||
|
btn1Url,
|
||||||
|
btn1Color,
|
||||||
|
btn2Text,
|
||||||
|
btn2Url,
|
||||||
|
btn2Color,
|
||||||
|
logoSrc,
|
||||||
|
logoWidth,
|
||||||
|
slogan,
|
||||||
|
address,
|
||||||
|
legalText,
|
||||||
|
bgColor: "#ffffff"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sauter les lignes associées au footer déjà traitées
|
||||||
|
i += 5;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// I. Bloc Texte (Paragraphes HTML standard ou WYSIWYG)
|
||||||
|
if (td.style.color || td.className.includes("wysiwyg") || td.innerHTML.includes("<p>")) {
|
||||||
|
blocksList.push({
|
||||||
|
id: `imported-text-${Date.now()}-${i}`,
|
||||||
|
type: 'text',
|
||||||
|
content: td.innerHTML.trim(),
|
||||||
|
bgColor,
|
||||||
|
textColor: td.style.color || '#4a4a4a',
|
||||||
|
padding,
|
||||||
|
align,
|
||||||
|
fontSize: td.style.fontSize?.replace("px", "") || "16",
|
||||||
|
lineHeight: td.style.lineHeight || "1.5",
|
||||||
|
fontFamily: td.style.fontFamily || 'Helvetica, Arial, sans-serif'
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blocksList.length > 0) {
|
||||||
|
parsedData = {
|
||||||
|
blocks: blocksList,
|
||||||
|
globalBg: bodyBg
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Échec parsing DOM fallback", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedData && parsedData.blocks && Array.isArray(parsedData.blocks)) {
|
||||||
|
setBlocks(parsedData.blocks);
|
||||||
|
if (parsedData.globalBg) {
|
||||||
|
setGlobalBg(parsedData.globalBg);
|
||||||
|
}
|
||||||
|
setSelectedBlockId(null);
|
||||||
|
alert("Projet importé avec succès ! Les structures et contenus HTML ont été restaurés.");
|
||||||
|
} else {
|
||||||
|
alert("Impossible de lire la structure de ce fichier HTML comme un projet compatible.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
e.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full bg-gray-100 text-sm text-slate-800 font-sans overflow-hidden">
|
<div className="flex h-screen w-full bg-gray-100 text-sm text-slate-800 font-sans overflow-hidden">
|
||||||
<style>{`@import url('https://fonts.googleapis.com/css2?family=Luckiest+Guy&family=Open+Sans:wght@400;700&family=Roboto:wght@400;700&display=swap');`}</style>
|
<style>{`@import url('https://fonts.googleapis.com/css2?family=Luckiest+Guy&family=Open+Sans:wght@400;700&family=Roboto:wght@400;700&display=swap');`}</style>
|
||||||
@@ -666,6 +1041,12 @@ p { margin-top: 0; margin-bottom: 10px; }
|
|||||||
<Save size={14} /> Sauvegarder Projet
|
<Save size={14} /> Sauvegarder Projet
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip text="Importer un fichier HTML précédemment exporté pour reprendre l'édition.">
|
||||||
|
<label className="w-full flex items-center justify-center gap-1 bg-white border border-orange-200 text-orange-700 hover:bg-orange-100 py-2 rounded text-xs font-medium cursor-pointer">
|
||||||
|
<Upload size={14} /> Importer HTML
|
||||||
|
<input type="file" accept=".html" onChange={importHTML} className="hidden" />
|
||||||
|
</label>
|
||||||
|
</Tooltip>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Tooltip text="Remet à zéro le contenu (titre, texte, liens) mais garde votre en-tête et pied de page personnalisés." className="flex-1">
|
<Tooltip text="Remet à zéro le contenu (titre, texte, liens) mais garde votre en-tête et pied de page personnalisés." className="flex-1">
|
||||||
<button onClick={resetBody} className="w-full flex items-center justify-center gap-1 bg-white border border-yellow-500 text-yellow-700 hover:bg-yellow-50 py-2 rounded text-xs font-medium">
|
<button onClick={resetBody} className="w-full flex items-center justify-center gap-1 bg-white border border-yellow-500 text-yellow-700 hover:bg-yellow-50 py-2 rounded text-xs font-medium">
|
||||||
@@ -738,7 +1119,21 @@ p { margin-top: 0; margin-bottom: 10px; }
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 border-t border-gray-200 bg-slate-50">
|
<div className="p-4 border-t border-gray-200 bg-slate-50 space-y-3">
|
||||||
|
<label className="flex gap-2 cursor-pointer text-xs font-medium text-slate-700 items-start">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={includeMetadata}
|
||||||
|
onChange={(e) => setIncludeMetadata(e.target.checked)}
|
||||||
|
className="cursor-pointer mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="block mb-0.5 font-bold">Fichier ré-importable</span>
|
||||||
|
<span className="text-[10px] text-slate-500 font-normal leading-tight block">
|
||||||
|
Ajoute des données invisibles (en commentaire HTML) pour pouvoir ré-ouvrir et modifier cette newsletter plus tard dans le builder.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
<Tooltip text="Générer et télécharger le fichier HTML final prêt à l'envoi.">
|
<Tooltip text="Générer et télécharger le fichier HTML final prêt à l'envoi.">
|
||||||
<button onClick={downloadHTML} disabled={isExporting} className={`w-full flex items-center justify-center gap-2 py-3 px-4 rounded-md font-medium transition-colors shadow-sm ${isExporting ? 'bg-indigo-400 cursor-wait' : 'bg-indigo-600 hover:bg-indigo-700'} text-white`}>
|
<button onClick={downloadHTML} disabled={isExporting} className={`w-full flex items-center justify-center gap-2 py-3 px-4 rounded-md font-medium transition-colors shadow-sm ${isExporting ? 'bg-indigo-400 cursor-wait' : 'bg-indigo-600 hover:bg-indigo-700'} text-white`}>
|
||||||
{isExporting ? <><Loader2 className="animate-spin" size={18} /> Traitement...</> : <><Download size={18} /> Exporter HTML</>}
|
{isExporting ? <><Loader2 className="animate-spin" size={18} /> Traitement...</> : <><Download size={18} /> Exporter HTML</>}
|
||||||
@@ -1107,7 +1502,19 @@ function PreviewBlock({ block }: { block: any }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'text':
|
case 'text':
|
||||||
return <div style={containerStyle} dangerouslySetInnerHTML={{ __html: block.content }} />;
|
return (
|
||||||
|
<div
|
||||||
|
className="wysiwyg"
|
||||||
|
style={{
|
||||||
|
...containerStyle,
|
||||||
|
color: block.textColor,
|
||||||
|
fontSize: `${block.fontSize}px`,
|
||||||
|
lineHeight: block.lineHeight || '1.5',
|
||||||
|
fontFamily: fontFamily
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: block.content }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case 'link':
|
case 'link':
|
||||||
return (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
|
|||||||
@@ -1 +1,24 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
.wysiwyg ul {
|
||||||
|
list-style-type: disc !important;
|
||||||
|
padding-left: 1.5rem !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wysiwyg ol {
|
||||||
|
list-style-type: decimal !important;
|
||||||
|
padding-left: 1.5rem !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wysiwyg li {
|
||||||
|
margin-bottom: 0.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wysiwyg p {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user