create-crm-tmp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/bin/create-crm-tmp.js +93 -0
  2. package/package.json +25 -0
  3. package/template/.prettierignore +33 -0
  4. package/template/.prettierrc.json +25 -0
  5. package/template/README.md +173 -0
  6. package/template/eslint.config.mjs +18 -0
  7. package/template/exemple-contacts.csv +11 -0
  8. package/template/next.config.ts +8 -0
  9. package/template/package.json +64 -0
  10. package/template/postcss.config.mjs +7 -0
  11. package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
  12. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
  13. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
  14. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
  15. package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
  16. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
  17. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
  18. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
  19. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
  20. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
  21. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
  22. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
  23. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
  24. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
  25. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
  26. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
  27. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
  28. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
  29. package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
  30. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
  31. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
  32. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
  33. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
  34. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
  35. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
  36. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
  37. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
  38. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
  39. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
  40. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
  41. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
  42. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
  43. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
  44. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
  45. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
  46. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
  47. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
  48. package/template/prisma/migrations/migration_lock.toml +3 -0
  49. package/template/prisma/schema.prisma +582 -0
  50. package/template/prisma.config.ts +14 -0
  51. package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
  52. package/template/src/app/(auth)/layout.tsx +3 -0
  53. package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
  54. package/template/src/app/(auth)/reset-password/page.tsx +146 -0
  55. package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
  56. package/template/src/app/(auth)/signin/page.tsx +166 -0
  57. package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
  58. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
  59. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
  60. package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
  61. package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
  62. package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
  63. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
  64. package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
  65. package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
  66. package/template/src/app/(dashboard)/layout.tsx +30 -0
  67. package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
  68. package/template/src/app/(dashboard)/templates/page.tsx +567 -0
  69. package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
  70. package/template/src/app/(dashboard)/users/page.tsx +457 -0
  71. package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
  72. package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
  73. package/template/src/app/api/audit-logs/route.ts +57 -0
  74. package/template/src/app/api/auth/[...all]/route.ts +4 -0
  75. package/template/src/app/api/auth/check-active/route.ts +31 -0
  76. package/template/src/app/api/auth/google/callback/route.ts +94 -0
  77. package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
  78. package/template/src/app/api/auth/google/route.ts +34 -0
  79. package/template/src/app/api/auth/google/status/route.ts +32 -0
  80. package/template/src/app/api/closing-reasons/route.ts +27 -0
  81. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
  82. package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
  83. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
  84. package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
  85. package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
  86. package/template/src/app/api/contacts/[id]/route.ts +322 -0
  87. package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
  88. package/template/src/app/api/contacts/export/route.ts +270 -0
  89. package/template/src/app/api/contacts/import/route.ts +381 -0
  90. package/template/src/app/api/contacts/route.ts +283 -0
  91. package/template/src/app/api/dashboard/stats/route.ts +299 -0
  92. package/template/src/app/api/email/track/[id]/route.ts +68 -0
  93. package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
  94. package/template/src/app/api/invite/complete/route.ts +88 -0
  95. package/template/src/app/api/invite/validate/route.ts +55 -0
  96. package/template/src/app/api/reminders/route.ts +95 -0
  97. package/template/src/app/api/reset-password/complete/route.ts +73 -0
  98. package/template/src/app/api/reset-password/request/route.ts +84 -0
  99. package/template/src/app/api/reset-password/validate/route.ts +49 -0
  100. package/template/src/app/api/reset-password/verify/route.ts +74 -0
  101. package/template/src/app/api/roles/[id]/route.ts +183 -0
  102. package/template/src/app/api/roles/route.ts +140 -0
  103. package/template/src/app/api/send/route.ts +282 -0
  104. package/template/src/app/api/settings/change-password/route.ts +95 -0
  105. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
  106. package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
  107. package/template/src/app/api/settings/company/route.ts +121 -0
  108. package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
  109. package/template/src/app/api/settings/google-ads/route.ts +122 -0
  110. package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
  111. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
  112. package/template/src/app/api/settings/google-sheet/route.ts +254 -0
  113. package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
  114. package/template/src/app/api/settings/meta-leads/route.ts +132 -0
  115. package/template/src/app/api/settings/profile/route.ts +42 -0
  116. package/template/src/app/api/settings/smtp/route.ts +130 -0
  117. package/template/src/app/api/settings/smtp/test/route.ts +121 -0
  118. package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
  119. package/template/src/app/api/settings/statuses/route.ts +83 -0
  120. package/template/src/app/api/statuses/route.ts +25 -0
  121. package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
  122. package/template/src/app/api/tasks/[id]/route.ts +728 -0
  123. package/template/src/app/api/tasks/meet/route.ts +240 -0
  124. package/template/src/app/api/tasks/route.ts +417 -0
  125. package/template/src/app/api/templates/[id]/route.ts +140 -0
  126. package/template/src/app/api/templates/route.ts +91 -0
  127. package/template/src/app/api/users/[id]/route.ts +168 -0
  128. package/template/src/app/api/users/list/route.ts +45 -0
  129. package/template/src/app/api/users/me/route.ts +48 -0
  130. package/template/src/app/api/users/route.ts +250 -0
  131. package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
  132. package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
  133. package/template/src/app/api/workflows/[id]/route.ts +192 -0
  134. package/template/src/app/api/workflows/process/route.ts +293 -0
  135. package/template/src/app/api/workflows/route.ts +124 -0
  136. package/template/src/app/favicon.ico +0 -0
  137. package/template/src/app/globals.css +1416 -0
  138. package/template/src/app/layout.tsx +31 -0
  139. package/template/src/app/page.tsx +32 -0
  140. package/template/src/components/dashboard/activity-chart.tsx +67 -0
  141. package/template/src/components/dashboard/contacts-chart.tsx +63 -0
  142. package/template/src/components/dashboard/recent-activity.tsx +164 -0
  143. package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
  144. package/template/src/components/dashboard/stat-card.tsx +61 -0
  145. package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
  146. package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
  147. package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
  148. package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
  149. package/template/src/components/editor.tsx +856 -0
  150. package/template/src/components/email-template.tsx +35 -0
  151. package/template/src/components/header.tsx +320 -0
  152. package/template/src/components/invitation-email-template.tsx +79 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +120 -0
  154. package/template/src/components/meet-confirmation-email-template.tsx +156 -0
  155. package/template/src/components/meet-update-email-template.tsx +209 -0
  156. package/template/src/components/page-header.tsx +61 -0
  157. package/template/src/components/reset-password-email-template.tsx +79 -0
  158. package/template/src/components/sidebar.tsx +294 -0
  159. package/template/src/components/skeleton.tsx +380 -0
  160. package/template/src/components/ui/commands.tsx +396 -0
  161. package/template/src/components/ui/components.tsx +150 -0
  162. package/template/src/components/ui/theme.tsx +5 -0
  163. package/template/src/components/view-as-banner.tsx +45 -0
  164. package/template/src/components/view-as-modal.tsx +186 -0
  165. package/template/src/contexts/mobile-menu-context.tsx +31 -0
  166. package/template/src/contexts/sidebar-context.tsx +107 -0
  167. package/template/src/contexts/task-reminder-context.tsx +239 -0
  168. package/template/src/contexts/view-as-context.tsx +84 -0
  169. package/template/src/hooks/use-user-role.ts +82 -0
  170. package/template/src/lib/audit-log.ts +45 -0
  171. package/template/src/lib/auth-client.ts +16 -0
  172. package/template/src/lib/auth.ts +35 -0
  173. package/template/src/lib/check-permission.ts +193 -0
  174. package/template/src/lib/contact-duplicate.ts +112 -0
  175. package/template/src/lib/contact-interactions.ts +371 -0
  176. package/template/src/lib/encryption.ts +99 -0
  177. package/template/src/lib/google-calendar.ts +300 -0
  178. package/template/src/lib/google-drive.ts +372 -0
  179. package/template/src/lib/permissions.ts +412 -0
  180. package/template/src/lib/prisma.ts +32 -0
  181. package/template/src/lib/roles.ts +120 -0
  182. package/template/src/lib/template-variables.ts +76 -0
  183. package/template/src/lib/utils.ts +46 -0
  184. package/template/src/lib/workflow-executor.ts +482 -0
  185. package/template/src/proxy.ts +91 -0
  186. package/template/tsconfig.json +34 -0
  187. package/template/vercel.json +8 -0
@@ -0,0 +1,856 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect, useMemo, useRef, forwardRef } from 'react';
4
+ import {
5
+ // Core system
6
+ createEditorSystem,
7
+
8
+ // Extensions
9
+ boldExtension,
10
+ italicExtension,
11
+ underlineExtension,
12
+ strikethroughExtension,
13
+ linkExtension,
14
+ horizontalRuleExtension,
15
+ TableExtension,
16
+ listExtension,
17
+ historyExtension,
18
+ imageExtension,
19
+ blockFormatExtension,
20
+ htmlExtension,
21
+ MarkdownExtension,
22
+ codeExtension,
23
+ codeFormatExtension,
24
+ HTMLEmbedExtension,
25
+ floatingToolbarExtension,
26
+ contextMenuExtension,
27
+ commandPaletteExtension,
28
+ DraggableBlockExtension,
29
+
30
+ // Utilities
31
+ ALL_MARKDOWN_TRANSFORMERS,
32
+
33
+ // Types
34
+ type ExtractCommands,
35
+ type ExtractStateQueries,
36
+ type BaseCommands,
37
+ } from '@lexkit/editor';
38
+ import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
39
+ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
40
+ import {
41
+ LexicalEditor,
42
+ $getSelection,
43
+ $isRangeSelection,
44
+ $insertNodes,
45
+ $createTextNode,
46
+ } from 'lexical';
47
+ import {
48
+ Bold,
49
+ Italic,
50
+ Underline,
51
+ Strikethrough,
52
+ List,
53
+ ListOrdered,
54
+ Undo,
55
+ Redo,
56
+ Image as ImageIcon,
57
+ AlignLeft,
58
+ AlignCenter,
59
+ AlignRight,
60
+ Upload,
61
+ Link,
62
+ Unlink,
63
+ Minus,
64
+ Code,
65
+ Terminal,
66
+ Table as TableIcon,
67
+ FileCode,
68
+ Eye,
69
+ Pencil,
70
+ Type,
71
+ Quote,
72
+ Indent,
73
+ Outdent,
74
+ } from 'lucide-react';
75
+ import { createPortal } from 'react-dom';
76
+ import { commandsToCommandPaletteItems, registerKeyboardShortcuts } from './ui/commands';
77
+ import { Select, Dropdown, Dialog } from './ui/components';
78
+ import { defaultTheme } from './ui/theme';
79
+
80
+ type TableConfig = {
81
+ rows?: number;
82
+ columns?: number;
83
+ includeHeaders?: boolean;
84
+ };
85
+
86
+ // Create markdown extension instance
87
+ const markdownExt = new MarkdownExtension().configure({
88
+ customTransformers: ALL_MARKDOWN_TRANSFORMERS,
89
+ });
90
+
91
+ // Define extensions array
92
+ export const extensions = [
93
+ boldExtension,
94
+ italicExtension,
95
+ underlineExtension,
96
+ strikethroughExtension,
97
+ linkExtension.configure({
98
+ linkSelectedTextOnPaste: true,
99
+ autoLinkText: true,
100
+ autoLinkUrls: true,
101
+ }),
102
+ horizontalRuleExtension,
103
+ new TableExtension().configure({
104
+ enableContextMenu: true,
105
+ markdownExtension: markdownExt,
106
+ }),
107
+ listExtension,
108
+ historyExtension,
109
+ imageExtension,
110
+ blockFormatExtension,
111
+ htmlExtension,
112
+ markdownExt,
113
+ codeExtension,
114
+ codeFormatExtension,
115
+ new HTMLEmbedExtension().configure({
116
+ markdownExtension: markdownExt,
117
+ }),
118
+ floatingToolbarExtension,
119
+ contextMenuExtension,
120
+ commandPaletteExtension,
121
+ new DraggableBlockExtension().configure({}),
122
+ ] as const;
123
+
124
+ // Create typed editor system
125
+ const { Provider, useEditor } = createEditorSystem<typeof extensions>();
126
+
127
+ // Extract types
128
+ type EditorCommands = BaseCommands & ExtractCommands<typeof extensions>;
129
+ type EditorStateQueries = ExtractStateQueries<typeof extensions>;
130
+ type ExtensionNames = (typeof extensions)[number]['name'];
131
+
132
+ // Ref interface for external control
133
+ export interface DefaultTemplateRef {
134
+ injectMarkdown: (content: string) => void;
135
+ injectHTML: (content: string) => void;
136
+ insertText: (text: string) => void;
137
+ getMarkdown: () => string;
138
+ getHTML: () => string;
139
+ }
140
+
141
+ // Hook for image handling logic
142
+ function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null) {
143
+ const fileInputRef = useRef<HTMLInputElement>(null);
144
+
145
+ const handlers = useMemo(
146
+ () => ({
147
+ insertFromUrl: () => {
148
+ const src = prompt('Enter image URL:');
149
+ if (!src) return;
150
+ const alt = prompt('Enter alt text:') || '';
151
+ const caption = prompt('Enter caption (optional):') || undefined;
152
+ commands.insertImage({ src, alt, caption });
153
+ },
154
+ insertFromFile: () => fileInputRef.current?.click(),
155
+ handleUpload: async (e: React.ChangeEvent<HTMLInputElement>) => {
156
+ const file = e.target.files?.[0];
157
+ if (!file) return;
158
+ let src: string;
159
+ if (imageExtension.config.uploadHandler) {
160
+ try {
161
+ src = await imageExtension.config.uploadHandler(file);
162
+ } catch (error) {
163
+ alert('Failed to upload image');
164
+ return;
165
+ }
166
+ } else {
167
+ src = URL.createObjectURL(file);
168
+ }
169
+ commands.insertImage({ src, alt: file.name, file });
170
+ e.target.value = '';
171
+ },
172
+ setAlignment: (alignment: 'left' | 'center' | 'right' | 'none') => {
173
+ commands.setImageAlignment(alignment);
174
+ },
175
+ setCaption: () => {
176
+ const newCaption = prompt('Enter caption:') || '';
177
+ commands.setImageCaption(newCaption);
178
+ },
179
+ }),
180
+ [commands],
181
+ );
182
+
183
+ return { handlers, fileInputRef };
184
+ }
185
+
186
+ // Floating Toolbar Component
187
+ function FloatingToolbarRenderer() {
188
+ const { commands, activeStates, extensions, hasExtension } = useEditor();
189
+
190
+ const [isVisible, setIsVisible] = useState(false);
191
+ const [selectionRect, setSelectionRect] = useState<{
192
+ x: number;
193
+ y: number;
194
+ positionFromRight?: boolean;
195
+ } | null>(null);
196
+
197
+ const floatingExtension = extensions.find((ext) => ext.name === 'floatingToolbar') as any;
198
+
199
+ useEffect(() => {
200
+ if (!floatingExtension) return;
201
+
202
+ const checkState = () => {
203
+ const visible = floatingExtension.getIsVisible();
204
+ const rect = floatingExtension.getSelectionRect();
205
+ setIsVisible(visible);
206
+ setSelectionRect(rect);
207
+ };
208
+
209
+ const interval = setInterval(checkState, 200);
210
+ return () => clearInterval(interval);
211
+ }, [floatingExtension]);
212
+
213
+ if (!isVisible || !selectionRect) return null;
214
+
215
+ const isImageSelected = activeStates.imageSelected;
216
+
217
+ return createPortal(
218
+ <div
219
+ className="lexkit-floating-toolbar"
220
+ style={{
221
+ position: 'absolute',
222
+ top: selectionRect.y,
223
+ ...(selectionRect.positionFromRight
224
+ ? { right: 10, left: 'auto' }
225
+ : { left: selectionRect.x, right: 'auto' }),
226
+ zIndex: 50,
227
+ maxWidth: 400,
228
+ flexWrap: 'wrap',
229
+ pointerEvents: 'auto',
230
+ }}
231
+ >
232
+ {isImageSelected ? (
233
+ <>
234
+ <button
235
+ type="button"
236
+ onClick={() => commands.setImageAlignment('left')}
237
+ className={`lexkit-toolbar-button ${activeStates.isImageAlignedLeft ? 'active' : ''}`}
238
+ title="Align Left"
239
+ >
240
+ <AlignLeft size={14} />
241
+ </button>
242
+ <button
243
+ type="button"
244
+ onClick={() => commands.setImageAlignment('center')}
245
+ className={`lexkit-toolbar-button ${activeStates.isImageAlignedCenter ? 'active' : ''}`}
246
+ title="Align Center"
247
+ >
248
+ <AlignCenter size={14} />
249
+ </button>
250
+ <button
251
+ type="button"
252
+ onClick={() => commands.setImageAlignment('right')}
253
+ className={`lexkit-toolbar-button ${activeStates.isImageAlignedRight ? 'active' : ''}`}
254
+ title="Align Right"
255
+ >
256
+ <AlignRight size={14} />
257
+ </button>
258
+ <div className="bg-border mx-1 h-6 w-px" />
259
+ <button
260
+ type="button"
261
+ onClick={() => commands.setImageCaption(prompt('Enter caption:') || '')}
262
+ className="lexkit-toolbar-button"
263
+ title="Edit Caption"
264
+ >
265
+ <Type size={14} />
266
+ </button>
267
+ </>
268
+ ) : (
269
+ <>
270
+ <button
271
+ type="button"
272
+ onClick={() => commands.toggleBold()}
273
+ className={`lexkit-toolbar-button ${activeStates.bold ? 'active' : ''}`}
274
+ title="Bold"
275
+ >
276
+ <Bold size={14} />
277
+ </button>
278
+ <button
279
+ type="button"
280
+ onClick={() => commands.toggleItalic()}
281
+ className={`lexkit-toolbar-button ${activeStates.italic ? 'active' : ''}`}
282
+ title="Italic"
283
+ >
284
+ <Italic size={14} />
285
+ </button>
286
+ <button
287
+ type="button"
288
+ onClick={() => commands.toggleUnderline()}
289
+ className={`lexkit-toolbar-button ${activeStates.underline ? 'active' : ''}`}
290
+ title="Underline"
291
+ >
292
+ <Underline size={14} />
293
+ </button>
294
+ <button
295
+ type="button"
296
+ onClick={() => commands.toggleStrikethrough()}
297
+ className={`lexkit-toolbar-button ${activeStates.strikethrough ? 'active' : ''}`}
298
+ title="Strikethrough"
299
+ >
300
+ <Strikethrough size={14} />
301
+ </button>
302
+ <div className="bg-border mx-1 h-6 w-px" />
303
+ <button
304
+ type="button"
305
+ onClick={() => commands.formatText('code')}
306
+ className={`lexkit-toolbar-button ${activeStates.code ? 'active' : ''}`}
307
+ title="Inline Code"
308
+ >
309
+ <Code size={14} />
310
+ </button>
311
+ <button
312
+ type="button"
313
+ onClick={() => (activeStates.isLink ? commands.removeLink() : commands.insertLink())}
314
+ className={`lexkit-toolbar-button ${activeStates.isLink ? 'active' : ''}`}
315
+ title={activeStates.isLink ? 'Remove Link' : 'Insert Link'}
316
+ >
317
+ {activeStates.isLink ? <Unlink size={14} /> : <Link size={14} />}
318
+ </button>
319
+ <div className="bg-border mx-1 h-6 w-px" />
320
+ {hasExtension('blockFormat') && (
321
+ <>
322
+ <button
323
+ type="button"
324
+ onClick={() => commands.toggleParagraph()}
325
+ className={`lexkit-toolbar-button ${!activeStates.isH1 && !activeStates.isH2 && !activeStates.isH3 && !activeStates.isH4 && !activeStates.isH5 && !activeStates.isH6 && !activeStates.isQuote ? 'active' : ''}`}
326
+ title="Paragraph"
327
+ >
328
+ P
329
+ </button>
330
+ <button
331
+ type="button"
332
+ onClick={() => commands.toggleHeading('h1')}
333
+ className={`lexkit-toolbar-button ${activeStates.isH1 ? 'active' : ''}`}
334
+ title="Heading 1"
335
+ >
336
+ H1
337
+ </button>
338
+ <button
339
+ type="button"
340
+ onClick={() => commands.toggleHeading('h2')}
341
+ className={`lexkit-toolbar-button ${activeStates.isH2 ? 'active' : ''}`}
342
+ title="Heading 2"
343
+ >
344
+ H2
345
+ </button>
346
+ <button
347
+ type="button"
348
+ onClick={() => commands.toggleHeading('h3')}
349
+ className={`lexkit-toolbar-button ${activeStates.isH3 ? 'active' : ''}`}
350
+ title="Heading 3"
351
+ >
352
+ H3
353
+ </button>
354
+ <button
355
+ type="button"
356
+ onClick={() => commands.toggleQuote()}
357
+ className={`lexkit-toolbar-button ${activeStates.isQuote ? 'active' : ''}`}
358
+ title="Quote"
359
+ >
360
+ <Quote size={14} />
361
+ </button>
362
+ {hasExtension('code') && (
363
+ <button
364
+ type="button"
365
+ onClick={() => commands.toggleCodeBlock()}
366
+ className={`lexkit-toolbar-button ${activeStates.isInCodeBlock ? 'active' : ''}`}
367
+ title="Code Block"
368
+ >
369
+ <Terminal size={14} />
370
+ </button>
371
+ )}
372
+ <div className="bg-border mx-1 h-6 w-px" />
373
+ </>
374
+ )}
375
+ {hasExtension('list') && (
376
+ <>
377
+ <button
378
+ onClick={() => commands.toggleUnorderedList()}
379
+ className={`lexkit-toolbar-button ${activeStates.unorderedList ? 'active' : ''}`}
380
+ title="Bullet List"
381
+ >
382
+ <List size={14} />
383
+ </button>
384
+ <button
385
+ onClick={() => commands.toggleOrderedList()}
386
+ className={`lexkit-toolbar-button ${activeStates.orderedList ? 'active' : ''}`}
387
+ title="Numbered List"
388
+ >
389
+ <ListOrdered size={14} />
390
+ </button>
391
+ </>
392
+ )}
393
+ </>
394
+ )}
395
+ </div>,
396
+ document.body,
397
+ );
398
+ }
399
+
400
+ // Toolbar Component
401
+ function Toolbar({
402
+ commands,
403
+ hasExtension,
404
+ activeStates,
405
+ }: {
406
+ commands: EditorCommands;
407
+ hasExtension: (name: ExtensionNames) => boolean;
408
+ activeStates: EditorStateQueries;
409
+ }) {
410
+ const { lexical: editor } = useEditor();
411
+ const { handlers, fileInputRef } = useImageHandlers(commands, editor);
412
+ const [showImageDropdown, setShowImageDropdown] = useState(false);
413
+ const [showAlignDropdown, setShowAlignDropdown] = useState(false);
414
+ const [showTableDialog, setShowTableDialog] = useState(false);
415
+ const [tableConfig, setTableConfig] = useState<TableConfig>({
416
+ rows: 3,
417
+ columns: 3,
418
+ includeHeaders: false,
419
+ });
420
+
421
+ const blockFormatOptions = [
422
+ { value: 'p', label: 'Paragraph' },
423
+ { value: 'h1', label: 'Heading 1' },
424
+ { value: 'h2', label: 'Heading 2' },
425
+ { value: 'h3', label: 'Heading 3' },
426
+ { value: 'h4', label: 'Heading 4' },
427
+ { value: 'h5', label: 'Heading 5' },
428
+ { value: 'h6', label: 'Heading 6' },
429
+ { value: 'quote', label: 'Quote' },
430
+ ];
431
+
432
+ const currentBlockFormat = activeStates.isH1
433
+ ? 'h1'
434
+ : activeStates.isH2
435
+ ? 'h2'
436
+ : activeStates.isH3
437
+ ? 'h3'
438
+ : activeStates.isH4
439
+ ? 'h4'
440
+ : activeStates.isH5
441
+ ? 'h5'
442
+ : activeStates.isH6
443
+ ? 'h6'
444
+ : activeStates.isQuote
445
+ ? 'quote'
446
+ : 'p';
447
+
448
+ const handleBlockFormatChange = (value: string) => {
449
+ if (value === 'p') commands.toggleParagraph();
450
+ else if (value.startsWith('h'))
451
+ commands.toggleHeading(value as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6');
452
+ else if (value === 'quote') commands.toggleQuote();
453
+ };
454
+
455
+ return (
456
+ <>
457
+ <div className="lexkit-toolbar">
458
+ {/* Text Formatting */}
459
+ <div className="lexkit-toolbar-section">
460
+ <button
461
+ type="button"
462
+ onClick={() => commands.toggleBold()}
463
+ className={`lexkit-toolbar-button ${activeStates.bold ? 'active' : ''}`}
464
+ title="Bold (Ctrl+B)"
465
+ >
466
+ <Bold size={16} />
467
+ </button>
468
+ <button
469
+ type="button"
470
+ onClick={() => commands.toggleItalic()}
471
+ className={`lexkit-toolbar-button ${activeStates.italic ? 'active' : ''}`}
472
+ title="Italic (Ctrl+I)"
473
+ >
474
+ <Italic size={16} />
475
+ </button>
476
+ <button
477
+ type="button"
478
+ onClick={() => commands.toggleUnderline()}
479
+ className={`lexkit-toolbar-button ${activeStates.underline ? 'active' : ''}`}
480
+ title="Underline (Ctrl+U)"
481
+ >
482
+ <Underline size={16} />
483
+ </button>
484
+ <button
485
+ type="button"
486
+ onClick={() => commands.toggleStrikethrough()}
487
+ className={`lexkit-toolbar-button ${activeStates.strikethrough ? 'active' : ''}`}
488
+ title="Strikethrough"
489
+ >
490
+ <Strikethrough size={16} />
491
+ </button>
492
+ <button
493
+ type="button"
494
+ onClick={() => commands.formatText('code')}
495
+ className={`lexkit-toolbar-button ${activeStates.code ? 'active' : ''}`}
496
+ title="Inline Code"
497
+ >
498
+ <Code size={16} />
499
+ </button>
500
+ <button
501
+ type="button"
502
+ onClick={() => (activeStates.isLink ? commands.removeLink() : commands.insertLink())}
503
+ className={`lexkit-toolbar-button ${activeStates.isLink ? 'active' : ''}`}
504
+ title={activeStates.isLink ? 'Remove Link' : 'Insert Link'}
505
+ >
506
+ {activeStates.isLink ? <Unlink size={16} /> : <Link size={16} />}
507
+ </button>
508
+ </div>
509
+
510
+ {/* Block Format */}
511
+ {hasExtension('blockFormat') && (
512
+ <div className="lexkit-toolbar-section">
513
+ <Select
514
+ value={currentBlockFormat}
515
+ onValueChange={handleBlockFormatChange}
516
+ options={blockFormatOptions}
517
+ placeholder="Format"
518
+ />
519
+ {hasExtension('code') && (
520
+ <button
521
+ type="button"
522
+ onClick={() => commands.toggleCodeBlock()}
523
+ className={`lexkit-toolbar-button ${activeStates.isInCodeBlock ? 'active' : ''}`}
524
+ title="Code Block"
525
+ >
526
+ <Terminal size={16} />
527
+ </button>
528
+ )}
529
+ </div>
530
+ )}
531
+
532
+ {/* Lists */}
533
+ {hasExtension('list') && (
534
+ <div className="lexkit-toolbar-section">
535
+ <button
536
+ type="button"
537
+ onClick={() => commands.toggleUnorderedList()}
538
+ className={`lexkit-toolbar-button ${activeStates.unorderedList ? 'active' : ''}`}
539
+ title="Bullet List"
540
+ >
541
+ <List size={16} />
542
+ </button>
543
+ <button
544
+ type="button"
545
+ onClick={() => commands.toggleOrderedList()}
546
+ className={`lexkit-toolbar-button ${activeStates.orderedList ? 'active' : ''}`}
547
+ title="Numbered List"
548
+ >
549
+ <ListOrdered size={16} />
550
+ </button>
551
+ {(activeStates.unorderedList || activeStates.orderedList) && (
552
+ <>
553
+ <button
554
+ type="button"
555
+ onClick={() => commands.indentList()}
556
+ className="lexkit-toolbar-button"
557
+ title="Indent List"
558
+ >
559
+ <Indent size={14} />
560
+ </button>
561
+ <button
562
+ type="button"
563
+ onClick={() => commands.outdentList()}
564
+ className="lexkit-toolbar-button"
565
+ title="Outdent List"
566
+ >
567
+ <Outdent size={14} />
568
+ </button>
569
+ </>
570
+ )}
571
+ </div>
572
+ )}
573
+
574
+ {/* Horizontal Rule */}
575
+ {hasExtension('horizontalRule') && (
576
+ <div className="lexkit-toolbar-section">
577
+ <button
578
+ type="button"
579
+ onClick={() => commands.insertHorizontalRule()}
580
+ className="lexkit-toolbar-button"
581
+ title="Insert Horizontal Rule"
582
+ >
583
+ <Minus size={16} />
584
+ </button>
585
+ </div>
586
+ )}
587
+
588
+ {/* Table */}
589
+ {hasExtension('table') && (
590
+ <div className="lexkit-toolbar-section">
591
+ <button
592
+ type="button"
593
+ onClick={() => setShowTableDialog(true)}
594
+ className="lexkit-toolbar-button"
595
+ title="Insert Table (Ctrl+Shift+T)"
596
+ >
597
+ <TableIcon size={16} />
598
+ </button>
599
+ </div>
600
+ )}
601
+
602
+ {/* History */}
603
+ {hasExtension('history') && (
604
+ <div className="lexkit-toolbar-section">
605
+ <button
606
+ type="button"
607
+ onClick={() => commands.undo()}
608
+ disabled={!activeStates.canUndo}
609
+ className="lexkit-toolbar-button"
610
+ title="Undo (Ctrl+Z)"
611
+ >
612
+ <Undo size={16} />
613
+ </button>
614
+ <button
615
+ type="button"
616
+ onClick={() => commands.redo()}
617
+ disabled={!activeStates.canRedo}
618
+ className="lexkit-toolbar-button"
619
+ title="Redo (Ctrl+Y)"
620
+ >
621
+ <Redo size={16} />
622
+ </button>
623
+ </div>
624
+ )}
625
+ </div>
626
+
627
+ {/* Table Dialog */}
628
+ <Dialog
629
+ isOpen={showTableDialog}
630
+ onClose={() => setShowTableDialog(false)}
631
+ title="Insert Table"
632
+ >
633
+ <div className="lexkit-table-dialog">
634
+ <div className="lexkit-form-group">
635
+ <label htmlFor="table-rows">Rows:</label>
636
+ <input
637
+ id="table-rows"
638
+ type="number"
639
+ min="1"
640
+ max="20"
641
+ value={tableConfig.rows}
642
+ onChange={(e) =>
643
+ setTableConfig((prev) => ({ ...prev, rows: parseInt(e.target.value) || 1 }))
644
+ }
645
+ className="lexkit-input"
646
+ />
647
+ </div>
648
+ <div className="lexkit-form-group">
649
+ <label htmlFor="table-columns">Columns:</label>
650
+ <input
651
+ id="table-columns"
652
+ type="number"
653
+ min="1"
654
+ max="20"
655
+ value={tableConfig.columns}
656
+ onChange={(e) =>
657
+ setTableConfig((prev) => ({ ...prev, columns: parseInt(e.target.value) || 1 }))
658
+ }
659
+ className="lexkit-input"
660
+ />
661
+ </div>
662
+ <div className="lexkit-form-group">
663
+ <label className="lexkit-checkbox-label">
664
+ <input
665
+ type="checkbox"
666
+ checked={tableConfig.includeHeaders || false}
667
+ onChange={(e) =>
668
+ setTableConfig((prev) => ({ ...prev, includeHeaders: e.target.checked }))
669
+ }
670
+ className="lexkit-checkbox"
671
+ />
672
+ Include headers
673
+ </label>
674
+ </div>
675
+ <div className="lexkit-dialog-actions">
676
+ <button
677
+ type="button"
678
+ onClick={() => setShowTableDialog(false)}
679
+ className="lexkit-button-secondary"
680
+ >
681
+ Cancel
682
+ </button>
683
+ <button
684
+ type="button"
685
+ onClick={() => {
686
+ commands.insertTable(tableConfig);
687
+ setShowTableDialog(false);
688
+ }}
689
+ className="lexkit-button-primary"
690
+ >
691
+ Insert Table
692
+ </button>
693
+ </div>
694
+ </div>
695
+ </Dialog>
696
+ </>
697
+ );
698
+ }
699
+
700
+ // Error Boundary
701
+ function ErrorBoundary({ children }: { children: React.ReactNode }) {
702
+ return <>{children}</>;
703
+ }
704
+
705
+ // Editor Content Component
706
+ function EditorContent({
707
+ className,
708
+ isDark,
709
+ toggleTheme,
710
+ onReady,
711
+ }: {
712
+ className?: string;
713
+ isDark: boolean;
714
+ toggleTheme: () => void;
715
+ onReady?: (methods: DefaultTemplateRef) => void;
716
+ }) {
717
+ const { commands, hasExtension, activeStates, lexical: editor } = useEditor();
718
+ const commandsRef = useRef<EditorCommands>(commands);
719
+ const readyRef = useRef(false);
720
+
721
+ useEffect(() => {
722
+ commandsRef.current = commands;
723
+ }, [commands]);
724
+
725
+ const methods = useMemo<DefaultTemplateRef>(
726
+ () => ({
727
+ injectMarkdown: (content: string) => {
728
+ setTimeout(() => {
729
+ if (editor) {
730
+ editor.update(() => {
731
+ commandsRef.current.importFromMarkdown(content, {
732
+ immediate: true,
733
+ preventFocus: true,
734
+ });
735
+ });
736
+ }
737
+ }, 100); // Small delay to ensure editor is ready
738
+ },
739
+ injectHTML: (content: string) => {
740
+ setTimeout(() => {
741
+ if (editor) {
742
+ editor.update(() => {
743
+ commandsRef.current.importFromHTML(content, { preventFocus: true });
744
+ });
745
+ }
746
+ }, 100);
747
+ },
748
+ insertText: (text: string) => {
749
+ if (editor) {
750
+ editor.update(() => {
751
+ const selection = $getSelection();
752
+ if ($isRangeSelection(selection)) {
753
+ selection.insertText(text);
754
+ } else {
755
+ // Si pas de sélection, insérer à la position du curseur
756
+ const textNode = $createTextNode(text);
757
+ $insertNodes([textNode]);
758
+ }
759
+ });
760
+ }
761
+ },
762
+ getMarkdown: () => commandsRef.current.exportToMarkdown(),
763
+ getHTML: () => commandsRef.current.exportToHTML(),
764
+ }),
765
+ [editor],
766
+ );
767
+
768
+ useEffect(() => {
769
+ if (!editor || !commands) return;
770
+
771
+ const paletteCommands = commandsToCommandPaletteItems(commands);
772
+ paletteCommands.forEach((cmd) => commands.registerCommand(cmd));
773
+
774
+ const originalShowCommand = commands.showCommandPalette;
775
+
776
+ const unregisterShortcuts = registerKeyboardShortcuts(commands, document.body);
777
+
778
+ if (!readyRef.current) {
779
+ readyRef.current = true;
780
+ onReady?.(methods);
781
+ }
782
+
783
+ return () => {
784
+ unregisterShortcuts();
785
+ (commands as any).showCommandPalette = originalShowCommand;
786
+ };
787
+ }, [editor, commands, onReady, methods]);
788
+
789
+ return (
790
+ <>
791
+ <div className="lexkit-editor-header">
792
+ <Toolbar commands={commands} hasExtension={hasExtension} activeStates={activeStates} />
793
+ </div>
794
+ <div className="lexkit-editor">
795
+ <div className="flex flex-1 flex-col" style={{ display: 'flex' }}>
796
+ <RichTextPlugin
797
+ contentEditable={<ContentEditable className="lexkit-content-editable" />}
798
+ placeholder={<div className="lexkit-placeholder">Écrivez...</div>}
799
+ ErrorBoundary={ErrorBoundary}
800
+ />
801
+ <FloatingToolbarRenderer />
802
+ </div>
803
+ </div>
804
+ </>
805
+ );
806
+ }
807
+
808
+ // Main DefaultTemplate Component
809
+ interface DefaultTemplateProps {
810
+ className?: string;
811
+ onReady?: (methods: DefaultTemplateRef) => void;
812
+ }
813
+
814
+ export const Editor = forwardRef<DefaultTemplateRef, DefaultTemplateProps>(
815
+ ({ className, onReady }, ref) => {
816
+ const [editorTheme, setEditorTheme] = useState<'light' | 'dark'>('light');
817
+
818
+ const isDark = editorTheme === 'dark';
819
+
820
+ useEffect(() => {
821
+ imageExtension.configure({
822
+ uploadHandler: async (file: File) => URL.createObjectURL(file),
823
+ defaultAlignment: 'center',
824
+ resizable: true,
825
+ pasteListener: { insert: true, replace: true },
826
+ debug: false,
827
+ });
828
+ }, []);
829
+
830
+ const toggleTheme = () => setEditorTheme(isDark ? 'light' : 'dark');
831
+
832
+ // Expose methods via ref
833
+ const [methods, setMethods] = useState<DefaultTemplateRef | null>(null);
834
+ React.useImperativeHandle(ref, () => methods as DefaultTemplateRef, [methods]);
835
+
836
+ const handleReady = (m: DefaultTemplateRef) => {
837
+ setMethods(m);
838
+ onReady?.(m);
839
+ };
840
+
841
+ return (
842
+ <div className={`lexkit-editor-wrapper ${className || ''}`} data-editor-theme={editorTheme}>
843
+ <Provider extensions={extensions} config={{ theme: defaultTheme }}>
844
+ <EditorContent
845
+ className={className}
846
+ isDark={isDark}
847
+ toggleTheme={toggleTheme}
848
+ onReady={handleReady}
849
+ />
850
+ </Provider>
851
+ </div>
852
+ );
853
+ },
854
+ );
855
+
856
+ Editor.displayName = 'Editor';