diff --git a/src/App.tsx b/src/App.tsx index a2744ba..2c8c86d 100644 --- a/src/App.tsx +++ b/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 = { 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 = { // --- COMPOSANT MINI RTE (Rich Text Editor) --- const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string) => void }) => { const editorRef = useRef(null); + const savedSelectionRef = useRef(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)
execCmd('insertUnorderedList')} icon={} title="Liste à puces" /> execCmd('insertOrderedList')} icon={} title="Liste numérotée" /> +
+ +
+ + handleColorChange(e.target.value)} + onFocus={saveSelection} + onMouseDown={saveSelection} + className="w-4 h-4 p-0 border-0 cursor-pointer bg-transparent" + /> +
+
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' }} />
@@ -236,6 +276,7 @@ export default function NewsletterBuilder() { const [selectedBlockId, setSelectedBlockId] = useState(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 ` @@ -605,6 +647,7 @@ p { margin-top: 0; margin-bottom: 10px; } ${rows} + `; }; @@ -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) => { + 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(//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(/
/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(/
/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(/
/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("

")) { + 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 (

@@ -666,6 +1041,12 @@ p { margin-top: 0; margin-bottom: 10px; } Sauvegarder Projet + + +
-
+
+
); case 'text': - return
; + return ( +
+ ); case 'link': return (
diff --git a/src/index.css b/src/index.css index a461c50..44f7514 100644 --- a/src/index.css +++ b/src/index.css @@ -1 +1,24 @@ -@import "tailwindcss"; \ No newline at end of file +@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; +} \ No newline at end of file