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 { 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 ---
|
||||
const DEFAULT_WIDTH = "600";
|
||||
@@ -63,7 +63,7 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
||||
bgColor: '#eeeeee',
|
||||
backgroundImage: '',
|
||||
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,
|
||||
subtitleColor: ORANGE_COLOR,
|
||||
padding: '30',
|
||||
@@ -179,6 +179,7 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
||||
// --- COMPOSANT MINI RTE (Rich Text Editor) ---
|
||||
const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string) => void }) => {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const savedSelectionRef = useRef<Range | null>(null);
|
||||
|
||||
const execCmd = (command: string, value: string | undefined = undefined) => {
|
||||
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(() => {
|
||||
if (editorRef.current && editorRef.current.innerHTML !== value) {
|
||||
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>
|
||||
<RteButton onClick={() => execCmd('insertUnorderedList')} icon={<List size={14} />} title="Liste à puces" />
|
||||
<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
|
||||
ref={editorRef}
|
||||
contentEditable
|
||||
className="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)}
|
||||
onBlur={(e) => onChange(e.currentTarget.innerHTML)}
|
||||
className="wysiwyg p-3 min-h-[150px] text-sm focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-200"
|
||||
onInput={(e) => { saveSelection(); onChange(e.currentTarget.innerHTML); }}
|
||||
onBlur={(e) => { saveSelection(); onChange(e.currentTarget.innerHTML); }}
|
||||
onMouseUp={saveSelection}
|
||||
onKeyUp={saveSelection}
|
||||
style={{ fontFamily: 'Helvetica, Arial, sans-serif' }}
|
||||
/>
|
||||
</div>
|
||||
@@ -236,6 +276,7 @@ export default function NewsletterBuilder() {
|
||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
||||
const [globalBg, setGlobalBg] = useState('#f3f4f6');
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [includeMetadata, setIncludeMetadata] = useState(false);
|
||||
|
||||
const hasHeader = blocks.some(b => b.type === 'header');
|
||||
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 fontFamily = block.fontFamily || FONT_OPTIONS.sans.value;
|
||||
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('');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
@@ -605,6 +647,7 @@ p { margin-top: 0; margin-bottom: 10px; }
|
||||
${rows}
|
||||
</table>
|
||||
</td></tr></table>
|
||||
<!-- NEWSLETTER_BUILDER_DATA: ${builderMetadata} -->
|
||||
</body></html>`;
|
||||
};
|
||||
|
||||
@@ -631,7 +674,7 @@ p { margin-top: 0; margin-bottom: 10px; }
|
||||
}
|
||||
return block;
|
||||
}));
|
||||
const html = generateHTML(processedBlocks);
|
||||
const html = generateHTML(processedBlocks, includeMetadata ? blocks : undefined);
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -644,6 +687,338 @@ p { margin-top: 0; margin-bottom: 10px; }
|
||||
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 (
|
||||
<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>
|
||||
@@ -666,6 +1041,12 @@ p { margin-top: 0; margin-bottom: 10px; }
|
||||
<Save size={14} /> Sauvegarder Projet
|
||||
</button>
|
||||
</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">
|
||||
<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">
|
||||
@@ -738,7 +1119,21 @@ p { margin-top: 0; margin-bottom: 10px; }
|
||||
)}
|
||||
</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.">
|
||||
<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</>}
|
||||
@@ -1107,7 +1502,19 @@ function PreviewBlock({ block }: { block: any }) {
|
||||
</div>
|
||||
);
|
||||
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':
|
||||
return (
|
||||
<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