Checkpoint: Correction de l'erreur de compatibilité React Quill avec React 19. Remplacement par TipTap (éditeur moderne compatible React 19). Barre d'outils complète (gras, italique, barré, titres H2/H3, listes). 7 boutons de variables dynamiques fonctionnels. Prévisualisation HTML en temps réel. Imports corrigés pour utiliser les exports nommés.
This commit is contained in:
@@ -175,3 +175,55 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Styles pour l'éditeur TipTap */
|
||||
.ProseMirror {
|
||||
outline: none;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.ProseMirror p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.ProseMirror h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 1rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.ProseMirror h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0.75rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.ProseMirror ul,
|
||||
.ProseMirror ol {
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.ProseMirror li {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.ProseMirror strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ProseMirror em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ProseMirror s {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.ProseMirror a {
|
||||
color: oklch(0.55 0.22 263);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ProseMirror:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import ReactQuill from 'react-quill';
|
||||
import 'react-quill/dist/quill.snow.css';
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { TextStyle } from '@tiptap/extension-text-style';
|
||||
import { Color } from '@tiptap/extension-color';
|
||||
import { Link } from '@tiptap/extension-link';
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import DashboardLayout from "@/components/DashboardLayout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -45,6 +48,22 @@ export default function AdminEmailTemplates() {
|
||||
{ enabled: !!selectedType }
|
||||
);
|
||||
|
||||
// Initialiser l'éditeur TipTap
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
TextStyle,
|
||||
Color,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
}),
|
||||
],
|
||||
content: formData.bodyContent,
|
||||
onUpdate: ({ editor }) => {
|
||||
setFormData({ ...formData, bodyContent: editor.getHTML() });
|
||||
},
|
||||
});
|
||||
|
||||
const upsertMutation = trpc.emailTemplates.upsert.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Template enregistré avec succès");
|
||||
@@ -68,7 +87,7 @@ export default function AdminEmailTemplates() {
|
||||
// Charger le template sélectionné
|
||||
useEffect(() => {
|
||||
if (currentTemplate) {
|
||||
setFormData({
|
||||
const newData = {
|
||||
type: currentTemplate.type,
|
||||
name: currentTemplate.name,
|
||||
logoUrl: currentTemplate.logoUrl || "",
|
||||
@@ -79,9 +98,14 @@ export default function AdminEmailTemplates() {
|
||||
bodyContent: currentTemplate.bodyContent || "",
|
||||
footerText: currentTemplate.footerText || "",
|
||||
active: currentTemplate.active,
|
||||
});
|
||||
};
|
||||
setFormData(newData);
|
||||
// Mettre à jour le contenu de l'éditeur
|
||||
if (editor) {
|
||||
editor.commands.setContent(newData.bodyContent);
|
||||
}
|
||||
}, [currentTemplate]);
|
||||
}
|
||||
}, [currentTemplate, editor]);
|
||||
|
||||
const handleSave = () => {
|
||||
upsertMutation.mutate({
|
||||
@@ -320,7 +344,7 @@ export default function AdminEmailTemplates() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{nomApprenant}}' })}
|
||||
onClick={() => editor?.chain().focus().insertContent('{{nomApprenant}}').run()}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Nom apprenant
|
||||
@@ -329,7 +353,7 @@ export default function AdminEmailTemplates() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{prenomApprenant}}' })}
|
||||
onClick={() => editor?.chain().focus().insertContent('{{prenomApprenant}}').run()}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Prénom apprenant
|
||||
@@ -338,7 +362,7 @@ export default function AdminEmailTemplates() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{nomFormation}}' })}
|
||||
onClick={() => editor?.chain().focus().insertContent('{{nomFormation}}').run()}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Nom formation
|
||||
@@ -347,7 +371,7 @@ export default function AdminEmailTemplates() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{nomSequence}}' })}
|
||||
onClick={() => editor?.chain().focus().insertContent('{{nomSequence}}').run()}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Nom séquence
|
||||
@@ -356,7 +380,7 @@ export default function AdminEmailTemplates() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{dateDebut}}' })}
|
||||
onClick={() => editor?.chain().focus().insertContent('{{dateDebut}}').run()}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Date début
|
||||
@@ -365,7 +389,7 @@ export default function AdminEmailTemplates() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{lieu}}' })}
|
||||
onClick={() => editor?.chain().focus().insertContent('{{lieu}}').run()}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Lieu
|
||||
@@ -374,32 +398,89 @@ export default function AdminEmailTemplates() {
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{formateur}}' })}
|
||||
onClick={() => editor?.chain().focus().insertContent('{{formateur}}').run()}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Formateur
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Éditeur WYSIWYG */}
|
||||
{/* Éditeur WYSIWYG TipTap */}
|
||||
<div className="border rounded-md">
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={formData.bodyContent}
|
||||
onChange={(value) => setFormData({ ...formData, bodyContent: value })}
|
||||
placeholder="Contenu principal de l'email...\n\nVous pouvez utiliser des variables : {{nomApprenant}}, {{nomFormation}}, {{nomSequence}}, etc."
|
||||
modules={{
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
['link'],
|
||||
['clean']
|
||||
]
|
||||
}}
|
||||
style={{ minHeight: '200px' }}
|
||||
/>
|
||||
{/* Barre d'outils */}
|
||||
{editor && (
|
||||
<div className="border-b p-2 flex flex-wrap gap-1 bg-muted/30">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
className={editor.isActive('bold') ? 'bg-muted' : ''}
|
||||
>
|
||||
<strong>B</strong>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
className={editor.isActive('italic') ? 'bg-muted' : ''}
|
||||
>
|
||||
<em>I</em>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
className={editor.isActive('strike') ? 'bg-muted' : ''}
|
||||
>
|
||||
<s>S</s>
|
||||
</Button>
|
||||
<div className="w-px h-6 bg-border mx-1" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
className={editor.isActive('heading', { level: 2 }) ? 'bg-muted' : ''}
|
||||
>
|
||||
H2
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
className={editor.isActive('heading', { level: 3 }) ? 'bg-muted' : ''}
|
||||
>
|
||||
H3
|
||||
</Button>
|
||||
<div className="w-px h-6 bg-border mx-1" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
className={editor.isActive('bulletList') ? 'bg-muted' : ''}
|
||||
>
|
||||
• Liste
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
className={editor.isActive('orderedList') ? 'bg-muted' : ''}
|
||||
>
|
||||
1. Liste
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* Zone d'édition */}
|
||||
<div className="p-3 min-h-[200px] prose prose-sm max-w-none">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Le contenu du corps du message. Utilisez les boutons ci-dessus pour insérer des variables dynamiques.
|
||||
|
||||
@@ -42,6 +42,11 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@tiptap/extension-color": "^3.11.0",
|
||||
"@tiptap/extension-link": "^3.11.0",
|
||||
"@tiptap/extension-text-style": "^3.11.0",
|
||||
"@tiptap/react": "^3.11.0",
|
||||
"@tiptap/starter-kit": "^3.11.0",
|
||||
"@trpc/client": "^11.6.0",
|
||||
"@trpc/react-query": "^11.6.0",
|
||||
"@trpc/server": "^11.6.0",
|
||||
@@ -71,7 +76,6 @@
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.64.0",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"recharts": "^2.15.4",
|
||||
"sonner": "^2.0.7",
|
||||
|
||||
831
pnpm-lock.yaml
generated
831
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
5
todo.md
5
todo.md
@@ -498,3 +498,8 @@
|
||||
- [x] Intégrer l'éditeur dans AdminEmailTemplates
|
||||
- [x] Ajouter des boutons pour insérer des variables dynamiques ({{nomApprenant}}, {{nomFormation}}, {{dateDebut}}, etc.)
|
||||
- [x] Tester l'éditeur et la prévisualisation
|
||||
|
||||
## Correction erreur React Quill incompatible avec React 19
|
||||
|
||||
- [x] Remplacer React Quill par une solution compatible (TipTap)
|
||||
- [x] Tester le nouvel éditeur
|
||||
|
||||
Reference in New Issue
Block a user