Update TemplateEditor.tsx
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { EmailTemplate } from '../types';
|
import { EmailTemplate } from '../types';
|
||||||
import { saveTemplate, generateSQL, generateSelectSQL, getTemplates, generateTemplateKey, generateN8nCode } from '../services/storage';
|
import { saveTemplate, generateSQL, generateSelectSQL, getTemplates, generateTemplateKey, generateN8nCode } from '../services/storage';
|
||||||
@@ -14,6 +15,18 @@ interface Props {
|
|||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback for crypto.randomUUID in non-secure (HTTP) contexts
|
||||||
|
const generateUUID = () => {
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const DEFAULT_HEADER = `<div style="background-color: #f8fafc; padding: 20px; text-align: center;">
|
const DEFAULT_HEADER = `<div style="background-color: #f8fafc; padding: 20px; text-align: center;">
|
||||||
<h1 style="color: #334155; margin: 0;">La Mia Azienda</h1>
|
<h1 style="color: #334155; margin: 0;">La Mia Azienda</h1>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -45,7 +58,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
const [showAiModal, setShowAiModal] = useState(false);
|
const [showAiModal, setShowAiModal] = useState(false);
|
||||||
const [nameError, setNameError] = useState('');
|
const [nameError, setNameError] = useState('');
|
||||||
|
|
||||||
// Variable detection logic
|
|
||||||
const detectVariables = useCallback(() => {
|
const detectVariables = useCallback(() => {
|
||||||
const regex = /\{\{([\w\d_-]+)\}\}/g;
|
const regex = /\{\{([\w\d_-]+)\}\}/g;
|
||||||
const allText = `${subject} ${header} ${body} ${footer}`;
|
const allText = `${subject} ${header} ${body} ${footer}`;
|
||||||
@@ -58,7 +70,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
detectVariables();
|
detectVariables();
|
||||||
}, [detectVariables]);
|
}, [detectVariables]);
|
||||||
|
|
||||||
// Clear name error when typing
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nameError) setNameError('');
|
if (nameError) setNameError('');
|
||||||
}, [name]);
|
}, [name]);
|
||||||
@@ -68,29 +79,35 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
|
|
||||||
if (!newKey) {
|
if (!newKey) {
|
||||||
setNameError('Il nome del template non può essere vuoto o contenere solo simboli.');
|
setNameError('Il nome del template non può essere vuoto o contenere solo simboli.');
|
||||||
alert('Il nome del template non può essere vuoto.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
|
console.log("Inizio processo di salvataggio per:", name);
|
||||||
|
|
||||||
// Check for duplicates
|
// Check for duplicates
|
||||||
const allTemplates = await getTemplates();
|
let allTemplates: EmailTemplate[] = [];
|
||||||
|
try {
|
||||||
|
allTemplates = await getTemplates();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Errore durante il recupero dei template esistenti:", err);
|
||||||
|
// Non blocchiamo necessariamente, ma avvisiamo
|
||||||
|
}
|
||||||
|
|
||||||
const isDuplicate = allTemplates.some(t => {
|
const isDuplicate = allTemplates.some(t => {
|
||||||
// Exclude current template if we are editing
|
|
||||||
if (initialTemplate && t.id === initialTemplate.id) return false;
|
if (initialTemplate && t.id === initialTemplate.id) return false;
|
||||||
return generateTemplateKey(t.name) === newKey;
|
return generateTemplateKey(t.name) === newKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
setNameError('Esiste già un template con questo nome.');
|
setNameError('Esiste già un template con questo nome.');
|
||||||
alert('Un template con questo nome (o ID risultante) esiste già. Per favore scegli un nome univoco.');
|
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTemplate: EmailTemplate = {
|
const newTemplate: EmailTemplate = {
|
||||||
id: initialTemplate?.id || crypto.randomUUID(),
|
id: initialTemplate?.id || generateUUID(),
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
subject,
|
subject,
|
||||||
@@ -100,10 +117,14 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
variables: detectedVars,
|
variables: detectedVars,
|
||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("Invio dati al server...", newTemplate);
|
||||||
await saveTemplate(newTemplate);
|
await saveTemplate(newTemplate);
|
||||||
|
console.log("Salvataggio completato con successo");
|
||||||
onSave();
|
onSave();
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
alert("Impossibile salvare il template. Controlla i log del server.");
|
console.error("ERRORE SALVATAGGIO:", e);
|
||||||
|
alert(`Errore: ${e.message || 'Impossibile salvare il template'}`);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
@@ -158,7 +179,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Maps internal tab keys to Italian display names
|
|
||||||
const tabNames: Record<string, string> = {
|
const tabNames: Record<string, string> = {
|
||||||
header: 'Testata',
|
header: 'Testata',
|
||||||
body: 'Corpo',
|
body: 'Corpo',
|
||||||
@@ -167,7 +187,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen bg-slate-50">
|
<div className="flex flex-col h-screen bg-slate-50">
|
||||||
{/* Top Bar */}
|
|
||||||
<header className="bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between sticky top-0 z-20 shrink-0">
|
<header className="bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between sticky top-0 z-20 shrink-0">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button onClick={onBack} className="p-2 hover:bg-slate-100 rounded-full text-slate-500">
|
<button onClick={onBack} className="p-2 hover:bg-slate-100 rounded-full text-slate-500">
|
||||||
@@ -199,11 +218,8 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* Left: Inputs */}
|
|
||||||
<div className="w-1/2 flex flex-col border-r border-slate-200 bg-white overflow-y-auto custom-scrollbar">
|
<div className="w-1/2 flex flex-col border-r border-slate-200 bg-white overflow-y-auto custom-scrollbar">
|
||||||
|
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
{/* Metadata */}
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-slate-700 mb-1">Nome Template</label>
|
<label className="block text-sm font-semibold text-slate-700 mb-1">Nome Template</label>
|
||||||
@@ -241,7 +257,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Variable Manager */}
|
|
||||||
<div className="bg-slate-50 p-4 rounded-lg border border-slate-200">
|
<div className="bg-slate-50 p-4 rounded-lg border border-slate-200">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider">Variabili Attive</span>
|
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider">Variabili Attive</span>
|
||||||
@@ -257,7 +272,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<div className="flex border-b border-slate-200">
|
<div className="flex border-b border-slate-200">
|
||||||
{(['header', 'body', 'footer'] as const).map((tab) => (
|
{(['header', 'body', 'footer'] as const).map((tab) => (
|
||||||
<button
|
<button
|
||||||
@@ -274,7 +288,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Editor Area */}
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<label className="text-sm font-semibold text-slate-700">Editor Contenuti</label>
|
<label className="text-sm font-semibold text-slate-700">Editor Contenuti</label>
|
||||||
@@ -291,7 +304,7 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
|
|
||||||
<div className="h-[400px]">
|
<div className="h-[400px]">
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
key={activeTab} // Force remount on tab change to sync contentEditable
|
key={activeTab}
|
||||||
value={getActiveContent()}
|
value={getActiveContent()}
|
||||||
onChange={setActiveContent}
|
onChange={setActiveContent}
|
||||||
placeholder={`Crea qui la sezione ${tabNames[activeTab]}...`}
|
placeholder={`Crea qui la sezione ${tabNames[activeTab]}...`}
|
||||||
@@ -306,7 +319,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Live Preview */}
|
|
||||||
<div className="w-1/2 bg-slate-100 flex flex-col overflow-hidden">
|
<div className="w-1/2 bg-slate-100 flex flex-col overflow-hidden">
|
||||||
<div className="p-3 bg-white border-b border-slate-200 flex justify-between items-center shadow-sm z-10 shrink-0">
|
<div className="p-3 bg-white border-b border-slate-200 flex justify-between items-center shadow-sm z-10 shrink-0">
|
||||||
<span className="font-semibold text-slate-600 flex items-center gap-2">
|
<span className="font-semibold text-slate-600 flex items-center gap-2">
|
||||||
@@ -316,17 +328,13 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
Renderizzato come HTML standard
|
Renderizzato come HTML standard
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Scrollable container: flex-1 ensures it takes available space, overflow-y-auto enables scrolling */}
|
|
||||||
<div className="flex-1 p-8 overflow-y-auto custom-scrollbar">
|
<div className="flex-1 p-8 overflow-y-auto custom-scrollbar">
|
||||||
{/* mx-auto centers the card without using flexbox on the parent which can cause scroll issues */}
|
|
||||||
<div className="w-full max-w-2xl bg-white shadow-xl rounded-lg overflow-hidden min-h-[600px] flex flex-col mx-auto">
|
<div className="w-full max-w-2xl bg-white shadow-xl rounded-lg overflow-hidden min-h-[600px] flex flex-col mx-auto">
|
||||||
{/* Simulate Subject Line in Preview */}
|
|
||||||
<div className="bg-slate-50 border-b border-slate-100 p-4">
|
<div className="bg-slate-50 border-b border-slate-100 p-4">
|
||||||
<span className="text-xs font-bold text-slate-400 uppercase">Oggetto:</span>
|
<span className="text-xs font-bold text-slate-400 uppercase">Oggetto:</span>
|
||||||
<p className="text-sm font-medium text-slate-800">{subject.replace(/\{\{([\w\d_-]+)\}\}/g, (match, p1) => `<span class="bg-yellow-100 text-yellow-800 px-1 rounded">${match}</span>`)}</p>
|
<p className="text-sm font-medium text-slate-800">{subject.replace(/\{\{([\w\d_-]+)\}\}/g, (match, p1) => `<span class="bg-yellow-100 text-yellow-800 px-1 rounded">${match}</span>`)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email Content */}
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: header }} />
|
<div dangerouslySetInnerHTML={{ __html: header }} />
|
||||||
<div dangerouslySetInnerHTML={{ __html: body }} className="flex-1" />
|
<div dangerouslySetInnerHTML={{ __html: body }} className="flex-1" />
|
||||||
<div dangerouslySetInnerHTML={{ __html: footer }} />
|
<div dangerouslySetInnerHTML={{ __html: footer }} />
|
||||||
@@ -335,7 +343,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SQL Modal */}
|
|
||||||
{showSqlModal && (
|
{showSqlModal && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl p-6 flex flex-col max-h-[90vh]">
|
<div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl p-6 flex flex-col max-h-[90vh]">
|
||||||
@@ -350,7 +357,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-8 overflow-y-auto px-1 pb-4">
|
<div className="space-y-8 overflow-y-auto px-1 pb-4">
|
||||||
{/* INSERT Section */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2 block">1. Setup (Esegui una volta nel DB)</label>
|
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2 block">1. Setup (Esegui una volta nel DB)</label>
|
||||||
@@ -385,7 +391,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* n8n Code Section */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-bold text-purple-600 uppercase tracking-wider mb-2 flex items-center gap-2">
|
<label className="text-xs font-bold text-purple-600 uppercase tracking-wider mb-2 flex items-center gap-2">
|
||||||
<Code size={16} />
|
<Code size={16} />
|
||||||
@@ -412,7 +417,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* AI Modal */}
|
|
||||||
{showAiModal && (
|
{showAiModal && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-lg p-6">
|
<div className="bg-white rounded-xl shadow-2xl w-full max-w-lg p-6">
|
||||||
@@ -446,11 +450,6 @@ const Editor: React.FC<Props> = ({ initialTemplate, onBack, onSave }) => {
|
|||||||
{!isGenerating && <Wand2 size={16} />}
|
{!isGenerating && <Wand2 size={16} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{!process.env.API_KEY && (
|
|
||||||
<p className="mt-3 text-xs text-red-500">
|
|
||||||
Nota: API_KEY non rilevata nell'ambiente. Questa funzione richiede una chiave API Gemini.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user