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:
Manus Sandbox
2025-11-23 16:05:38 -05:00
parent e16bdb22a1
commit 4e825dbf99
5 changed files with 804 additions and 235 deletions

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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