maj
pop up aide header footer bloqué upload photo etc....
This commit is contained in:
426
src/App.tsx
426
src/App.tsx
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { 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 } 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, Info } from 'lucide-react';
|
||||||
|
|
||||||
// --- CONFIGURATION & CONSTANTES ---
|
// --- CONFIGURATION & CONSTANTES ---
|
||||||
const DEFAULT_WIDTH = "600";
|
const DEFAULT_WIDTH = "600";
|
||||||
@@ -18,9 +18,44 @@ const FONT_OPTIONS = {
|
|||||||
interface Block {
|
interface Block {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
[key: string]: any; // Permet d'accéder à n'importe quelle propriété dynamiquement
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- COMPOSANT TOOLTIP (AIDE AU SURVOL) ---
|
||||||
|
const Tooltip = ({ text, children }: { text: string; children: React.ReactNode }) => {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
// Démarre le timer de 2 secondes
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
setShow(true);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
// Annule le timer si la souris part avant les 2 secondes
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
setShow(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center justify-center w-full" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||||
|
{children}
|
||||||
|
{show && (
|
||||||
|
<div className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 px-3 py-2 bg-slate-800 text-white text-xs rounded shadow-lg z-50 w-max max-w-[200px] text-center pointer-events-none animate-in fade-in zoom-in duration-200">
|
||||||
|
{text}
|
||||||
|
{/* Petite flèche vers le bas */}
|
||||||
|
<div className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-slate-800"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Modèles de blocs par défaut
|
// Modèles de blocs par défaut
|
||||||
const BLOCK_TEMPLATES: Record<string, any> = {
|
const BLOCK_TEMPLATES: Record<string, any> = {
|
||||||
header: {
|
header: {
|
||||||
@@ -29,7 +64,7 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
|||||||
content: "BULLETIN MENSUEL\nD'INFORMATIONS SYNDiCALES",
|
content: "BULLETIN MENSUEL\nD'INFORMATIONS SYNDiCALES",
|
||||||
subtitle: 'AUTOMNE 2025',
|
subtitle: 'AUTOMNE 2025',
|
||||||
bgColor: '#eeeeee',
|
bgColor: '#eeeeee',
|
||||||
backgroundImage: '', // Nouveau champ image de fond
|
backgroundImage: '',
|
||||||
textColor: PURPLE_COLOR,
|
textColor: PURPLE_COLOR,
|
||||||
subtitleColor: ORANGE_COLOR,
|
subtitleColor: ORANGE_COLOR,
|
||||||
padding: '30',
|
padding: '30',
|
||||||
@@ -61,7 +96,8 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
|||||||
text: {
|
text: {
|
||||||
id: null,
|
id: null,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: 'Les 10 et 18 septembre ainsi que le 2 octobre ont rappelé que la colère ne s’était pas éteinte pendant l’été. Au-delà des chiffres de grève, la dégradation des conditions de travail des personnels de la DGFiP s’accentue.',
|
// On utilise du HTML par défaut maintenant pour le RTE
|
||||||
|
content: '<p>Les 10 et 18 septembre ainsi que le 2 octobre ont rappelé que la colère ne s’était pas éteinte pendant l’été.</p><p>Au-delà des chiffres de grève, la dégradation des conditions de travail des personnels de la DGFiP s’accentue.</p>',
|
||||||
bgColor: '#ffffff',
|
bgColor: '#ffffff',
|
||||||
textColor: '#4a4a4a',
|
textColor: '#4a4a4a',
|
||||||
padding: '20',
|
padding: '20',
|
||||||
@@ -132,28 +168,178 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
|||||||
btn2Url: 'https://adherer.solidairesfinancespubliques.org/',
|
btn2Url: 'https://adherer.solidairesfinancespubliques.org/',
|
||||||
btn2Color: '#22c55e',
|
btn2Color: '#22c55e',
|
||||||
logoSrc: 'https://solidairesfinancespubliques.org/images/logo/pastille-122.png',
|
logoSrc: 'https://solidairesfinancespubliques.org/images/logo/pastille-122.png',
|
||||||
|
logoWidth: '80', // Nouvelle propriété pour la taille du logo
|
||||||
slogan: 'Solidaires Finances Publiques est la première force syndicale de la DGFiP',
|
slogan: 'Solidaires Finances Publiques est la première force syndicale de la DGFiP',
|
||||||
address: 'Solidaires Finances Publiques\nBoite 24\n80 rue de Montreuil\n75011 PARIS\nTél. 01.89.16.48.49\nhttps://solidairesfinancespubliques.org\ncontact@solidairesfinancespubliques.org',
|
address: 'Solidaires Finances Publiques\nSection DG et PR\n\n139 rue de Bercy\nBâtiment VAUBAN pièce 0060 Sud 1\n75012 PARIS\n\nTél. 01 53 18 60 36 - 01 53 18 60 14 - 01 53 18 41 47\n\nhttps://sections.solidairesfinancespubliques.info/b38/\nsolidairesfinancespubliques.servicescentraux@dgfip.finances.gouv.fr',
|
||||||
legalText: 'Vous êtes destinataire de ce message d\'origine syndicale conformément aux dispositions de l\'article 8 de l\'arrêté du 4 novembre 2014 relatif aux conditions générales d\'utilisation par les organisations syndicales. Vous pouvez vous désabonner en répondant "Désabonnement".',
|
legalText: 'Vous êtes destinataire de ce message d\'origine syndicale conformément aux dispositions de l\'article 8 de l\'arrêté du 4 novembre 2014 relatif aux conditions générales d\'utilisation par les organisations syndicales. Vous pouvez vous désabonner en répondant "Désabonnement".',
|
||||||
bgColor: '#ffffff',
|
bgColor: '#ffffff',
|
||||||
fontFamily: FONT_OPTIONS.sans.value
|
fontFamily: FONT_OPTIONS.sans.value
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- COMPOSANT MINI RTE (Rich Text Editor) ---
|
||||||
|
const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string) => void }) => {
|
||||||
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Fonction pour exécuter les commandes d'édition
|
||||||
|
const execCmd = (command: string, value: string | undefined = undefined) => {
|
||||||
|
document.execCommand(command, false, value);
|
||||||
|
if (editorRef.current) {
|
||||||
|
onChange(editorRef.current.innerHTML);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Synchronisation initiale et lors des changements externes importants
|
||||||
|
useEffect(() => {
|
||||||
|
if (editorRef.current && editorRef.current.innerHTML !== value) {
|
||||||
|
// Petite protection pour ne pas perdre le focus/curseur si le contenu est similaire
|
||||||
|
// Dans une vraie app prod, on utiliserait une lib comme Slate ou TipTap
|
||||||
|
if (document.activeElement !== editorRef.current) {
|
||||||
|
editorRef.current.innerHTML = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-gray-300 rounded overflow-hidden bg-white">
|
||||||
|
{/* Barre d'outils */}
|
||||||
|
<div className="flex items-center gap-1 p-1 border-b border-gray-200 bg-gray-50 flex-wrap">
|
||||||
|
<RteButton onClick={() => execCmd('bold')} icon={<Bold size={14} />} title="Mettre en gras" />
|
||||||
|
<RteButton onClick={() => execCmd('italic')} icon={<Italic size={14} />} title="Mettre en italique" />
|
||||||
|
<RteButton onClick={() => execCmd('underline')} icon={<Underline size={14} />} title="Souligner" />
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Zone d'édition */}
|
||||||
|
<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)}
|
||||||
|
style={{ fontFamily: 'Helvetica, Arial, sans-serif' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bouton RTE avec Tooltip intégré via title
|
||||||
|
const RteButton = ({ onClick, icon, title }: { onClick: () => void, icon: any, title: string }) => (
|
||||||
|
<Tooltip text={title}>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.preventDefault(); onClick(); }}
|
||||||
|
className="p-1.5 hover:bg-gray-200 rounded text-gray-700"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
export default function NewsletterBuilder() {
|
export default function NewsletterBuilder() {
|
||||||
const [blocks, setBlocks] = useState<Block[]>([
|
const [blocks, setBlocks] = useState<Block[]>([]); // Initialisé vide pour l'effet de chargement
|
||||||
|
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
||||||
|
const [globalBg, setGlobalBg] = useState('#f3f4f6');
|
||||||
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
|
|
||||||
|
// Vérification de la présence des blocs uniques
|
||||||
|
const hasHeader = blocks.some(b => b.type === 'header');
|
||||||
|
const hasFooter = blocks.some(b => b.type === 'footer');
|
||||||
|
|
||||||
|
// --- CHARGEMENT & SAUVEGARDE (LOCAL STORAGE) ---
|
||||||
|
|
||||||
|
// Charger au démarrage
|
||||||
|
useEffect(() => {
|
||||||
|
const savedBlocks = localStorage.getItem('newsletter_blocks');
|
||||||
|
const savedBg = localStorage.getItem('newsletter_bg');
|
||||||
|
|
||||||
|
if (savedBlocks) {
|
||||||
|
try {
|
||||||
|
setBlocks(JSON.parse(savedBlocks));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Erreur lecture sauvegarde", e);
|
||||||
|
loadDefaults();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedBg) setGlobalBg(savedBg);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadDefaults = () => {
|
||||||
|
setBlocks([
|
||||||
{ ...BLOCK_TEMPLATES.header, id: 'init-1' },
|
{ ...BLOCK_TEMPLATES.header, id: 'init-1' },
|
||||||
{ ...BLOCK_TEMPLATES.title, id: 'init-title' },
|
{ ...BLOCK_TEMPLATES.title, id: 'init-title' },
|
||||||
{ ...BLOCK_TEMPLATES.text, id: 'init-2' },
|
{ ...BLOCK_TEMPLATES.text, id: 'init-2' },
|
||||||
{ ...BLOCK_TEMPLATES.link, id: 'init-link' }
|
{ ...BLOCK_TEMPLATES.link, id: 'init-link' }
|
||||||
]);
|
]);
|
||||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
};
|
||||||
const [globalBg, setGlobalBg] = useState('#f3f4f6');
|
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const saveProject = () => {
|
||||||
|
localStorage.setItem('newsletter_blocks', JSON.stringify(blocks));
|
||||||
|
localStorage.setItem('newsletter_bg', globalBg);
|
||||||
|
alert("Projet sauvegardé avec succès dans le navigateur !");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Réinitialisation COMPLETE (Usine)
|
||||||
|
const resetProject = () => {
|
||||||
|
if (confirm("Attention : Cela va effacer votre sauvegarde actuelle et remettre les contenus par défaut (y compris l'en-tête et le pied de page). Continuer ?")) {
|
||||||
|
localStorage.removeItem('newsletter_blocks');
|
||||||
|
localStorage.removeItem('newsletter_bg');
|
||||||
|
loadDefaults();
|
||||||
|
setGlobalBg('#f3f4f6');
|
||||||
|
setSelectedBlockId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Réinitialisation CORPS UNIQUEMENT (Garde Header & Footer)
|
||||||
|
const resetBody = () => {
|
||||||
|
if (confirm("Voulez-vous réinitialiser le corps du message ?\n\nVotre EN-TÊTE et votre PIED DE PAGE actuels seront CONSERVÉS.\nSeul le contenu central sera remplacé par le modèle par défaut.")) {
|
||||||
|
// 1. Trouver les blocs existants à conserver
|
||||||
|
const currentHeader = blocks.find(b => b.type === 'header');
|
||||||
|
const currentFooter = blocks.find(b => b.type === 'footer');
|
||||||
|
|
||||||
|
// 2. Créer les blocs du corps par défaut
|
||||||
|
const defaultBody = [
|
||||||
|
{ ...BLOCK_TEMPLATES.title, id: `reset-title-${Date.now()}` },
|
||||||
|
{ ...BLOCK_TEMPLATES.text, id: `reset-text-${Date.now()}` },
|
||||||
|
{ ...BLOCK_TEMPLATES.link, id: `reset-link-${Date.now()}` }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 3. Reconstruire la liste
|
||||||
|
const newBlocks = [];
|
||||||
|
|
||||||
|
// Ajouter Header (actuel ou nouveau si absent)
|
||||||
|
if (currentHeader) {
|
||||||
|
newBlocks.push(currentHeader);
|
||||||
|
} else {
|
||||||
|
newBlocks.push({ ...BLOCK_TEMPLATES.header, id: `new-header-${Date.now()}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter Corps
|
||||||
|
newBlocks.push(...defaultBody);
|
||||||
|
|
||||||
|
// Ajouter Footer (actuel s'il existe)
|
||||||
|
if (currentFooter) {
|
||||||
|
newBlocks.push(currentFooter);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlocks(newBlocks);
|
||||||
|
setSelectedBlockId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- LOGIQUE D'ÉDITION ---
|
// --- LOGIQUE D'ÉDITION ---
|
||||||
|
|
||||||
const addBlock = (type: string) => {
|
const addBlock = (type: string) => {
|
||||||
|
// Si header ou footer déjà présent, on n'ajoute pas (sécurité supplémentaire)
|
||||||
|
if (type === 'header' && hasHeader) return;
|
||||||
|
if (type === 'footer' && hasFooter) return;
|
||||||
|
|
||||||
const template = BLOCK_TEMPLATES[type];
|
const template = BLOCK_TEMPLATES[type];
|
||||||
const newBlock = JSON.parse(JSON.stringify(template));
|
const newBlock = JSON.parse(JSON.stringify(template));
|
||||||
newBlock.id = `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
newBlock.id = `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
@@ -196,6 +382,7 @@ export default function NewsletterBuilder() {
|
|||||||
|
|
||||||
const convertImageToBase64 = async (url: string) => {
|
const convertImageToBase64 = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
|
if (url.startsWith('data:')) return url; // Déjà en base64
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error('Network response');
|
if (!response.ok) throw new Error('Network response');
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
@@ -233,6 +420,11 @@ export default function NewsletterBuilder() {
|
|||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// FACTEUR D'ÉCHELLE POUR HAUTE DÉFINITION (Retina/Print)
|
||||||
|
// On multiplie par 3 pour avoir une image 3x plus grande que nécessaire
|
||||||
|
// Le navigateur la réduira, ce qui créera un rendu très net.
|
||||||
|
const scale = 3;
|
||||||
|
|
||||||
const padding = parseInt(block.padding) || 0;
|
const padding = parseInt(block.padding) || 0;
|
||||||
const fontSize = parseInt(block.fontSize) || 24;
|
const fontSize = parseInt(block.fontSize) || 24;
|
||||||
const subFontSize = parseInt(block.subtitleFontSize) || 20;
|
const subFontSize = parseInt(block.subtitleFontSize) || 20;
|
||||||
@@ -247,8 +439,16 @@ export default function NewsletterBuilder() {
|
|||||||
const contentHeight = titleHeight + subtitleHeight;
|
const contentHeight = titleHeight + subtitleHeight;
|
||||||
const canvasHeight = contentHeight + (padding * 2);
|
const canvasHeight = contentHeight + (padding * 2);
|
||||||
|
|
||||||
canvas.width = width;
|
// Définition de la taille physique du canvas (x3)
|
||||||
canvas.height = canvasHeight;
|
canvas.width = width * scale;
|
||||||
|
canvas.height = canvasHeight * scale;
|
||||||
|
|
||||||
|
// On scale le contexte pour continuer à dessiner avec des coordonnées logiques
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
|
// Amélioration de la qualité de rendu
|
||||||
|
ctx.imageSmoothingEnabled = true;
|
||||||
|
ctx.imageSmoothingQuality = 'high';
|
||||||
|
|
||||||
// 1. DESSINER LE FOND
|
// 1. DESSINER LE FOND
|
||||||
ctx.fillStyle = block.bgColor;
|
ctx.fillStyle = block.bgColor;
|
||||||
@@ -274,10 +474,6 @@ export default function NewsletterBuilder() {
|
|||||||
offsetY = (canvasHeight - drawHeight) / 2;
|
offsetY = (canvasHeight - drawHeight) / 2;
|
||||||
}
|
}
|
||||||
ctx.drawImage(bgImageObj, offsetX, offsetY, drawWidth, drawHeight);
|
ctx.drawImage(bgImageObj, offsetX, offsetY, drawWidth, drawHeight);
|
||||||
|
|
||||||
// Optionnel : Ajouter un overlay semi-transparent pour la lisibilité
|
|
||||||
// ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
|
||||||
// ctx.fillRect(0, 0, width, canvasHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanFontFamily = block.fontFamily.replace(/'/g, "").replace(/"/g, "");
|
const cleanFontFamily = block.fontFamily.replace(/'/g, "").replace(/"/g, "");
|
||||||
@@ -293,24 +489,7 @@ export default function NewsletterBuilder() {
|
|||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. DESSINER LE TEXTE
|
// 3. DESSINER LA DÉCORATION (AVANT LE TEXTE pour qu'elle soit derrière)
|
||||||
ctx.font = `${fontSize}px ${cleanFontFamily}`;
|
|
||||||
ctx.fillStyle = block.textColor;
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
|
|
||||||
lines.forEach((line: string, i: number) => {
|
|
||||||
const y = padding + (i * fontSize * lineHeightRatio);
|
|
||||||
ctx.fillText(line, x, y);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (block.subtitle) {
|
|
||||||
ctx.font = `${subFontSize}px ${cleanFontFamily}`;
|
|
||||||
ctx.fillStyle = block.subtitleColor || ORANGE_COLOR;
|
|
||||||
const subY = padding + titleHeight + gap;
|
|
||||||
ctx.fillText(block.subtitle, x, subY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. DESSINER LA DÉCORATION
|
|
||||||
if (block.decoration && block.decoration.enabled) {
|
if (block.decoration && block.decoration.enabled) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
const decSize = parseInt(block.decoration.size) || 50;
|
const decSize = parseInt(block.decoration.size) || 50;
|
||||||
@@ -331,7 +510,25 @@ export default function NewsletterBuilder() {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(canvas.toDataURL('image/png'));
|
// 4. DESSINER LE TEXTE (PAR DESSUS LA DÉCORATION)
|
||||||
|
ctx.font = `${fontSize}px ${cleanFontFamily}`;
|
||||||
|
ctx.fillStyle = block.textColor;
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
|
||||||
|
lines.forEach((line: string, i: number) => {
|
||||||
|
const y = padding + (i * fontSize * lineHeightRatio);
|
||||||
|
ctx.fillText(line, x, y);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (block.subtitle) {
|
||||||
|
ctx.font = `${subFontSize}px ${cleanFontFamily}`;
|
||||||
|
ctx.fillStyle = block.subtitleColor || ORANGE_COLOR;
|
||||||
|
const subY = padding + titleHeight + gap;
|
||||||
|
ctx.fillText(block.subtitle, x, subY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion en PNG haute qualité
|
||||||
|
resolve(canvas.toDataURL('image/png', 1.0));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -369,7 +566,7 @@ export default function NewsletterBuilder() {
|
|||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td align="${block.align}" style="background-color: ${block.bgColor}; padding: ${block.padding}px; color: ${block.textColor}; font-size: ${block.fontSize}px; line-height: ${block.lineHeight}; ${commonStyle}">
|
<td align="${block.align}" style="background-color: ${block.bgColor}; padding: ${block.padding}px; color: ${block.textColor}; font-size: ${block.fontSize}px; line-height: ${block.lineHeight}; ${commonStyle}">
|
||||||
${block.content.replace(/\n/g, '<br>')}
|
${block.content}
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
|
||||||
@@ -412,7 +609,7 @@ export default function NewsletterBuilder() {
|
|||||||
</td></tr>
|
</td></tr>
|
||||||
<tr><td style="padding: 0 10px;"><div style="border-bottom: 1px dotted ${ORANGE_COLOR}; height: 1px; width: 100%;"></div></td></tr>
|
<tr><td style="padding: 0 10px;"><div style="border-bottom: 1px dotted ${ORANGE_COLOR}; height: 1px; width: 100%;"></div></td></tr>
|
||||||
<tr><td align="center" style="background-color: #ffffff; padding: 20px; ${commonStyle}">
|
<tr><td align="center" style="background-color: #ffffff; padding: 20px; ${commonStyle}">
|
||||||
<img src="${block.logoSrc}" alt="Logo" width="80" height="80" style="display: block; border: 0; border-radius: 50%; margin-bottom: 10px;" />
|
<img src="${block.logoSrc}" alt="Logo" width="${block.logoWidth || 80}" style="display: block; border: 0; margin-bottom: 10px; max-width: 100%; height: auto;" />
|
||||||
<div style="font-style: italic; font-size: 14px; margin-bottom: 15px; font-weight: bold;">${block.slogan}</div>
|
<div style="font-style: italic; font-size: 14px; margin-bottom: 15px; font-weight: bold;">${block.slogan}</div>
|
||||||
<div style="font-size: 14px; line-height: 1.4; color: #333;">${block.address.replace(/\n/g, '<br>')}</div>
|
<div style="font-size: 14px; line-height: 1.4; color: #333;">${block.address.replace(/\n/g, '<br>')}</div>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
@@ -428,7 +625,15 @@ export default function NewsletterBuilder() {
|
|||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Newsletter</title>
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Newsletter</title>
|
||||||
<style>body { margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; } table { border-collapse: collapse !important; } body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }</style>
|
<style>
|
||||||
|
body { margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||||
|
img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
|
||||||
|
table { border-collapse: collapse !important; }
|
||||||
|
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }
|
||||||
|
ul, ol { margin-top: 0; margin-bottom: 10px; padding-left: 20px; }
|
||||||
|
li { margin-bottom: 5px; }
|
||||||
|
p { margin-top: 0; margin-bottom: 10px; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; padding: 0; background-color: ${globalBg};">
|
<body style="margin: 0; padding: 0; background-color: ${globalBg};">
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%"><tr><td align="center" bgcolor="${globalBg}" style="padding: 20px 0;">
|
<table border="0" cellpadding="0" cellspacing="0" width="100%"><tr><td align="center" bgcolor="${globalBg}" style="padding: 20px 0;">
|
||||||
@@ -450,7 +655,6 @@ export default function NewsletterBuilder() {
|
|||||||
const base64Logo = await convertImageToBase64(block.logoSrc);
|
const base64Logo = await convertImageToBase64(block.logoSrc);
|
||||||
return { ...block, logoSrc: base64Logo };
|
return { ...block, logoSrc: base64Logo };
|
||||||
} else if (block.type === 'header') {
|
} else if (block.type === 'header') {
|
||||||
// Cette fonction gère maintenant l'image de fond et la dessine dans le canvas
|
|
||||||
const base64Header = await createHeaderImage(block);
|
const base64Header = await createHeaderImage(block);
|
||||||
return { type: 'image', src: base64Header, alt: block.content.replace(/\n/g, ' '), width: '100%', align: 'center', padding: 0, bgColor: block.bgColor, isHeaderResult: true };
|
return { type: 'image', src: base64Header, alt: block.content.replace(/\n/g, ' '), width: '100%', align: 'center', padding: 0, bgColor: block.bgColor, isHeaderResult: true };
|
||||||
}
|
}
|
||||||
@@ -482,18 +686,60 @@ export default function NewsletterBuilder() {
|
|||||||
<p className="text-xs text-slate-500 mt-1">Version Bulletin Mensuel</p>
|
<p className="text-xs text-slate-500 mt-1">Version Bulletin Mensuel</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* SECTION GESTION PROJET */}
|
||||||
|
<div className="p-4 border-b border-gray-200 bg-orange-50">
|
||||||
|
<h2 className="text-xs font-semibold text-orange-700 uppercase tracking-wider mb-3">Gestion Projet</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Tooltip text="Enregistre votre travail actuel dans le navigateur pour le retrouver plus tard.">
|
||||||
|
<button onClick={saveProject} 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">
|
||||||
|
<Save size={14} /> Sauvegarder Projet
|
||||||
|
</button>
|
||||||
|
</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.">
|
||||||
|
<button onClick={resetBody} className="flex-1 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">
|
||||||
|
<FileX size={14} /> Reset Corps
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Efface tout et restaure la configuration par défaut du bulletin.">
|
||||||
|
<button onClick={resetProject} className="flex-1 flex items-center justify-center gap-1 bg-white border border-red-200 text-red-600 hover:bg-red-50 py-2 rounded text-xs font-medium">
|
||||||
|
<RotateCcw size={14} /> Reset Tout
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border-gray-200">
|
||||||
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Ajouter un bloc</h2>
|
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Ajouter un bloc</h2>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<BlockButton icon={<Layout size={16} />} label="En-tête" onClick={() => addBlock('header')} />
|
<Tooltip text="Ajoute le bandeau supérieur avec le titre et le sous-titre du bulletin.">
|
||||||
|
<BlockButton icon={<Layout size={16} />} label="En-tête" onClick={() => addBlock('header')} disabled={hasHeader} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Ajoute une ligne horizontale pour séparer les sections.">
|
||||||
<BlockButton icon={<Minus size={16} />} label="Séparateur" onClick={() => addBlock('divider')} />
|
<BlockButton icon={<Minus size={16} />} label="Séparateur" onClick={() => addBlock('divider')} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Ajoute un titre de section en violet et majuscules.">
|
||||||
<BlockButton icon={<Heading size={16} />} label="Titre" onClick={() => addBlock('title')} />
|
<BlockButton icon={<Heading size={16} />} label="Titre" onClick={() => addBlock('title')} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Ajoute un paragraphe de texte riche éditable.">
|
||||||
<BlockButton icon={<Type size={16} />} label="Texte" onClick={() => addBlock('text')} />
|
<BlockButton icon={<Type size={16} />} label="Texte" onClick={() => addBlock('text')} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Ajoute un lien hypertexte simple aligné à droite.">
|
||||||
<BlockButton icon={<LinkIcon size={16} />} label="Lien Texte" onClick={() => addBlock('link')} />
|
<BlockButton icon={<LinkIcon size={16} />} label="Lien Texte" onClick={() => addBlock('link')} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Ajoute un bouton d'action coloré (ex: Lire la suite).">
|
||||||
<BlockButton icon={<MousePointer2 size={16} />} label="Bouton" onClick={() => addBlock('button')} />
|
<BlockButton icon={<MousePointer2 size={16} />} label="Bouton" onClick={() => addBlock('button')} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Ajoute une image ou une photo.">
|
||||||
<BlockButton icon={<ImageIcon size={16} />} label="Image" onClick={() => addBlock('image')} />
|
<BlockButton icon={<ImageIcon size={16} />} label="Image" onClick={() => addBlock('image')} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Ajoute un espace vide vertical pour aérer la mise en page.">
|
||||||
<BlockButton icon={<AlignRight size={16} />} label="Espace" onClick={() => addBlock('spacer')} />
|
<BlockButton icon={<AlignRight size={16} />} label="Espace" onClick={() => addBlock('spacer')} />
|
||||||
<BlockButton icon={<Code size={16} />} label="Pied" onClick={() => addBlock('footer')} />
|
</Tooltip>
|
||||||
|
<Tooltip text="Ajoute le pied de page avec logo, adresse et mentions légales.">
|
||||||
|
<BlockButton icon={<Code size={16} />} label="Pied" onClick={() => addBlock('footer')} disabled={hasFooter} />
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -522,9 +768,11 @@ export default function NewsletterBuilder() {
|
|||||||
</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">
|
||||||
|
<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</>}
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -553,9 +801,13 @@ export default function NewsletterBuilder() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlockButton({ icon, label, onClick }: { icon: any, label: string, onClick: () => void }) {
|
function BlockButton({ icon, label, onClick, disabled }: { icon: any, label: string, onClick: () => void, disabled?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<button onClick={onClick} className="flex flex-col items-center justify-center p-3 bg-white border border-gray-200 rounded hover:bg-indigo-50 hover:border-indigo-200 hover:text-indigo-600 transition-all gap-2 shadow-sm">
|
<button
|
||||||
|
onClick={disabled ? undefined : onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`flex flex-col items-center justify-center p-3 bg-white border border-gray-200 rounded transition-all gap-2 shadow-sm w-full ${disabled ? 'opacity-50 cursor-not-allowed bg-gray-100' : 'hover:bg-indigo-50 hover:border-indigo-200 hover:text-indigo-600'}`}
|
||||||
|
>
|
||||||
{icon}
|
{icon}
|
||||||
<span className="text-xs font-medium text-center">{label}</span>
|
<span className="text-xs font-medium text-center">{label}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -565,6 +817,17 @@ function BlockButton({ icon, label, onClick }: { icon: any, label: string, onCli
|
|||||||
function EditPanel({ block, updateBlock, deleteBlock }: { block: any, updateBlock: (id: string, field: string, value: any) => void, deleteBlock: (id: string) => void }) {
|
function EditPanel({ block, updateBlock, deleteBlock }: { block: any, updateBlock: (id: string, field: string, value: any) => void, deleteBlock: (id: string) => void }) {
|
||||||
if (!block) return null;
|
if (!block) return null;
|
||||||
|
|
||||||
|
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>, field: string) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
updateBlock(block.id, field, reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center border-b pb-2">
|
<div className="flex justify-between items-center border-b pb-2">
|
||||||
@@ -573,8 +836,16 @@ function EditPanel({ block, updateBlock, deleteBlock }: { block: any, updateBloc
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* ÉDITION CONTENU (HEADER, TEXTE, LINK, BUTTON, TITLE) */}
|
{/* ÉDITION TEXTE AVEC RTE */}
|
||||||
{(['header', 'text', 'button', 'link', 'title'].includes(block.type)) && (
|
{block.type === 'text' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 mb-1">Contenu</label>
|
||||||
|
<SimpleRTE value={block.content} onChange={(val) => updateBlock(block.id, 'content', val)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ÉDITION CONTENU SIMPLE (HEADER, LINK, BUTTON, TITLE) */}
|
||||||
|
{(['header', 'button', 'link', 'title'].includes(block.type)) && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-500 mb-1">Contenu</label>
|
<label className="block text-xs font-semibold text-slate-500 mb-1">Contenu</label>
|
||||||
@@ -608,14 +879,20 @@ function EditPanel({ block, updateBlock, deleteBlock }: { block: any, updateBloc
|
|||||||
{/* ÉDITION SPÉCIFIQUE HEADER : IMAGE DE FOND */}
|
{/* ÉDITION SPÉCIFIQUE HEADER : IMAGE DE FOND */}
|
||||||
{block.type === 'header' && (
|
{block.type === 'header' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-500 mb-1">Image de fond (URL)</label>
|
<label className="block text-xs font-semibold text-slate-500 mb-1">Image de fond</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={block.backgroundImage || ''}
|
value={block.backgroundImage || ''}
|
||||||
onChange={(e) => updateBlock(block.id, 'backgroundImage', e.target.value)}
|
onChange={(e) => updateBlock(block.id, 'backgroundImage', e.target.value)}
|
||||||
className="w-full p-2 border border-gray-300 rounded text-sm"
|
className="w-full p-2 border border-gray-300 rounded text-sm mb-2"
|
||||||
placeholder="https://..."
|
placeholder="URL image (https://...)"
|
||||||
/>
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="cursor-pointer bg-slate-100 hover:bg-slate-200 text-slate-600 px-3 py-2 rounded text-xs flex items-center gap-1 border border-slate-300">
|
||||||
|
<Upload size={12} /> Téléverser une image
|
||||||
|
<input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(e, 'backgroundImage')} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -681,7 +958,19 @@ function EditPanel({ block, updateBlock, deleteBlock }: { block: any, updateBloc
|
|||||||
<input type="color" value={block.btn2Color} onChange={(e) => updateBlock(block.id, 'btn2Color', e.target.value)} className="w-full h-6 p-0" />
|
<input type="color" value={block.btn2Color} onChange={(e) => updateBlock(block.id, 'btn2Color', e.target.value)} className="w-full h-6 p-0" />
|
||||||
</div>
|
</div>
|
||||||
<input type="text" value={block.btn2Url} onChange={(e) => updateBlock(block.id, 'btn2Url', e.target.value)} className="w-full p-1 border text-xs" />
|
<input type="text" value={block.btn2Url} onChange={(e) => updateBlock(block.id, 'btn2Url', e.target.value)} className="w-full p-1 border text-xs" />
|
||||||
<input type="text" value={block.logoSrc} onChange={(e) => updateBlock(block.id, 'logoSrc', e.target.value)} className="w-full p-2 border border-gray-300 rounded text-sm mt-2" placeholder="Logo URL" />
|
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between gap-2 mb-1">
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 mb-1 mt-2">Logo</label>
|
||||||
|
<input type="number" value={block.logoWidth || 80} onChange={(e) => updateBlock(block.id, 'logoWidth', e.target.value)} className="w-16 p-1 border border-gray-300 rounded text-xs mt-2" placeholder="px" />
|
||||||
|
</div>
|
||||||
|
<input type="text" value={block.logoSrc} onChange={(e) => updateBlock(block.id, 'logoSrc', e.target.value)} className="w-full p-2 border border-gray-300 rounded text-sm mb-1" placeholder="Logo URL" />
|
||||||
|
<label className="cursor-pointer bg-slate-100 hover:bg-slate-200 text-slate-600 px-3 py-2 rounded text-xs flex items-center gap-1 border border-slate-300 w-fit">
|
||||||
|
<Upload size={12} /> Téléverser un logo
|
||||||
|
<input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(e, 'logoSrc')} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="text" value={block.slogan} onChange={(e) => updateBlock(block.id, 'slogan', e.target.value)} className="w-full p-2 border border-gray-300 rounded text-sm font-bold mt-2" />
|
<input type="text" value={block.slogan} onChange={(e) => updateBlock(block.id, 'slogan', e.target.value)} className="w-full p-2 border border-gray-300 rounded text-sm font-bold mt-2" />
|
||||||
<textarea rows={4} className="w-full p-2 border border-gray-300 rounded text-sm mt-2" value={block.address} onChange={(e) => updateBlock(block.id, 'address', e.target.value)} />
|
<textarea rows={4} className="w-full p-2 border border-gray-300 rounded text-sm mt-2" value={block.address} onChange={(e) => updateBlock(block.id, 'address', e.target.value)} />
|
||||||
<textarea rows={4} className="w-full p-2 border border-gray-300 rounded text-xs text-gray-500 mt-2" value={block.legalText} onChange={(e) => updateBlock(block.id, 'legalText', e.target.value)} />
|
<textarea rows={4} className="w-full p-2 border border-gray-300 rounded text-xs text-gray-500 mt-2" value={block.legalText} onChange={(e) => updateBlock(block.id, 'legalText', e.target.value)} />
|
||||||
@@ -689,10 +978,30 @@ function EditPanel({ block, updateBlock, deleteBlock }: { block: any, updateBloc
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* URL COMMUNE */}
|
{/* URL COMMUNE */}
|
||||||
{(block.type === 'image' || block.type === 'button' || block.type === 'link') && (
|
{(block.type === 'button' || block.type === 'link') && (
|
||||||
<div><label className="block text-xs font-semibold text-slate-500 mb-1">Lien / Src</label><input type="text" value={block.url || block.src} onChange={(e) => updateBlock(block.id, block.type === 'image' ? 'src' : 'url', e.target.value)} className="w-full p-2 border border-gray-300 rounded text-sm" /></div>
|
<div><label className="block text-xs font-semibold text-slate-500 mb-1">Lien / Src</label><input type="text" value={block.url || block.src} onChange={(e) => updateBlock(block.id, block.type === 'image' ? 'src' : 'url', e.target.value)} className="w-full p-2 border border-gray-300 rounded text-sm" /></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* UPLOAD IMAGE SPÉCIFIQUE */}
|
||||||
|
{block.type === 'image' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 mb-1">Image</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={block.src}
|
||||||
|
onChange={(e) => updateBlock(block.id, 'src', e.target.value)}
|
||||||
|
className="w-full p-2 border border-gray-300 rounded text-sm mb-2"
|
||||||
|
placeholder="URL image (https://...)"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="cursor-pointer bg-slate-100 hover:bg-slate-200 text-slate-600 px-3 py-2 rounded text-xs flex items-center gap-1 border border-slate-300">
|
||||||
|
<Upload size={12} /> Téléverser une image
|
||||||
|
<input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(e, 'src')} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ALIGNEMENT */}
|
{/* ALIGNEMENT */}
|
||||||
{block.type !== 'spacer' && block.type !== 'footer' && block.type !== 'divider' && (
|
{block.type !== 'spacer' && block.type !== 'footer' && block.type !== 'divider' && (
|
||||||
<div>
|
<div>
|
||||||
@@ -746,7 +1055,13 @@ function PreviewBlock({ block }: { block: any }) {
|
|||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
}}>
|
}}>
|
||||||
<h1 style={{...textStyle, fontWeight: 'normal', lineHeight: '1.1', position: 'relative', zIndex: 1}}>{block.content}</h1>
|
{/* DECORATION (Z-INDEX 0) POUR ÊTRE DERRIÈRE */}
|
||||||
|
{block.decoration?.enabled && (
|
||||||
|
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: `translate(-50%, -50%) translate(${block.decoration.xOffset}px, ${block.decoration.yOffset}px) rotate(${block.decoration.rotation}deg)`, color: block.decoration.color, fontSize: `${block.decoration.size}px`, fontFamily: fontFamily, pointerEvents: 'none', zIndex: 0, lineHeight: 1 }}>{block.decoration.content}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TEXTE (Z-INDEX 10) POUR ÊTRE DEVANT */}
|
||||||
|
<h1 style={{...textStyle, fontWeight: 'normal', lineHeight: '1.1', position: 'relative', zIndex: 10}}>{block.content}</h1>
|
||||||
{block.subtitle && (
|
{block.subtitle && (
|
||||||
<div style={{
|
<div style={{
|
||||||
color: block.subtitleColor || ORANGE_COLOR,
|
color: block.subtitleColor || ORANGE_COLOR,
|
||||||
@@ -754,14 +1069,11 @@ function PreviewBlock({ block }: { block: any }) {
|
|||||||
marginTop: '10px',
|
marginTop: '10px',
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: 1
|
zIndex: 10
|
||||||
}}>
|
}}>
|
||||||
{block.subtitle}
|
{block.subtitle}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{block.decoration?.enabled && (
|
|
||||||
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: `translate(-50%, -50%) translate(${block.decoration.xOffset}px, ${block.decoration.yOffset}px) rotate(${block.decoration.rotation}deg)`, color: block.decoration.color, fontSize: `${block.decoration.size}px`, fontFamily: fontFamily, pointerEvents: 'none', zIndex: 0, lineHeight: 1 }}>{block.decoration.content}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/* RESTAURATION DE LA LIGNE ORANGE */}
|
{/* RESTAURATION DE LA LIGNE ORANGE */}
|
||||||
<div style={{ backgroundColor: ORANGE_COLOR, height: '1px', width: '100%' }}></div>
|
<div style={{ backgroundColor: ORANGE_COLOR, height: '1px', width: '100%' }}></div>
|
||||||
@@ -789,7 +1101,7 @@ function PreviewBlock({ block }: { block: any }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'text':
|
case 'text':
|
||||||
return <div style={containerStyle}><div style={textStyle}>{block.content}</div></div>;
|
return <div style={containerStyle} dangerouslySetInnerHTML={{ __html: block.content }} />;
|
||||||
case 'link':
|
case 'link':
|
||||||
return (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
@@ -808,7 +1120,7 @@ function PreviewBlock({ block }: { block: any }) {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ padding: '0 10px' }}><div style={{ borderBottom: `1px dotted ${ORANGE_COLOR}`, width: '100%', height: '1px' }}></div></div>
|
<div style={{ padding: '0 10px' }}><div style={{ borderBottom: `1px dotted ${ORANGE_COLOR}`, width: '100%', height: '1px' }}></div></div>
|
||||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||||
<img src={block.logoSrc} alt="logo" style={{ width: '80px', height: '80px', borderRadius: '50%', margin: '0 auto 10px auto', display: 'block', objectFit: 'cover' }} />
|
<img src={block.logoSrc} alt="logo" style={{ width: `${block.logoWidth || 80}px`, height: 'auto', display: 'block', margin: '0 auto 10px auto', maxWidth: '100%' }} />
|
||||||
<div style={{ fontWeight: 'bold', fontStyle: 'italic', fontSize: '14px', marginBottom: '10px' }}>{block.slogan}</div>
|
<div style={{ fontWeight: 'bold', fontStyle: 'italic', fontSize: '14px', marginBottom: '10px' }}>{block.slogan}</div>
|
||||||
<div style={{ whiteSpace: 'pre-wrap', fontSize: '14px', lineHeight: '1.4' }}>{block.address}</div>
|
<div style={{ whiteSpace: 'pre-wrap', fontSize: '14px', lineHeight: '1.4' }}>{block.address}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user