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 { useState, useEffect } from "react";
import ReactQuill from 'react-quill'; import { useEditor, EditorContent } from '@tiptap/react';
import 'react-quill/dist/quill.snow.css'; 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 { trpc } from "@/lib/trpc";
import DashboardLayout from "@/components/DashboardLayout"; import DashboardLayout from "@/components/DashboardLayout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -45,6 +48,22 @@ export default function AdminEmailTemplates() {
{ enabled: !!selectedType } { 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({ const upsertMutation = trpc.emailTemplates.upsert.useMutation({
onSuccess: () => { onSuccess: () => {
toast.success("Template enregistré avec succès"); toast.success("Template enregistré avec succès");
@@ -68,7 +87,7 @@ export default function AdminEmailTemplates() {
// Charger le template sélectionné // Charger le template sélectionné
useEffect(() => { useEffect(() => {
if (currentTemplate) { if (currentTemplate) {
setFormData({ const newData = {
type: currentTemplate.type, type: currentTemplate.type,
name: currentTemplate.name, name: currentTemplate.name,
logoUrl: currentTemplate.logoUrl || "", logoUrl: currentTemplate.logoUrl || "",
@@ -79,9 +98,14 @@ export default function AdminEmailTemplates() {
bodyContent: currentTemplate.bodyContent || "", bodyContent: currentTemplate.bodyContent || "",
footerText: currentTemplate.footerText || "", footerText: currentTemplate.footerText || "",
active: currentTemplate.active, active: currentTemplate.active,
}); };
setFormData(newData);
// Mettre à jour le contenu de l'éditeur
if (editor) {
editor.commands.setContent(newData.bodyContent);
} }
}, [currentTemplate]); }
}, [currentTemplate, editor]);
const handleSave = () => { const handleSave = () => {
upsertMutation.mutate({ upsertMutation.mutate({
@@ -320,7 +344,7 @@ export default function AdminEmailTemplates() {
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{nomApprenant}}' })} onClick={() => editor?.chain().focus().insertContent('{{nomApprenant}}').run()}
> >
<Plus className="h-3 w-3 mr-1" /> <Plus className="h-3 w-3 mr-1" />
Nom apprenant Nom apprenant
@@ -329,7 +353,7 @@ export default function AdminEmailTemplates() {
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{prenomApprenant}}' })} onClick={() => editor?.chain().focus().insertContent('{{prenomApprenant}}').run()}
> >
<Plus className="h-3 w-3 mr-1" /> <Plus className="h-3 w-3 mr-1" />
Prénom apprenant Prénom apprenant
@@ -338,7 +362,7 @@ export default function AdminEmailTemplates() {
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{nomFormation}}' })} onClick={() => editor?.chain().focus().insertContent('{{nomFormation}}').run()}
> >
<Plus className="h-3 w-3 mr-1" /> <Plus className="h-3 w-3 mr-1" />
Nom formation Nom formation
@@ -347,7 +371,7 @@ export default function AdminEmailTemplates() {
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{nomSequence}}' })} onClick={() => editor?.chain().focus().insertContent('{{nomSequence}}').run()}
> >
<Plus className="h-3 w-3 mr-1" /> <Plus className="h-3 w-3 mr-1" />
Nom séquence Nom séquence
@@ -356,7 +380,7 @@ export default function AdminEmailTemplates() {
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{dateDebut}}' })} onClick={() => editor?.chain().focus().insertContent('{{dateDebut}}').run()}
> >
<Plus className="h-3 w-3 mr-1" /> <Plus className="h-3 w-3 mr-1" />
Date début Date début
@@ -365,7 +389,7 @@ export default function AdminEmailTemplates() {
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{lieu}}' })} onClick={() => editor?.chain().focus().insertContent('{{lieu}}').run()}
> >
<Plus className="h-3 w-3 mr-1" /> <Plus className="h-3 w-3 mr-1" />
Lieu Lieu
@@ -374,32 +398,89 @@ export default function AdminEmailTemplates() {
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setFormData({ ...formData, bodyContent: formData.bodyContent + '{{formateur}}' })} onClick={() => editor?.chain().focus().insertContent('{{formateur}}').run()}
> >
<Plus className="h-3 w-3 mr-1" /> <Plus className="h-3 w-3 mr-1" />
Formateur Formateur
</Button> </Button>
</div> </div>
{/* Éditeur WYSIWYG */} {/* Éditeur WYSIWYG TipTap */}
<div className="border rounded-md"> <div className="border rounded-md">
<ReactQuill {/* Barre d'outils */}
theme="snow" {editor && (
value={formData.bodyContent} <div className="border-b p-2 flex flex-wrap gap-1 bg-muted/30">
onChange={(value) => setFormData({ ...formData, bodyContent: value })} <Button
placeholder="Contenu principal de l'email...\n\nVous pouvez utiliser des variables : {{nomApprenant}}, {{nomFormation}}, {{nomSequence}}, etc." type="button"
modules={{ variant="ghost"
toolbar: [ size="sm"
[{ 'header': [1, 2, 3, false] }], onClick={() => editor.chain().focus().toggleBold().run()}
['bold', 'italic', 'underline', 'strike'], className={editor.isActive('bold') ? 'bg-muted' : ''}
[{ 'list': 'ordered'}, { 'list': 'bullet' }], >
[{ 'color': [] }, { 'background': [] }], <strong>B</strong>
['link'], </Button>
['clean'] <Button
] type="button"
}} variant="ghost"
style={{ minHeight: '200px' }} 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> </div>
<p className="text-xs text-muted-foreground mt-1"> <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. 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-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.2", "@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/client": "^11.6.0",
"@trpc/react-query": "^11.6.0", "@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0", "@trpc/server": "^11.6.0",
@@ -71,7 +76,6 @@
"react-day-picker": "^9.11.1", "react-day-picker": "^9.11.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-hook-form": "^7.64.0", "react-hook-form": "^7.64.0",
"react-quill": "^2.0.0",
"react-resizable-panels": "^3.0.6", "react-resizable-panels": "^3.0.6",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"sonner": "^2.0.7", "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] Intégrer l'éditeur dans AdminEmailTemplates
- [x] Ajouter des boutons pour insérer des variables dynamiques ({{nomApprenant}}, {{nomFormation}}, {{dateDebut}}, etc.) - [x] Ajouter des boutons pour insérer des variables dynamiques ({{nomApprenant}}, {{nomFormation}}, {{dateDebut}}, etc.)
- [x] Tester l'éditeur et la prévisualisation - [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