ajout de l'option base64 pour le header
This commit is contained in:
229
src/App.tsx
229
src/App.tsx
@@ -1,4 +1,4 @@
|
|||||||
import { 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 } from 'lucide-react';
|
||||||
|
|
||||||
// --- CONFIGURATION & CONSTANTES ---
|
// --- CONFIGURATION & CONSTANTES ---
|
||||||
@@ -22,19 +22,17 @@ interface Block {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- COMPOSANT TOOLTIP (AIDE AU SURVOL) ---
|
// --- COMPOSANT TOOLTIP (AIDE AU SURVOL) ---
|
||||||
const Tooltip = ({ text, children }: { text: string; children: React.ReactNode }) => {
|
const Tooltip = ({ text, children, className = "w-full" }: { text: string; children: React.ReactNode; className?: string }) => {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
// Démarre le timer de 2 secondes
|
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
setShow(true);
|
setShow(true);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
// Annule le timer si la souris part avant les 2 secondes
|
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearTimeout(timerRef.current);
|
clearTimeout(timerRef.current);
|
||||||
timerRef.current = null;
|
timerRef.current = null;
|
||||||
@@ -43,12 +41,11 @@ const Tooltip = ({ text, children }: { text: string; children: React.ReactNode }
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center justify-center w-full" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
<div className={`relative flex items-center justify-center ${className}`} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||||
{children}
|
{children}
|
||||||
{show && (
|
{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">
|
<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}
|
{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 className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-slate-800"></div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -65,6 +62,8 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
|||||||
subtitle: 'AUTOMNE 2025',
|
subtitle: 'AUTOMNE 2025',
|
||||||
bgColor: '#eeeeee',
|
bgColor: '#eeeeee',
|
||||||
backgroundImage: '',
|
backgroundImage: '',
|
||||||
|
url: '', // Lien de redirection
|
||||||
|
exportAsImage: true, // 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',
|
||||||
@@ -96,7 +95,6 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
|||||||
text: {
|
text: {
|
||||||
id: null,
|
id: null,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
// 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>',
|
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',
|
||||||
@@ -145,6 +143,7 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
|||||||
id: null,
|
id: null,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
src: 'https://via.placeholder.com/600x300',
|
src: 'https://via.placeholder.com/600x300',
|
||||||
|
url: '', // Ajout possible d'un lien
|
||||||
alt: 'Image de description',
|
alt: 'Image de description',
|
||||||
bgColor: '#ffffff',
|
bgColor: '#ffffff',
|
||||||
padding: '0',
|
padding: '0',
|
||||||
@@ -168,7 +167,7 @@ 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
|
logoWidth: '80',
|
||||||
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\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',
|
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".',
|
||||||
@@ -181,7 +180,6 @@ const BLOCK_TEMPLATES: Record<string, any> = {
|
|||||||
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);
|
||||||
|
|
||||||
// Fonction pour exécuter les commandes d'édition
|
|
||||||
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);
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
@@ -189,11 +187,8 @@ const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Synchronisation initiale et lors des changements externes importants
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorRef.current && editorRef.current.innerHTML !== value) {
|
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) {
|
if (document.activeElement !== editorRef.current) {
|
||||||
editorRef.current.innerHTML = value;
|
editorRef.current.innerHTML = value;
|
||||||
}
|
}
|
||||||
@@ -202,7 +197,6 @@ const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-gray-300 rounded overflow-hidden bg-white">
|
<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">
|
<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('bold')} icon={<Bold size={14} />} title="Mettre en gras" />
|
||||||
<RteButton onClick={() => execCmd('italic')} icon={<Italic size={14} />} title="Mettre en italique" />
|
<RteButton onClick={() => execCmd('italic')} icon={<Italic size={14} />} title="Mettre en italique" />
|
||||||
@@ -212,7 +206,6 @@ const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string)
|
|||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Zone d'édition */}
|
|
||||||
<div
|
<div
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
contentEditable
|
contentEditable
|
||||||
@@ -225,9 +218,8 @@ const SimpleRTE = ({ value, onChange }: { value: string, onChange: (val: string)
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bouton RTE avec Tooltip intégré via title
|
|
||||||
const RteButton = ({ onClick, icon, title }: { onClick: () => void, icon: any, title: string }) => (
|
const RteButton = ({ onClick, icon, title }: { onClick: () => void, icon: any, title: string }) => (
|
||||||
<Tooltip text={title}>
|
<Tooltip text={title} className="w-auto">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.preventDefault(); onClick(); }}
|
onClick={(e) => { e.preventDefault(); onClick(); }}
|
||||||
className="p-1.5 hover:bg-gray-200 rounded text-gray-700"
|
className="p-1.5 hover:bg-gray-200 rounded text-gray-700"
|
||||||
@@ -240,18 +232,18 @@ const RteButton = ({ onClick, icon, title }: { onClick: () => void, icon: any, t
|
|||||||
|
|
||||||
|
|
||||||
export default function NewsletterBuilder() {
|
export default function NewsletterBuilder() {
|
||||||
const [blocks, setBlocks] = useState<Block[]>([]); // Initialisé vide pour l'effet de chargement
|
const [blocks, setBlocks] = useState<Block[]>([]);
|
||||||
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);
|
||||||
|
|
||||||
// Vérification de la présence des blocs uniques
|
|
||||||
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');
|
||||||
|
|
||||||
// --- CHARGEMENT & SAUVEGARDE (LOCAL STORAGE) ---
|
const getSelectedBlock = () => blocks.find(b => b.id === selectedBlockId);
|
||||||
|
const selectedBlock = getSelectedBlock();
|
||||||
|
|
||||||
// Charger au démarrage
|
// --- CHARGEMENT & SAUVEGARDE ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedBlocks = localStorage.getItem('newsletter_blocks');
|
const savedBlocks = localStorage.getItem('newsletter_blocks');
|
||||||
const savedBg = localStorage.getItem('newsletter_bg');
|
const savedBg = localStorage.getItem('newsletter_bg');
|
||||||
@@ -285,9 +277,8 @@ export default function NewsletterBuilder() {
|
|||||||
alert("Projet sauvegardé avec succès dans le navigateur !");
|
alert("Projet sauvegardé avec succès dans le navigateur !");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Réinitialisation COMPLETE (Usine)
|
|
||||||
const resetProject = () => {
|
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 ?")) {
|
if (confirm("Attention : Cela va effacer votre sauvegarde actuelle et remettre les contenus par défaut. Continuer ?")) {
|
||||||
localStorage.removeItem('newsletter_blocks');
|
localStorage.removeItem('newsletter_blocks');
|
||||||
localStorage.removeItem('newsletter_bg');
|
localStorage.removeItem('newsletter_bg');
|
||||||
loadDefaults();
|
loadDefaults();
|
||||||
@@ -296,37 +287,23 @@ export default function NewsletterBuilder() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Réinitialisation CORPS UNIQUEMENT (Garde Header & Footer)
|
|
||||||
const resetBody = () => {
|
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.")) {
|
if (confirm("Voulez-vous réinitialiser le corps du message ?\n\nVotre EN-TÊTE et votre PIED DE PAGE actuels seront CONSERVÉS.")) {
|
||||||
// 1. Trouver les blocs existants à conserver
|
|
||||||
const currentHeader = blocks.find(b => b.type === 'header');
|
const currentHeader = blocks.find(b => b.type === 'header');
|
||||||
const currentFooter = blocks.find(b => b.type === 'footer');
|
const currentFooter = blocks.find(b => b.type === 'footer');
|
||||||
|
|
||||||
// 2. Créer les blocs du corps par défaut
|
|
||||||
const defaultBody = [
|
const defaultBody = [
|
||||||
{ ...BLOCK_TEMPLATES.title, id: `reset-title-${Date.now()}` },
|
{ ...BLOCK_TEMPLATES.title, id: `reset-title-${Date.now()}` },
|
||||||
{ ...BLOCK_TEMPLATES.text, id: `reset-text-${Date.now()}` },
|
{ ...BLOCK_TEMPLATES.text, id: `reset-text-${Date.now()}` },
|
||||||
{ ...BLOCK_TEMPLATES.link, id: `reset-link-${Date.now()}` }
|
{ ...BLOCK_TEMPLATES.link, id: `reset-link-${Date.now()}` }
|
||||||
];
|
];
|
||||||
|
|
||||||
// 3. Reconstruire la liste
|
|
||||||
const newBlocks = [];
|
const newBlocks = [];
|
||||||
|
if (currentHeader) newBlocks.push(currentHeader);
|
||||||
|
else newBlocks.push({ ...BLOCK_TEMPLATES.header, id: `new-header-${Date.now()}` });
|
||||||
|
|
||||||
// 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);
|
newBlocks.push(...defaultBody);
|
||||||
|
if (currentFooter) newBlocks.push(currentFooter);
|
||||||
// Ajouter Footer (actuel s'il existe)
|
|
||||||
if (currentFooter) {
|
|
||||||
newBlocks.push(currentFooter);
|
|
||||||
}
|
|
||||||
|
|
||||||
setBlocks(newBlocks);
|
setBlocks(newBlocks);
|
||||||
setSelectedBlockId(null);
|
setSelectedBlockId(null);
|
||||||
@@ -334,9 +311,7 @@ export default function NewsletterBuilder() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- 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 === 'header' && hasHeader) return;
|
||||||
if (type === 'footer' && hasFooter) return;
|
if (type === 'footer' && hasFooter) return;
|
||||||
|
|
||||||
@@ -376,13 +351,10 @@ export default function NewsletterBuilder() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectedBlock = () => blocks.find(b => b.id === selectedBlockId);
|
|
||||||
|
|
||||||
// --- MOTEUR D'EXPORT ---
|
// --- MOTEUR D'EXPORT ---
|
||||||
|
|
||||||
const convertImageToBase64 = async (url: string) => {
|
const convertImageToBase64 = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
if (url.startsWith('data:')) return url; // Déjà en base64
|
if (url.startsWith('data:')) return url;
|
||||||
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();
|
||||||
@@ -399,7 +371,6 @@ export default function NewsletterBuilder() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createHeaderImage = async (block: any) => {
|
const createHeaderImage = async (block: any) => {
|
||||||
// Si une image de fond est présente, on la charge d'abord
|
|
||||||
let bgImageObj: HTMLImageElement | null = null;
|
let bgImageObj: HTMLImageElement | null = null;
|
||||||
if (block.backgroundImage) {
|
if (block.backgroundImage) {
|
||||||
try {
|
try {
|
||||||
@@ -411,7 +382,7 @@ export default function NewsletterBuilder() {
|
|||||||
else resolve(null);
|
else resolve(null);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load background image for header canvas", e);
|
console.warn("Failed to load background image", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,11 +391,7 @@ 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 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;
|
||||||
@@ -435,39 +402,29 @@ export default function NewsletterBuilder() {
|
|||||||
const lines = block.content.split('\n');
|
const lines = block.content.split('\n');
|
||||||
const titleHeight = lines.length * (fontSize * lineHeightRatio);
|
const titleHeight = lines.length * (fontSize * lineHeightRatio);
|
||||||
const subtitleHeight = block.subtitle ? (subFontSize * lineHeightRatio) + gap : 0;
|
const subtitleHeight = block.subtitle ? (subFontSize * lineHeightRatio) + gap : 0;
|
||||||
|
|
||||||
const contentHeight = titleHeight + subtitleHeight;
|
const contentHeight = titleHeight + subtitleHeight;
|
||||||
const canvasHeight = contentHeight + (padding * 2);
|
const canvasHeight = contentHeight + (padding * 2);
|
||||||
|
|
||||||
// Définition de la taille physique du canvas (x3)
|
|
||||||
canvas.width = width * scale;
|
canvas.width = width * scale;
|
||||||
canvas.height = canvasHeight * scale;
|
canvas.height = canvasHeight * scale;
|
||||||
|
|
||||||
// On scale le contexte pour continuer à dessiner avec des coordonnées logiques
|
|
||||||
ctx.scale(scale, scale);
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
// Amélioration de la qualité de rendu
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
ctx.imageSmoothingEnabled = true;
|
||||||
ctx.imageSmoothingQuality = 'high';
|
ctx.imageSmoothingQuality = 'high';
|
||||||
|
|
||||||
// 1. DESSINER LE FOND
|
|
||||||
ctx.fillStyle = block.bgColor;
|
ctx.fillStyle = block.bgColor;
|
||||||
ctx.fillRect(0, 0, width, canvasHeight);
|
ctx.fillRect(0, 0, width, canvasHeight);
|
||||||
|
|
||||||
// 2. DESSINER L'IMAGE DE FOND (Mode Cover)
|
|
||||||
if (bgImageObj) {
|
if (bgImageObj) {
|
||||||
const imgRatio = bgImageObj.width / bgImageObj.height;
|
const imgRatio = bgImageObj.width / bgImageObj.height;
|
||||||
const canvasRatio = width / canvasHeight;
|
const canvasRatio = width / canvasHeight;
|
||||||
let drawWidth, drawHeight, offsetX, offsetY;
|
let drawWidth, drawHeight, offsetX, offsetY;
|
||||||
|
|
||||||
if (imgRatio > canvasRatio) {
|
if (imgRatio > canvasRatio) {
|
||||||
// Image plus large que le canvas (rogner côtés)
|
|
||||||
drawHeight = canvasHeight;
|
drawHeight = canvasHeight;
|
||||||
drawWidth = bgImageObj.width * (canvasHeight / bgImageObj.height);
|
drawWidth = bgImageObj.width * (canvasHeight / bgImageObj.height);
|
||||||
offsetX = (width - drawWidth) / 2;
|
offsetX = (width - drawWidth) / 2;
|
||||||
offsetY = 0;
|
offsetY = 0;
|
||||||
} else {
|
} else {
|
||||||
// Image plus haute que le canvas (rogner haut/bas)
|
|
||||||
drawWidth = width;
|
drawWidth = width;
|
||||||
drawHeight = bgImageObj.height * (width / bgImageObj.width);
|
drawHeight = bgImageObj.height * (width / bgImageObj.width);
|
||||||
offsetX = 0;
|
offsetX = 0;
|
||||||
@@ -477,7 +434,6 @@ export default function NewsletterBuilder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleanFontFamily = block.fontFamily.replace(/'/g, "").replace(/"/g, "");
|
const cleanFontFamily = block.fontFamily.replace(/'/g, "").replace(/"/g, "");
|
||||||
|
|
||||||
let x = padding;
|
let x = padding;
|
||||||
if (block.align === 'center') {
|
if (block.align === 'center') {
|
||||||
x = width / 2;
|
x = width / 2;
|
||||||
@@ -489,7 +445,6 @@ export default function NewsletterBuilder() {
|
|||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. DESSINER LA DÉCORATION (AVANT LE TEXTE pour qu'elle soit derrière)
|
|
||||||
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;
|
||||||
@@ -510,7 +465,6 @@ export default function NewsletterBuilder() {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. DESSINER LE TEXTE (PAR DESSUS LA DÉCORATION)
|
|
||||||
ctx.font = `${fontSize}px ${cleanFontFamily}`;
|
ctx.font = `${fontSize}px ${cleanFontFamily}`;
|
||||||
ctx.fillStyle = block.textColor;
|
ctx.fillStyle = block.textColor;
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
@@ -527,7 +481,6 @@ export default function NewsletterBuilder() {
|
|||||||
ctx.fillText(block.subtitle, x, subY);
|
ctx.fillText(block.subtitle, x, subY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conversion en PNG haute qualité
|
|
||||||
resolve(canvas.toDataURL('image/png', 1.0));
|
resolve(canvas.toDataURL('image/png', 1.0));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -536,15 +489,24 @@ export default function NewsletterBuilder() {
|
|||||||
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;`;
|
||||||
|
|
||||||
const borderRow = `<tr><td style="background-color: ${ORANGE_COLOR}; height: 1px; font-size: 0; line-height: 0;"> </td></tr>`;
|
const borderRow = `<tr><td style="background-color: ${ORANGE_COLOR}; height: 1px; font-size: 0; line-height: 0;"> </td></tr>`;
|
||||||
|
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case 'header':
|
case 'header':
|
||||||
return `
|
// Ce cas est appelé SEULEMENT si block.exportAsImage est FAUX
|
||||||
<tr><td align="${block.align}" style="background-color: ${block.bgColor}; padding: ${block.padding}px; background-image: url('${block.backgroundImage}'); background-size: cover; background-position: center;">
|
let headerContent = `
|
||||||
<h1 style="margin: 0; color: ${block.textColor}; font-size: ${block.fontSize}px; ${commonStyle} line-height: 1.1;">${block.content.replace(/\n/g, '<br>')}</h1>
|
<h1 style="margin: 0; color: ${block.textColor}; font-size: ${block.fontSize}px; ${commonStyle} line-height: 1.1;">${block.content.replace(/\n/g, '<br>')}</h1>
|
||||||
${block.subtitle ? `<div style="color: ${block.subtitleColor || ORANGE_COLOR}; font-size: ${block.subtitleFontSize || 20}px; margin-top: 10px; ${commonStyle}">${block.subtitle}</div>` : ''}
|
${block.subtitle ? `<div style="color: ${block.subtitleColor || ORANGE_COLOR}; font-size: ${block.subtitleFontSize || 20}px; margin-top: 10px; ${commonStyle}">${block.subtitle}</div>` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// On ajoute le lien de redirection global si présent
|
||||||
|
if (block.url) {
|
||||||
|
headerContent = `<a href="${block.url}" target="_blank" style="text-decoration:none; display:block;">${headerContent}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr><td align="${block.align}" style="background-color: ${block.bgColor}; padding: ${block.padding}px; background-image: url('${block.backgroundImage}'); background-size: cover; background-position: center;">
|
||||||
|
${headerContent}
|
||||||
</td></tr>
|
</td></tr>
|
||||||
${borderRow}`;
|
${borderRow}`;
|
||||||
|
|
||||||
@@ -592,7 +554,9 @@ export default function NewsletterBuilder() {
|
|||||||
</tr>`;
|
</tr>`;
|
||||||
|
|
||||||
case 'image':
|
case 'image':
|
||||||
const imgHtml = `<tr><td align="${block.align}" style="background-color: ${block.bgColor}; padding: ${block.padding}px;"><img src="${block.src}" alt="${block.alt}" width="${block.width === '100%' ? '600' : block.width}" style="display: block; border: 0; max-width: 100%; width: ${block.width}; height: auto;" /></td></tr>`;
|
const innerImg = `<img src="${block.src}" alt="${block.alt}" width="${block.width === '100%' ? '600' : block.width}" style="display: block; border: 0; max-width: 100%; width: ${block.width}; height: auto;" />`;
|
||||||
|
const imgContent = block.url ? `<a href="${block.url}" target="_blank" style="text-decoration:none;">${innerImg}</a>` : innerImg;
|
||||||
|
const imgHtml = `<tr><td align="${block.align}" style="background-color: ${block.bgColor}; padding: ${block.padding}px;">${imgContent}</td></tr>`;
|
||||||
return block.isHeaderResult ? imgHtml + borderRow : imgHtml;
|
return block.isHeaderResult ? imgHtml + borderRow : imgHtml;
|
||||||
|
|
||||||
case 'spacer':
|
case 'spacer':
|
||||||
@@ -655,8 +619,15 @@ p { margin-top: 0; margin-bottom: 10px; }
|
|||||||
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') {
|
||||||
const base64Header = await createHeaderImage(block);
|
// La case dicte le comportement de l'export
|
||||||
return { type: 'image', src: base64Header, alt: block.content.replace(/\n/g, ' '), width: '100%', align: 'center', padding: 0, bgColor: block.bgColor, isHeaderResult: true };
|
if (block.exportAsImage !== false) {
|
||||||
|
const base64Header = await createHeaderImage(block);
|
||||||
|
// On transforme en bloc "image" pour le rendu final
|
||||||
|
return { type: 'image', src: base64Header, alt: block.content.replace(/\n/g, ' '), width: '100%', align: 'center', padding: 0, bgColor: block.bgColor, isHeaderResult: true, url: block.url };
|
||||||
|
} else {
|
||||||
|
// Si on garde en HTML, on laisse le bloc tel quel pour le générateur
|
||||||
|
return block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return block;
|
return block;
|
||||||
}));
|
}));
|
||||||
@@ -696,13 +667,13 @@ p { margin-top: 0; margin-bottom: 10px; }
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</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.">
|
<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="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">
|
<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">
|
||||||
<FileX size={14} /> Reset Corps
|
<FileX size={14} /> Reset Corps
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text="Efface tout et restaure la configuration par défaut du bulletin.">
|
<Tooltip text="Efface tout et restaure la configuration par défaut du bulletin." className="flex-1">
|
||||||
<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">
|
<button onClick={resetProject} className="w-full 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
|
<RotateCcw size={14} /> Reset Tout
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -714,31 +685,31 @@ p { margin-top: 0; margin-bottom: 10px; }
|
|||||||
<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">
|
||||||
<Tooltip text="Ajoute le bandeau supérieur avec le titre et le sous-titre du bulletin.">
|
<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} />
|
<BlockButton icon={<Layout size={16} />} label="En-tête" onClick={() => addBlock('header')} disabled={hasHeader} isActive={selectedBlock?.type === 'header'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text="Ajoute une ligne horizontale pour séparer les sections.">
|
<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')} isActive={selectedBlock?.type === 'divider'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text="Ajoute un titre de section en violet et majuscules.">
|
<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')} isActive={selectedBlock?.type === 'title'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text="Ajoute un paragraphe de texte riche éditable.">
|
<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')} isActive={selectedBlock?.type === 'text'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text="Ajoute un lien hypertexte simple aligné à droite.">
|
<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')} isActive={selectedBlock?.type === 'link'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text="Ajoute un bouton d'action coloré (ex: Lire la suite).">
|
<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')} isActive={selectedBlock?.type === 'button'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text="Ajoute une image ou une photo.">
|
<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')} isActive={selectedBlock?.type === 'image'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text="Ajoute un espace vide vertical pour aérer la mise en page.">
|
<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')} isActive={selectedBlock?.type === 'spacer'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text="Ajoute le pied de page avec logo, adresse et mentions légales.">
|
<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} />
|
<BlockButton icon={<Code size={16} />} label="Pied" onClick={() => addBlock('footer')} disabled={hasFooter} isActive={selectedBlock?.type === 'footer'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -801,12 +772,15 @@ p { margin-top: 0; margin-bottom: 10px; }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlockButton({ icon, label, onClick, disabled }: { icon: any, label: string, onClick: () => void, disabled?: boolean }) {
|
function BlockButton({ icon, label, onClick, disabled, isActive }: { icon: any, label: string, onClick: () => void, disabled?: boolean, isActive?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={disabled ? undefined : onClick}
|
onClick={disabled ? undefined : onClick}
|
||||||
disabled={disabled}
|
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'}`}
|
className={`flex flex-col items-center justify-center p-3 border rounded transition-all gap-2 shadow-sm w-full
|
||||||
|
${disabled ? 'opacity-50 cursor-not-allowed bg-gray-100 border-gray-200' :
|
||||||
|
isActive ? 'bg-indigo-50 border-indigo-500 text-indigo-700 ring-1 ring-indigo-500' :
|
||||||
|
'bg-white border-gray-200 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>
|
||||||
@@ -876,21 +850,43 @@ function EditPanel({ block, updateBlock, deleteBlock }: { block: any, updateBloc
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ÉDITION SPÉCIFIQUE HEADER : IMAGE DE FOND */}
|
{/* ÉDITION SPÉCIFIQUE HEADER : IMAGE DE FOND & OPTIONS D'EXPORT */}
|
||||||
{block.type === 'header' && (
|
{block.type === 'header' && (
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<label className="block text-xs font-semibold text-slate-500 mb-1">Image de fond</label>
|
<div>
|
||||||
<input
|
<label className="block text-xs font-semibold text-slate-500 mb-1">Image de fond (URL)</label>
|
||||||
type="text"
|
<input
|
||||||
value={block.backgroundImage || ''}
|
type="text"
|
||||||
onChange={(e) => updateBlock(block.id, 'backgroundImage', e.target.value)}
|
value={block.backgroundImage || ''}
|
||||||
className="w-full p-2 border border-gray-300 rounded text-sm mb-2"
|
onChange={(e) => updateBlock(block.id, 'backgroundImage', e.target.value)}
|
||||||
placeholder="URL image (https://...)"
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<Upload size={12} /> Téléverser une image
|
<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">
|
||||||
<input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(e, 'backgroundImage')} />
|
<Upload size={12} /> Téléverser une image
|
||||||
|
<input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(e, 'backgroundImage')} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* NOUVEAU BLOC: Paramètres d'export */}
|
||||||
|
<div className="bg-slate-50 p-3 rounded border border-slate-200 space-y-3">
|
||||||
|
<h4 className="text-xs font-bold text-slate-700 uppercase">Options d'export</h4>
|
||||||
|
<label className="flex gap-2 cursor-pointer text-xs font-medium text-slate-700 items-start">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={block.exportAsImage !== false}
|
||||||
|
onChange={(e) => updateBlock(block.id, 'exportAsImage', e.target.checked)}
|
||||||
|
className="cursor-pointer mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="block mb-1">Convertir tout l'en-tête en image Base64</span>
|
||||||
|
<span className="text-[10px] text-slate-500 font-normal leading-tight block">
|
||||||
|
Coché : le texte et le fond fusionnent. <br/>
|
||||||
|
Décoché : garde le format HTML (le fond reste un lien URL, la décoration est masquée).
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -977,15 +973,10 @@ function EditPanel({ block, updateBlock, deleteBlock }: { block: any, updateBloc
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* URL COMMUNE */}
|
|
||||||
{(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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* UPLOAD IMAGE SPÉCIFIQUE */}
|
{/* UPLOAD IMAGE SPÉCIFIQUE */}
|
||||||
{block.type === 'image' && (
|
{block.type === 'image' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-500 mb-1">Image</label>
|
<label className="block text-xs font-semibold text-slate-500 mb-1">Source de l'image (URL ou fichier)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={block.src}
|
value={block.src}
|
||||||
@@ -1002,6 +993,20 @@ function EditPanel({ block, updateBlock, deleteBlock }: { block: any, updateBloc
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* LIEN DE REDIRECTION (COMMUN POUR IMAGE, HEADER, BUTTON, LINK) */}
|
||||||
|
{(block.type === 'button' || block.type === 'link' || block.type === 'image' || block.type === 'header') && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 mb-1">Lien de redirection (au clic)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={block.url || ''}
|
||||||
|
onChange={(e) => updateBlock(block.id, 'url', e.target.value)}
|
||||||
|
className="w-full p-2 border border-gray-300 rounded text-sm"
|
||||||
|
placeholder="https://..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ALIGNEMENT */}
|
{/* ALIGNEMENT */}
|
||||||
{block.type !== 'spacer' && block.type !== 'footer' && block.type !== 'divider' && (
|
{block.type !== 'spacer' && block.type !== 'footer' && block.type !== 'divider' && (
|
||||||
<div>
|
<div>
|
||||||
@@ -1045,7 +1050,7 @@ function PreviewBlock({ block }: { block: any }) {
|
|||||||
|
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case 'header':
|
case 'header':
|
||||||
return (
|
const headerPreview = (
|
||||||
<>
|
<>
|
||||||
<div style={{
|
<div style={{
|
||||||
...containerStyle,
|
...containerStyle,
|
||||||
@@ -1055,12 +1060,12 @@ function PreviewBlock({ block }: { block: any }) {
|
|||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
}}>
|
}}>
|
||||||
{/* DECORATION (Z-INDEX 0) POUR ÊTRE DERRIÈRE */}
|
{/* DECORATION */}
|
||||||
{block.decoration?.enabled && (
|
{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 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 */}
|
{/* TEXTE */}
|
||||||
<h1 style={{...textStyle, fontWeight: 'normal', lineHeight: '1.1', position: 'relative', zIndex: 10}}>{block.content}</h1>
|
<h1 style={{...textStyle, fontWeight: 'normal', lineHeight: '1.1', position: 'relative', zIndex: 10}}>{block.content}</h1>
|
||||||
{block.subtitle && (
|
{block.subtitle && (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -1079,6 +1084,7 @@ function PreviewBlock({ block }: { block: any }) {
|
|||||||
<div style={{ backgroundColor: ORANGE_COLOR, height: '1px', width: '100%' }}></div>
|
<div style={{ backgroundColor: ORANGE_COLOR, height: '1px', width: '100%' }}></div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
return block.url ? <a href={block.url} onClick={(e) => e.preventDefault()} style={{textDecoration:'none', display:'block', cursor:'pointer'}}>{headerPreview}</a> : headerPreview;
|
||||||
|
|
||||||
case 'title':
|
case 'title':
|
||||||
return (
|
return (
|
||||||
@@ -1129,7 +1135,8 @@ function PreviewBlock({ block }: { block: any }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'image':
|
case 'image':
|
||||||
return <div style={containerStyle}><img src={block.src} alt={block.alt} style={{ maxWidth: '100%', width: block.width === '100%' ? '100%' : block.width, height: 'auto', display: 'inline-block' }} /></div>;
|
const innerImg = <img src={block.src} alt={block.alt} style={{ maxWidth: '100%', width: block.width === '100%' ? '100%' : block.width, height: 'auto', display: 'inline-block' }} />;
|
||||||
|
return <div style={containerStyle}>{block.url ? <a href={block.url} onClick={(e) => e.preventDefault()}>{innerImg}</a> : innerImg}</div>;
|
||||||
case 'button':
|
case 'button':
|
||||||
return (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
|
|||||||
Reference in New Issue
Block a user