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:
@@ -174,4 +174,56 @@
|
|||||||
max-width: 1280px; /* Standard content width */
|
max-width: 1280px; /* Standard content width */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* 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 { 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.
|
||||||
|
|||||||
@@ -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
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] 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
|
||||||
|
|||||||
Reference in New Issue
Block a user