create-crm-tmp 1.1.3 → 2.1.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 (276) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/.prettierignore +2 -0
  4. package/template/README.md +230 -115
  5. package/template/components.json +22 -0
  6. package/template/eslint.config.mjs +13 -0
  7. package/template/exemple-contacts.csv +54 -0
  8. package/template/next.config.ts +41 -1
  9. package/template/package.json +63 -15
  10. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  11. package/template/prisma/schema.prisma +311 -67
  12. package/template/src/app/(auth)/invite/[token]/page.tsx +28 -29
  13. package/template/src/app/(auth)/layout.tsx +1 -1
  14. package/template/src/app/(auth)/reset-password/complete/page.tsx +21 -27
  15. package/template/src/app/(auth)/reset-password/page.tsx +14 -10
  16. package/template/src/app/(auth)/reset-password/verify/page.tsx +14 -10
  17. package/template/src/app/(auth)/signin/page.tsx +34 -23
  18. package/template/src/app/(dashboard)/agenda/page.tsx +3655 -2357
  19. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  20. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +609 -338
  21. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  22. package/template/src/app/(dashboard)/automatisation/page.tsx +463 -186
  23. package/template/src/app/(dashboard)/closing/page.tsx +517 -469
  24. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6151 -4210
  25. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1702 -0
  26. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  27. package/template/src/app/(dashboard)/contacts/page.tsx +4124 -2130
  28. package/template/src/app/(dashboard)/dashboard/page.tsx +119 -105
  29. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  30. package/template/src/app/(dashboard)/error.tsx +37 -0
  31. package/template/src/app/(dashboard)/layout.tsx +6 -2
  32. package/template/src/app/(dashboard)/loading.tsx +5 -0
  33. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  34. package/template/src/app/(dashboard)/settings/page.tsx +1773 -3362
  35. package/template/src/app/(dashboard)/templates/page.tsx +504 -303
  36. package/template/src/app/(dashboard)/users/list/page.tsx +364 -355
  37. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  38. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  39. package/template/src/app/(dashboard)/users/roles/page.tsx +169 -140
  40. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  41. package/template/src/app/api/audit-logs/route.ts +1 -1
  42. package/template/src/app/api/auth/check-active/route.ts +3 -2
  43. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  44. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  45. package/template/src/app/api/auth/google/route.ts +2 -1
  46. package/template/src/app/api/auth/google/status/route.ts +7 -31
  47. package/template/src/app/api/companies/[id]/activities/route.ts +129 -0
  48. package/template/src/app/api/companies/[id]/route.ts +194 -0
  49. package/template/src/app/api/companies/export/route.ts +206 -0
  50. package/template/src/app/api/companies/route.ts +196 -0
  51. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  52. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  53. package/template/src/app/api/contact-views/route.ts +146 -0
  54. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +55 -0
  55. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +20 -48
  56. package/template/src/app/api/contacts/[id]/files/route.ts +125 -186
  57. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  58. package/template/src/app/api/contacts/[id]/interactions/route.ts +45 -8
  59. package/template/src/app/api/contacts/[id]/kyc/route.ts +81 -0
  60. package/template/src/app/api/contacts/[id]/meet/route.ts +55 -29
  61. package/template/src/app/api/contacts/[id]/route.ts +184 -21
  62. package/template/src/app/api/contacts/[id]/send-email/route.ts +33 -11
  63. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +67 -0
  64. package/template/src/app/api/contacts/export/route.ts +22 -31
  65. package/template/src/app/api/contacts/import/route.ts +77 -44
  66. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  67. package/template/src/app/api/contacts/origins/route.ts +63 -0
  68. package/template/src/app/api/contacts/route.ts +322 -57
  69. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  70. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  71. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -3
  72. package/template/src/app/api/dashboard/widgets/route.ts +19 -19
  73. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  74. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  75. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  76. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  77. package/template/src/app/api/integrations/google-sheet/sync/route.ts +28 -542
  78. package/template/src/app/api/invite/complete/route.ts +20 -23
  79. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  80. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  81. package/template/src/app/api/reminders/clear/route.ts +120 -0
  82. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  83. package/template/src/app/api/reminders/route.ts +165 -39
  84. package/template/src/app/api/reminders/state/route.ts +164 -0
  85. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  86. package/template/src/app/api/reset-password/request/route.ts +1 -1
  87. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  88. package/template/src/app/api/send/route.ts +25 -47
  89. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  90. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  91. package/template/src/app/api/settings/company/route.ts +19 -26
  92. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  93. package/template/src/app/api/settings/google-ads/route.ts +34 -23
  94. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  95. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  96. package/template/src/app/api/settings/google-sheet/[id]/route.ts +48 -23
  97. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +56 -32
  98. package/template/src/app/api/settings/google-sheet/preview/route.ts +110 -0
  99. package/template/src/app/api/settings/google-sheet/route.ts +34 -23
  100. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  101. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  102. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -24
  103. package/template/src/app/api/settings/meta-leads/route.ts +34 -25
  104. package/template/src/app/api/settings/smtp/route.ts +53 -6
  105. package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
  106. package/template/src/app/api/settings/statuses/route.ts +24 -22
  107. package/template/src/app/api/statuses/route.ts +2 -5
  108. package/template/src/app/api/tasks/[id]/attendees/route.ts +36 -13
  109. package/template/src/app/api/tasks/[id]/route.ts +357 -145
  110. package/template/src/app/api/tasks/meet/route.ts +37 -26
  111. package/template/src/app/api/tasks/route.ts +201 -96
  112. package/template/src/app/api/templates/[id]/route.ts +22 -13
  113. package/template/src/app/api/templates/route.ts +22 -5
  114. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  115. package/template/src/app/api/users/[id]/route.ts +22 -16
  116. package/template/src/app/api/users/commercials/route.ts +38 -0
  117. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  118. package/template/src/app/api/users/list/route.ts +57 -19
  119. package/template/src/app/api/users/route.ts +89 -34
  120. package/template/src/app/api/webhooks/google-ads/route.ts +40 -1
  121. package/template/src/app/api/webhooks/meta-leads/route.ts +38 -1
  122. package/template/src/app/api/workflows/[id]/route.ts +29 -6
  123. package/template/src/app/api/workflows/process/route.ts +505 -170
  124. package/template/src/app/api/workflows/route.ts +42 -4
  125. package/template/src/app/globals.css +512 -32
  126. package/template/src/app/layout.tsx +28 -9
  127. package/template/src/app/page.tsx +37 -7
  128. package/template/src/components/address-autocomplete.tsx +233 -0
  129. package/template/src/components/config-error-alert.tsx +46 -0
  130. package/template/src/components/contacts/filter-bar.tsx +190 -0
  131. package/template/src/components/contacts/filter-builder.tsx +574 -0
  132. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  133. package/template/src/components/contacts/views-tab-bar.tsx +449 -0
  134. package/template/src/components/dashboard/activity-chart.tsx +6 -1
  135. package/template/src/components/dashboard/add-widget-dialog.tsx +13 -17
  136. package/template/src/components/dashboard/color-picker.tsx +7 -8
  137. package/template/src/components/dashboard/recent-activity.tsx +2 -5
  138. package/template/src/components/dashboard/stat-card.tsx +1 -3
  139. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -1
  140. package/template/src/components/dashboard/top-contacts-list.tsx +7 -13
  141. package/template/src/components/dashboard/upcoming-tasks-list.tsx +2 -5
  142. package/template/src/components/dashboard/widget-wrapper.tsx +3 -6
  143. package/template/src/components/date-picker.tsx +399 -0
  144. package/template/src/components/editor/upload-editor-image.ts +42 -0
  145. package/template/src/components/editor.tsx +188 -35
  146. package/template/src/components/email-template.tsx +4 -2
  147. package/template/src/components/global-search.tsx +360 -0
  148. package/template/src/components/header.tsx +200 -107
  149. package/template/src/components/inactive-account-guard.tsx +58 -0
  150. package/template/src/components/integration-notifications-listener.tsx +12 -0
  151. package/template/src/components/invitation-email-template.tsx +4 -2
  152. package/template/src/components/lazy-editor.tsx +11 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  154. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  155. package/template/src/components/meet-update-email-template.tsx +10 -3
  156. package/template/src/components/page-header.tsx +19 -15
  157. package/template/src/components/protected-page.tsx +94 -0
  158. package/template/src/components/reset-password-email-template.tsx +4 -2
  159. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  160. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  161. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  162. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  163. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  164. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  165. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  166. package/template/src/components/sidebar.tsx +117 -100
  167. package/template/src/components/skeleton.tsx +128 -45
  168. package/template/src/components/ui/accordion.tsx +64 -0
  169. package/template/src/components/ui/alert-dialog.tsx +139 -0
  170. package/template/src/components/ui/button.tsx +71 -0
  171. package/template/src/components/ui/components.tsx +1 -1
  172. package/template/src/components/ui/date-picker.tsx +422 -0
  173. package/template/src/components/ui/datetime-picker.tsx +338 -0
  174. package/template/src/components/ui/status-select.tsx +271 -0
  175. package/template/src/components/ui/tooltip.tsx +37 -0
  176. package/template/src/components/view-as-banner.tsx +1 -1
  177. package/template/src/components/view-as-modal.tsx +30 -19
  178. package/template/src/config/nav-pages.ts +108 -0
  179. package/template/src/contexts/app-toast-context.tsx +362 -0
  180. package/template/src/contexts/dashboard-theme-context.tsx +2 -7
  181. package/template/src/contexts/sidebar-context.tsx +27 -53
  182. package/template/src/contexts/task-reminder-context.tsx +134 -160
  183. package/template/src/contexts/view-as-context.tsx +32 -10
  184. package/template/src/hooks/use-alert.tsx +65 -0
  185. package/template/src/hooks/use-confirm.tsx +87 -0
  186. package/template/src/hooks/use-contact-views.ts +140 -0
  187. package/template/src/hooks/use-contacts.ts +69 -0
  188. package/template/src/hooks/use-fetch.ts +17 -0
  189. package/template/src/hooks/use-focus-trap.ts +73 -0
  190. package/template/src/hooks/use-statuses.ts +22 -0
  191. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  192. package/template/src/lib/address-api.ts +155 -0
  193. package/template/src/lib/auth.ts +8 -1
  194. package/template/src/lib/cache.ts +73 -0
  195. package/template/src/lib/check-permission.ts +12 -177
  196. package/template/src/lib/config-links.ts +14 -0
  197. package/template/src/lib/contact-duplicate.ts +79 -61
  198. package/template/src/lib/contact-interactions.ts +24 -22
  199. package/template/src/lib/contact-view-filters.ts +301 -0
  200. package/template/src/lib/contacts-list-url.ts +190 -0
  201. package/template/src/lib/dashboard-stats.ts +282 -0
  202. package/template/src/lib/dashboard-themes.ts +0 -5
  203. package/template/src/lib/date-utils.ts +176 -0
  204. package/template/src/lib/default-widgets.ts +0 -2
  205. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  206. package/template/src/lib/editor-image-limits.ts +19 -0
  207. package/template/src/lib/email-html-sanitize.ts +19 -0
  208. package/template/src/lib/encryption.ts +9 -6
  209. package/template/src/lib/fr-geography.ts +192 -0
  210. package/template/src/lib/get-auth-user.ts +25 -0
  211. package/template/src/lib/google-calendar-agenda.ts +201 -0
  212. package/template/src/lib/google-calendar.ts +309 -17
  213. package/template/src/lib/google-fetch.ts +63 -0
  214. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  215. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  216. package/template/src/lib/integration-import-log.ts +21 -0
  217. package/template/src/lib/local-storage.ts +34 -0
  218. package/template/src/lib/permissions.ts +268 -40
  219. package/template/src/lib/prisma.ts +15 -12
  220. package/template/src/lib/qstash.ts +65 -0
  221. package/template/src/lib/reminder-state-server.ts +80 -0
  222. package/template/src/lib/reminder-state.ts +29 -0
  223. package/template/src/lib/roles.ts +12 -15
  224. package/template/src/lib/supabase-storage.ts +113 -0
  225. package/template/src/lib/template-variables.ts +204 -29
  226. package/template/src/lib/utils.ts +71 -11
  227. package/template/src/lib/widget-registry.ts +0 -4
  228. package/template/src/lib/workflow-executor.ts +391 -228
  229. package/template/src/proxy.ts +35 -73
  230. package/template/src/types/contact-views.ts +351 -0
  231. package/template/vercel.json +5 -0
  232. package/template/WORKFLOWS_CRON.md +0 -185
  233. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  234. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  235. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  236. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  237. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  238. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  239. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  240. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  241. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  242. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  243. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  244. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  245. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  246. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  247. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  248. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  249. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  250. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  251. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  252. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  253. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  254. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  255. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  256. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  257. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  258. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  259. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  260. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  261. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  262. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  263. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  264. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  265. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  266. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  267. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  268. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  269. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  270. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  271. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  272. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  273. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  274. package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
  275. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  276. package/template/src/lib/google-drive.ts +0 -380
@@ -64,9 +64,6 @@ import {
64
64
  Code,
65
65
  Terminal,
66
66
  Table as TableIcon,
67
- FileCode,
68
- Eye,
69
- Pencil,
70
67
  Type,
71
68
  Quote,
72
69
  Indent,
@@ -74,8 +71,16 @@ import {
74
71
  } from 'lucide-react';
75
72
  import { createPortal } from 'react-dom';
76
73
  import { commandsToCommandPaletteItems, registerKeyboardShortcuts } from './ui/commands';
77
- import { Select, Dropdown, Dialog } from './ui/components';
74
+ import { Select, Dialog } from './ui/components';
78
75
  import { defaultTheme } from './ui/theme';
76
+ import { useAppToast } from '@/contexts/app-toast-context';
77
+ import { devToast } from '@/lib/utils';
78
+ import { assertImageFileWithinLimit } from '@/lib/editor-image-limits';
79
+ import { uploadEditorImage } from './editor/upload-editor-image';
80
+ import {
81
+ applyOrderedImageLayoutAfterImport,
82
+ mergeImgDimensionAttrsIntoStyle,
83
+ } from '@/lib/editor-html-image-dimensions';
79
84
 
80
85
  type TableConfig = {
81
86
  rows?: number;
@@ -83,8 +88,36 @@ type TableConfig = {
83
88
  includeHeaders?: boolean;
84
89
  };
85
90
 
91
+ function fileToDataUrl(file: File): Promise<string> {
92
+ return new Promise((resolve, reject) => {
93
+ const reader = new FileReader();
94
+ reader.onload = () => {
95
+ const result = reader.result;
96
+ if (typeof result === 'string') {
97
+ resolve(result);
98
+ } else {
99
+ reject(new Error('Impossible de convertir le fichier en data URL'));
100
+ }
101
+ };
102
+ reader.onerror = () =>
103
+ reject(reader.error ?? new Error('Erreur de lecture du fichier image'));
104
+ reader.readAsDataURL(file);
105
+ });
106
+ }
107
+
108
+ async function imageFileToDataUrl(file: File, maxImageBytes?: number): Promise<string> {
109
+ if (maxImageBytes !== undefined) {
110
+ assertImageFileWithinLimit(file, maxImageBytes);
111
+ }
112
+ return fileToDataUrl(file);
113
+ }
114
+
86
115
  // Create markdown extension instance
116
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
117
+ // @ts-ignore - Lexical duplicate types between packages
87
118
  const markdownExt = new MarkdownExtension().configure({
119
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
120
+ // @ts-ignore - Lexical duplicate types between packages
88
121
  customTransformers: ALL_MARKDOWN_TRANSFORMERS,
89
122
  });
90
123
 
@@ -139,16 +172,21 @@ export interface DefaultTemplateRef {
139
172
  }
140
173
 
141
174
  // Hook for image handling logic
142
- function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null) {
175
+ function useImageHandlers(
176
+ commands: EditorCommands,
177
+ editor: LexicalEditor | null,
178
+ maxImageBytes?: number,
179
+ ) {
143
180
  const fileInputRef = useRef<HTMLInputElement>(null);
181
+ const toast = useAppToast();
144
182
 
145
183
  const handlers = useMemo(
146
184
  () => ({
147
185
  insertFromUrl: () => {
148
- const src = prompt('Enter image URL:');
186
+ const src = prompt("URL de l'image (https://…) :");
149
187
  if (!src) return;
150
- const alt = prompt('Enter alt text:') || '';
151
- const caption = prompt('Enter caption (optional):') || undefined;
188
+ const alt = prompt('Texte alternatif (accessibilité) :') || '';
189
+ const caption = prompt('Légende (optionnel) :') || undefined;
152
190
  commands.insertImage({ src, alt, caption });
153
191
  },
154
192
  insertFromFile: () => fileInputRef.current?.click(),
@@ -160,11 +198,19 @@ function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null
160
198
  try {
161
199
  src = await imageExtension.config.uploadHandler(file);
162
200
  } catch (error) {
163
- alert('Failed to upload image');
201
+ // Toast déjà affiché par uploadHandler (signature / collage)
202
+ console.error('Failed to upload image:', error);
203
+ e.target.value = '';
164
204
  return;
165
205
  }
166
206
  } else {
167
- src = URL.createObjectURL(file);
207
+ try {
208
+ src = await imageFileToDataUrl(file, maxImageBytes);
209
+ } catch (error) {
210
+ toast.error(devToast("Impossible d'ajouter cette image.", error));
211
+ e.target.value = '';
212
+ return;
213
+ }
168
214
  }
169
215
  commands.insertImage({ src, alt: file.name, file });
170
216
  e.target.value = '';
@@ -177,7 +223,7 @@ function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null
177
223
  commands.setImageCaption(newCaption);
178
224
  },
179
225
  }),
180
- [commands],
226
+ [commands, maxImageBytes, toast],
181
227
  );
182
228
 
183
229
  return { handlers, fileInputRef };
@@ -264,6 +310,10 @@ function FloatingToolbarRenderer() {
264
310
  >
265
311
  <Type size={14} />
266
312
  </button>
313
+ <div className="bg-border mx-1 h-6 w-px" />
314
+ <span className="text-muted-foreground px-1.5 text-[11px] leading-6 whitespace-nowrap">
315
+ Shift + glisser pour garder le ratio
316
+ </span>
267
317
  </>
268
318
  ) : (
269
319
  <>
@@ -402,15 +452,15 @@ function Toolbar({
402
452
  commands,
403
453
  hasExtension,
404
454
  activeStates,
455
+ maxImageBytes,
405
456
  }: {
406
457
  commands: EditorCommands;
407
458
  hasExtension: (name: ExtensionNames) => boolean;
408
459
  activeStates: EditorStateQueries;
460
+ maxImageBytes?: number;
409
461
  }) {
410
462
  const { lexical: editor } = useEditor();
411
- const { handlers, fileInputRef } = useImageHandlers(commands, editor);
412
- const [showImageDropdown, setShowImageDropdown] = useState(false);
413
- const [showAlignDropdown, setShowAlignDropdown] = useState(false);
463
+ const { handlers, fileInputRef } = useImageHandlers(commands, editor, maxImageBytes);
414
464
  const [showTableDialog, setShowTableDialog] = useState(false);
415
465
  const [tableConfig, setTableConfig] = useState<TableConfig>({
416
466
  rows: 3,
@@ -585,6 +635,35 @@ function Toolbar({
585
635
  </div>
586
636
  )}
587
637
 
638
+ {/* Image : fichier local ou URL (handlers étaient définis mais non exposés dans la barre) */}
639
+ {hasExtension('image') && (
640
+ <div className="lexkit-toolbar-section">
641
+ <input
642
+ ref={fileInputRef}
643
+ type="file"
644
+ accept="image/*"
645
+ className="hidden"
646
+ onChange={handlers.handleUpload}
647
+ />
648
+ <button
649
+ type="button"
650
+ onClick={() => handlers.insertFromFile()}
651
+ className="lexkit-toolbar-button"
652
+ title="Insérer une image depuis un fichier"
653
+ >
654
+ <Upload size={16} />
655
+ </button>
656
+ <button
657
+ type="button"
658
+ onClick={() => handlers.insertFromUrl()}
659
+ className="lexkit-toolbar-button"
660
+ title="Insérer une image depuis une URL"
661
+ >
662
+ <ImageIcon size={16} />
663
+ </button>
664
+ </div>
665
+ )}
666
+
588
667
  {/* Table */}
589
668
  {hasExtension('table') && (
590
669
  <div className="lexkit-toolbar-section">
@@ -635,13 +714,18 @@ function Toolbar({
635
714
  <label htmlFor="table-rows">Rows:</label>
636
715
  <input
637
716
  id="table-rows"
638
- type="number"
639
- min="1"
640
- max="20"
717
+ type="text"
718
+ inputMode="numeric"
641
719
  value={tableConfig.rows}
642
- onChange={(e) =>
643
- setTableConfig((prev) => ({ ...prev, rows: parseInt(e.target.value) || 1 }))
644
- }
720
+ onChange={(e) => {
721
+ const value = e.target.value;
722
+ if (value === '' || /^\d+$/.test(value)) {
723
+ const num = parseInt(value) || 1;
724
+ if (num >= 1 && num <= 20) {
725
+ setTableConfig((prev) => ({ ...prev, rows: num }));
726
+ }
727
+ }
728
+ }}
645
729
  className="lexkit-input"
646
730
  />
647
731
  </div>
@@ -649,13 +733,18 @@ function Toolbar({
649
733
  <label htmlFor="table-columns">Columns:</label>
650
734
  <input
651
735
  id="table-columns"
652
- type="number"
653
- min="1"
654
- max="20"
736
+ type="text"
737
+ inputMode="numeric"
655
738
  value={tableConfig.columns}
656
- onChange={(e) =>
657
- setTableConfig((prev) => ({ ...prev, columns: parseInt(e.target.value) || 1 }))
658
- }
739
+ onChange={(e) => {
740
+ const value = e.target.value;
741
+ if (value === '' || /^\d+$/.test(value)) {
742
+ const num = parseInt(value) || 1;
743
+ if (num >= 1 && num <= 20) {
744
+ setTableConfig((prev) => ({ ...prev, columns: num }));
745
+ }
746
+ }
747
+ }}
659
748
  className="lexkit-input"
660
749
  />
661
750
  </div>
@@ -708,11 +797,13 @@ function EditorContent({
708
797
  isDark,
709
798
  toggleTheme,
710
799
  onReady,
800
+ maxImageBytes,
711
801
  }: {
712
802
  className?: string;
713
803
  isDark: boolean;
714
804
  toggleTheme: () => void;
715
805
  onReady?: (methods: DefaultTemplateRef) => void;
806
+ maxImageBytes?: number;
716
807
  }) {
717
808
  const { commands, hasExtension, activeStates, lexical: editor } = useEditor();
718
809
  const commandsRef = useRef<EditorCommands>(commands);
@@ -738,10 +829,46 @@ function EditorContent({
738
829
  },
739
830
  injectHTML: (content: string) => {
740
831
  setTimeout(() => {
741
- if (editor) {
742
- editor.update(() => {
743
- commandsRef.current.importFromHTML(content, { preventFocus: true });
744
- });
832
+ if (!editor) return;
833
+ const safeContent = typeof content === 'string' ? content.trim() : '';
834
+ const containsHtmlTag = /<[^>]+>/.test(safeContent);
835
+ const plainParagraph = `<p>${safeContent
836
+ .replaceAll('&', '&amp;')
837
+ .replaceAll('<', '&lt;')
838
+ .replaceAll('>', '&gt;')
839
+ .replaceAll('"', '&quot;')
840
+ .replaceAll("'", '&#39;')
841
+ .replaceAll('\n', '<br />')}</p>`;
842
+
843
+ const contentForImport = !safeContent
844
+ ? '<p></p>'
845
+ : containsHtmlTag
846
+ ? mergeImgDimensionAttrsIntoStyle(safeContent)
847
+ : plainParagraph;
848
+
849
+ const afterImport = () => {
850
+ if (!containsHtmlTag || !contentForImport.includes('<img')) return;
851
+ applyOrderedImageLayoutAfterImport(editor, contentForImport);
852
+ };
853
+
854
+ try {
855
+ const result = commandsRef.current.importFromHTML(contentForImport, {
856
+ preventFocus: true,
857
+ }) as Promise<void> | void;
858
+
859
+ if (result && typeof (result as Promise<void>).then === 'function') {
860
+ (result as Promise<void>)
861
+ .then(afterImport)
862
+ .catch((error) => {
863
+ console.error('Erreur import HTML éditeur, fallback contenu vide:', error);
864
+ void commandsRef.current.importFromHTML('<p></p>', { preventFocus: true });
865
+ });
866
+ } else {
867
+ afterImport();
868
+ }
869
+ } catch (error) {
870
+ console.error('Erreur injectHTML éditeur:', error);
871
+ void commandsRef.current.importFromHTML('<p></p>', { preventFocus: true });
745
872
  }
746
873
  }, 100);
747
874
  },
@@ -760,7 +887,14 @@ function EditorContent({
760
887
  }
761
888
  },
762
889
  getMarkdown: () => commandsRef.current.exportToMarkdown(),
763
- getHTML: () => commandsRef.current.exportToHTML(),
890
+ getHTML: () => {
891
+ try {
892
+ const raw = commandsRef.current.exportToHTML() || '';
893
+ return mergeImgDimensionAttrsIntoStyle(raw);
894
+ } catch {
895
+ return commandsRef.current.exportToHTML() || '';
896
+ }
897
+ },
764
898
  }),
765
899
  [editor],
766
900
  );
@@ -789,7 +923,12 @@ function EditorContent({
789
923
  return (
790
924
  <>
791
925
  <div className="lexkit-editor-header">
792
- <Toolbar commands={commands} hasExtension={hasExtension} activeStates={activeStates} />
926
+ <Toolbar
927
+ commands={commands}
928
+ hasExtension={hasExtension}
929
+ activeStates={activeStates}
930
+ maxImageBytes={maxImageBytes}
931
+ />
793
932
  </div>
794
933
  <div className="lexkit-editor">
795
934
  <div className="flex flex-1 flex-col" style={{ display: 'flex' }}>
@@ -809,23 +948,36 @@ function EditorContent({
809
948
  interface DefaultTemplateProps {
810
949
  className?: string;
811
950
  onReady?: (methods: DefaultTemplateRef) => void;
951
+ /** Si défini, refuse les images plus lourdes (fichier + collage). */
952
+ maxImageBytes?: number;
812
953
  }
813
954
 
814
955
  export const Editor = forwardRef<DefaultTemplateRef, DefaultTemplateProps>(
815
- ({ className, onReady }, ref) => {
956
+ ({ className, onReady, maxImageBytes }, ref) => {
816
957
  const [editorTheme, setEditorTheme] = useState<'light' | 'dark'>('light');
958
+ const toast = useAppToast();
817
959
 
818
960
  const isDark = editorTheme === 'dark';
819
961
 
820
962
  useEffect(() => {
821
963
  imageExtension.configure({
822
- uploadHandler: async (file: File) => URL.createObjectURL(file),
964
+ uploadHandler: async (file: File) => {
965
+ if (maxImageBytes !== undefined) {
966
+ assertImageFileWithinLimit(file, maxImageBytes);
967
+ }
968
+ try {
969
+ return await uploadEditorImage(file);
970
+ } catch {
971
+ // Fallback: data URL (base64)
972
+ return imageFileToDataUrl(file, maxImageBytes);
973
+ }
974
+ },
823
975
  defaultAlignment: 'center',
824
976
  resizable: true,
825
977
  pasteListener: { insert: true, replace: true },
826
978
  debug: false,
827
979
  });
828
- }, []);
980
+ }, [maxImageBytes, toast]);
829
981
 
830
982
  const toggleTheme = () => setEditorTheme(isDark ? 'light' : 'dark');
831
983
 
@@ -846,6 +998,7 @@ export const Editor = forwardRef<DefaultTemplateRef, DefaultTemplateProps>(
846
998
  isDark={isDark}
847
999
  toggleTheme={toggleTheme}
848
1000
  onReady={handleReady}
1001
+ maxImageBytes={maxImageBytes}
849
1002
  />
850
1003
  </Provider>
851
1004
  </div>
@@ -1,3 +1,5 @@
1
+ import { sanitizeEmailHtml } from '@/lib/email-html-sanitize';
2
+
1
3
  interface EmailTemplateProps {
2
4
  firstName: string;
3
5
  signature?: string | null;
@@ -7,7 +9,7 @@ export function EmailTemplate({ firstName, signature }: EmailTemplateProps) {
7
9
  return (
8
10
  <div
9
11
  style={{
10
- fontFamily: 'Arial, sans-serif',
12
+ fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
11
13
  padding: '20px',
12
14
  maxWidth: '600px',
13
15
  margin: '0 auto',
@@ -27,7 +29,7 @@ export function EmailTemplate({ firstName, signature }: EmailTemplateProps) {
27
29
  fontSize: '14px',
28
30
  lineHeight: '1.6',
29
31
  }}
30
- dangerouslySetInnerHTML={{ __html: signature }}
32
+ dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(signature) }}
31
33
  />
32
34
  )}
33
35
  </div>