@tuturuuu/ui 0.7.0 → 0.9.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 (226) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/biome.json +1 -1
  3. package/package.json +75 -73
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/currency-input.test.tsx +43 -0
  29. package/src/components/ui/currency-input.tsx +1 -1
  30. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  31. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  32. package/src/components/ui/custom/combobox.test.tsx +195 -0
  33. package/src/components/ui/custom/combobox.tsx +273 -156
  34. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  35. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  36. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  37. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  38. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  39. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  40. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  41. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  42. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  43. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  44. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  45. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  46. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  47. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  48. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  49. package/src/components/ui/custom/workspace-select.tsx +8 -3
  50. package/src/components/ui/dialog.test.tsx +52 -0
  51. package/src/components/ui/dialog.tsx +6 -2
  52. package/src/components/ui/dropdown-menu.tsx +5 -1
  53. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  54. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  55. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  56. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  57. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  58. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  59. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  60. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  61. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  62. package/src/components/ui/finance/invoices/utils.ts +3 -1
  63. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  64. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  65. package/src/components/ui/finance/transactions/form.tsx +2 -0
  66. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  67. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  68. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  69. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  70. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  71. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  72. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  73. package/src/components/ui/finance/wallets/form.tsx +15 -4
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  75. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  76. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  77. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  78. package/src/components/ui/input-otp.tsx +1 -1
  79. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  80. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  81. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  82. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  83. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  84. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  85. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  86. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  87. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  88. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  89. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  90. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  91. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  92. package/src/components/ui/money-input.test.tsx +64 -0
  93. package/src/components/ui/money-input.tsx +63 -0
  94. package/src/components/ui/navigation-menu.tsx +1 -1
  95. package/src/components/ui/pagination.tsx +1 -1
  96. package/src/components/ui/radio-group.tsx +1 -1
  97. package/src/components/ui/select.tsx +5 -1
  98. package/src/components/ui/sheet.tsx +1 -1
  99. package/src/components/ui/sidebar.tsx +1 -1
  100. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  101. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  102. package/src/components/ui/storefront/cart-summary.tsx +104 -80
  103. package/src/components/ui/storefront/checkout-overlay.tsx +26 -0
  104. package/src/components/ui/storefront/hero-panel.tsx +2 -8
  105. package/src/components/ui/storefront/image-panel.tsx +6 -0
  106. package/src/components/ui/storefront/index.ts +11 -0
  107. package/src/components/ui/storefront/listing-card.tsx +84 -22
  108. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  109. package/src/components/ui/storefront/product-detail.tsx +289 -0
  110. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  111. package/src/components/ui/storefront/storefront-surface.test.tsx +221 -3
  112. package/src/components/ui/storefront/storefront-surface.tsx +288 -153
  113. package/src/components/ui/storefront/types.ts +27 -1
  114. package/src/components/ui/storefront/utils.ts +117 -27
  115. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  116. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  117. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  118. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  119. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  120. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  121. package/src/components/ui/text-editor/content-migration.ts +41 -18
  122. package/src/components/ui/text-editor/editor.tsx +69 -14
  123. package/src/components/ui/text-editor/extensions.ts +9 -3
  124. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  125. package/src/components/ui/text-editor/image-extension.ts +40 -18
  126. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  127. package/src/components/ui/text-editor/video-extension.ts +11 -2
  128. package/src/components/ui/toast.tsx +1 -1
  129. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  130. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  131. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  132. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  133. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  134. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +113 -46
  135. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  136. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  139. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  141. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +51 -9
  142. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  143. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +127 -38
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  148. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  149. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  150. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  151. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  152. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  153. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  154. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  155. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  156. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  157. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +410 -4
  158. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +106 -14
  159. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  160. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  161. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  162. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +186 -0
  163. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +59 -2
  164. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  165. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  166. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  167. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  168. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  169. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  170. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  171. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  172. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  173. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  174. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  175. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  176. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  177. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  178. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  179. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  180. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  181. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  182. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  183. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  184. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  185. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  186. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +237 -3
  187. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  188. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  189. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  190. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  191. package/src/components/ui/tu-do/shared/board-header.tsx +465 -937
  192. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  193. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  194. package/src/components/ui/tu-do/shared/board-views.tsx +596 -82
  195. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  196. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  197. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  198. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  199. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  200. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  201. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  202. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  203. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  204. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  205. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  206. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  210. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +44 -15
  211. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  212. package/src/declarations.d.ts +1 -0
  213. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  214. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  215. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  216. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  217. package/src/hooks/use-calendar-sync.tsx +247 -243
  218. package/src/hooks/use-calendar.tsx +323 -138
  219. package/src/hooks/use-task-actions.ts +24 -0
  220. package/src/hooks/use-user-workspace-config.ts +75 -0
  221. package/src/hooks/use-workspace-currency.ts +8 -3
  222. package/src/hooks/useBoardRealtime.ts +6 -3
  223. package/src/hooks/useBoardRealtime.types.ts +11 -0
  224. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
  225. package/src/hooks/useCursorTracking.ts +91 -27
  226. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -22,7 +22,7 @@ import { cn } from '@tuturuuu/utils/format';
22
22
  import { containsHtml, sanitizeHtml } from '@tuturuuu/utils/html-sanitizer';
23
23
  import dayjs from 'dayjs';
24
24
  import timezone from 'dayjs/plugin/timezone';
25
- import { useCallback, useEffect, useRef, useState } from 'react';
25
+ import { memo, useCallback, useEffect, useRef, useState } from 'react';
26
26
  import {
27
27
  ContextMenu,
28
28
  ContextMenuContent,
@@ -34,6 +34,7 @@ import {
34
34
  ContextMenuTrigger,
35
35
  } from '../../context-menu';
36
36
  import { GRID_SNAP, HOUR_HEIGHT, MAX_HOURS, MIN_EVENT_HEIGHT } from './config';
37
+ import { CalendarEventProviderIcon } from './event-provider-display';
37
38
  import { useCalendarSettings } from './settings/settings-context';
38
39
 
39
40
  dayjs.extend(timezone);
@@ -58,7 +59,7 @@ interface EventCardProps {
58
59
  level?: number; // Level for stacking events
59
60
  }
60
61
 
61
- export function EventCard({ dates, event, level = 0 }: EventCardProps) {
62
+ function EventCardComponent({ dates, event, level = 0 }: EventCardProps) {
62
63
  const {
63
64
  id,
64
65
  title,
@@ -83,6 +84,7 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
83
84
  _isReused,
84
85
  _previewType,
85
86
  _warning,
87
+ _optimisticStatus,
86
88
  } = event as CalendarEvent & {
87
89
  _isHabit?: boolean;
88
90
  _habitCompleted?: boolean;
@@ -90,6 +92,7 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
90
92
  _isReused?: boolean;
91
93
  _previewType?: 'habit' | 'task';
92
94
  _warning?: string;
95
+ _optimisticStatus?: 'creating' | 'updating' | 'deleting' | 'error';
93
96
  };
94
97
 
95
98
  // Default values for overlap properties if not provided
@@ -107,6 +110,11 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
107
110
  hoveredEventColumn,
108
111
  setHoveredEventColumn,
109
112
  affectedEventIds,
113
+ disableBuiltInEventUi,
114
+ preservePastEventOpacity,
115
+ renderEventContextMenu,
116
+ isEventReadOnly,
117
+ readOnly,
110
118
  } = useCalendar();
111
119
 
112
120
  // NOTE: Event filtering for hideNonPreviewEvents is handled in CalendarEventMatrix
@@ -114,6 +122,7 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
114
122
  const { settings } = useCalendarSettings();
115
123
  const queryClient = useQueryClient();
116
124
  const tz = settings?.timezone?.timezone;
125
+ const isReadOnlyEvent = readOnly || isEventReadOnly(event);
117
126
 
118
127
  // Local state for immediate UI updates
119
128
  const [localEvent, setLocalEvent] = useState<CalendarEvent>(event);
@@ -168,6 +177,12 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
168
177
  isDragging: false,
169
178
  isResizing: false,
170
179
  });
180
+ const isOptimisticallyPending =
181
+ _optimisticStatus === 'creating' ||
182
+ _optimisticStatus === 'updating' ||
183
+ _optimisticStatus === 'deleting';
184
+ const isOptimisticallyMutating =
185
+ _optimisticStatus === 'updating' || _optimisticStatus === 'deleting';
171
186
 
172
187
  // Status feedback timeout
173
188
  const statusTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -443,7 +458,7 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
443
458
  if (shouldBeTransparent) {
444
459
  cardEl.style.opacity = '0.05';
445
460
  cardEl.style.pointerEvents = 'none';
446
- } else if (isPastEvent) {
461
+ } else if (isPastEvent && !preservePastEventOpacity) {
447
462
  cardEl.style.opacity = '0.5';
448
463
  cardEl.style.pointerEvents = 'all';
449
464
  } else {
@@ -467,12 +482,14 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
467
482
  hoveredBaseEventId,
468
483
  hoveredEventColumn,
469
484
  endDate,
485
+ preservePastEventOpacity,
470
486
  ]);
471
487
 
472
488
  // Event resizing - only enable for non-multi-day events or the start/end segments
473
489
  useEffect(() => {
474
490
  // Disable resizing for middle segments of multi-day events
475
491
  // Note: locked events CAN still be resized - locked only prevents auto-scheduling
492
+ if (isReadOnlyEvent) return;
476
493
  if (_isMultiDay && _dayPosition === 'middle') return;
477
494
 
478
495
  const handleEl = handleRef.current;
@@ -663,12 +680,14 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
663
680
  showStatusFeedback, // Update visual state
664
681
  updateVisualState,
665
682
  queryClient,
683
+ isReadOnlyEvent,
666
684
  ]);
667
685
 
668
686
  // Event dragging - only enable for non-multi-day events
669
687
  useEffect(() => {
670
688
  // Disable dragging for multi-day events only
671
689
  // Note: locked events CAN still be dragged - locked only prevents auto-scheduling
690
+ if (isReadOnlyEvent) return;
672
691
  if (_isMultiDay) return;
673
692
 
674
693
  const contentEl = contentRef.current;
@@ -914,6 +933,7 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
914
933
  scheduleUpdate,
915
934
  showStatusFeedback, // Update visual state for immediate feedback
916
935
  updateVisualState,
936
+ isReadOnlyEvent,
917
937
  ]);
918
938
 
919
939
  // Color styles based on event color
@@ -989,13 +1009,8 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
989
1009
  const eventId = event._originalId || id;
990
1010
  const newLockedState = !locked;
991
1011
 
992
- console.log(
993
- `Toggling lock status for event ${eventId} from ${locked} to ${newLockedState}`
994
- );
995
-
996
1012
  updateEvent(eventId, { locked: newLockedState })
997
1013
  .then(() => {
998
- console.log(`Successfully updated lock status to ${newLockedState}`);
999
1014
  // Update local state immediately for better UX
1000
1015
  setLocalEvent((prev) => ({
1001
1016
  ...prev,
@@ -1038,6 +1053,7 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
1038
1053
  // For visual effects, check if this is likely a shorter event (higher in stack)
1039
1054
  // Shorter events get enhanced visual treatment
1040
1055
  const isLikelyTopEvent = hasOverlaps && duration < 1.5; // Events < 1.5 hours likely on top
1056
+ const customContextMenu = renderEventContextMenu?.(event);
1041
1057
 
1042
1058
  return (
1043
1059
  <ContextMenu>
@@ -1046,6 +1062,7 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
1046
1062
  type="button"
1047
1063
  ref={cardRef}
1048
1064
  id={`event-${id}`}
1065
+ data-testid={`calendar-event-${event._originalId || id}`}
1049
1066
  className={cn(
1050
1067
  'pointer-events-auto absolute max-w-none select-none overflow-hidden rounded-r-md rounded-l transition-all duration-300',
1051
1068
  'group hover:ring-1 focus:outline-none',
@@ -1053,7 +1070,10 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
1053
1070
  'transform shadow-md': isDragging || isResizing, // Subtle transform during interaction
1054
1071
  'shadow-sm': hasOverlaps && !isDragging && !isResizing, // Subtle shadow for stacked events
1055
1072
  'hover:shadow-md': hasOverlaps, // Enhanced shadow on hover for stacked events
1056
- 'opacity-50': isPastEvent && !isAffectedByPreview, // Lower opacity for past events
1073
+ 'opacity-50':
1074
+ isPastEvent &&
1075
+ !isAffectedByPreview &&
1076
+ !preservePastEventOpacity, // Lower opacity for past events
1057
1077
  'opacity-30 grayscale transition-all duration-500':
1058
1078
  isAffectedByPreview, // Dim affected events during preview
1059
1079
  'rounded-l-none border-l-4': showStartIndicator, // Special styling for continuation from previous day
@@ -1063,6 +1083,10 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
1063
1083
  'opacity-60': _isHabit && _habitCompleted, // Dimmed for completed habits
1064
1084
  // Preview-specific styling - dashed border only for NEW/MOVED events (not reused)
1065
1085
  'border-2 border-dashed': _isPreview && !_isReused,
1086
+ 'opacity-60 outline outline-dashed outline-1 outline-primary/50':
1087
+ isOptimisticallyMutating,
1088
+ 'opacity-80 ring-1 ring-primary/30':
1089
+ isOptimisticallyPending && !isOptimisticallyMutating,
1066
1090
  },
1067
1091
  level ? 'border border-l-2' : 'border-l-2',
1068
1092
  border,
@@ -1118,6 +1142,7 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
1118
1142
  wasResizedRef.current = false;
1119
1143
  }}
1120
1144
  aria-label={`Event: ${title || 'Untitled event'}${hasCalendarInfo ? ` from ${calendarDisplayName}` : ''}`}
1145
+ aria-busy={isOptimisticallyPending || updateStatus === 'syncing'}
1121
1146
  title={
1122
1147
  hasCalendarInfo ? `Calendar: ${calendarDisplayName}` : undefined
1123
1148
  }
@@ -1163,27 +1188,32 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
1163
1188
  )}
1164
1189
 
1165
1190
  {/* Edit button overlay */}
1166
- <div
1167
- className={cn(
1168
- 'absolute top-2 rounded-full p-0.5 opacity-0 shadow-sm',
1169
- _isHabit ? 'right-5' : 'right-2', // Offset if habit icon is shown
1170
- 'z-10 transition-opacity group-hover:opacity-100', // Higher z-index
1171
- {
1172
- 'opacity-0!':
1173
- isDragging || isResizing || updateStatus !== 'idle',
1174
- } // Hide during interaction or status updates
1175
- )}
1176
- onClick={(e) => {
1177
- e.stopPropagation();
1178
- e.preventDefault();
1179
- openModal(event._originalId || id);
1180
- }}
1181
- >
1182
- <Pencil className="h-3 w-3" />
1183
- </div>
1191
+ {!disableBuiltInEventUi && (
1192
+ <div
1193
+ className={cn(
1194
+ 'absolute top-2 rounded-full p-0.5 opacity-0 shadow-sm',
1195
+ _isHabit ? 'right-5' : 'right-2', // Offset if habit icon is shown
1196
+ 'z-10 transition-opacity group-hover:opacity-100', // Higher z-index
1197
+ {
1198
+ 'opacity-0!':
1199
+ isDragging ||
1200
+ isResizing ||
1201
+ updateStatus !== 'idle' ||
1202
+ isOptimisticallyPending,
1203
+ } // Hide during interaction or status updates
1204
+ )}
1205
+ onClick={(e) => {
1206
+ e.stopPropagation();
1207
+ e.preventDefault();
1208
+ openModal(event._originalId || id);
1209
+ }}
1210
+ >
1211
+ <Pencil className="h-3 w-3" />
1212
+ </div>
1213
+ )}
1184
1214
 
1185
1215
  {/* Status indicators */}
1186
- {updateStatus === 'syncing' && (
1216
+ {updateStatus === 'syncing' && !isOptimisticallyPending && (
1187
1217
  <div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-background/5">
1188
1218
  {/* <div
1189
1219
  className="animate-shimmer h-full w-full bg-linear-to-r from-transparent via-background/10 to-transparent"
@@ -1243,6 +1273,10 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
1243
1273
  {localEvent.locked && !_isPreview && (
1244
1274
  <Lock className="mr-1 inline-block h-3 w-3 align-middle opacity-70" />
1245
1275
  )}
1276
+ <CalendarEventProviderIcon
1277
+ event={localEvent}
1278
+ className="mr-1 h-3 w-3 opacity-80"
1279
+ />
1246
1280
  <span>{localEvent.title || 'Untitled event'}</span>
1247
1281
  {_isPreview && !_isReused && (
1248
1282
  <span
@@ -1299,7 +1333,7 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
1299
1333
  </div>
1300
1334
 
1301
1335
  {/* Only show resize handle for non-multi-day events or start/end segments */}
1302
- {(!_isMultiDay || _dayPosition !== 'middle') && (
1336
+ {!isReadOnlyEvent && (!_isMultiDay || _dayPosition !== 'middle') && (
1303
1337
  <div
1304
1338
  ref={handleRef}
1305
1339
  className={cn(
@@ -1310,121 +1344,176 @@ export function EventCard({ dates, event, level = 0 }: EventCardProps) {
1310
1344
  )}
1311
1345
  </button>
1312
1346
  </ContextMenuTrigger>
1313
- <ContextMenuContent className="w-48">
1314
- <ContextMenuItem
1315
- onClick={() => openModal(event._originalId || id)}
1316
- className="flex items-center gap-2"
1317
- >
1318
- <Edit className="h-4 w-4" />
1319
- <span>Edit Event</span>
1320
- </ContextMenuItem>
1321
-
1322
- <ContextMenuItem
1323
- onClick={handleLockToggle}
1324
- className="flex items-center gap-2"
1325
- >
1326
- {locked ? (
1327
- <>
1328
- <Unlock className="h-4 w-4" />
1329
- <span>Unlock Event</span>
1330
- </>
1331
- ) : (
1332
- <>
1333
- <Lock className="h-4 w-4" />
1334
- <span>Lock Event</span>
1335
- </>
1336
- )}
1337
- </ContextMenuItem>
1338
-
1339
- <ContextMenuSub>
1340
- <ContextMenuSubTrigger className="flex items-center gap-2">
1341
- <Palette className="h-4 w-4" />
1342
- <span className="text-foreground">Change Color</span>
1343
- </ContextMenuSubTrigger>
1344
- <ContextMenuSubContent className="grid w-56 grid-cols-2 gap-1 p-2">
1345
- <ContextMenuItem
1346
- onClick={() => handleColorChange('RED')}
1347
- className="flex items-center gap-2"
1348
- >
1349
- <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-red/80 bg-calendar-bg-red"></div>
1350
- <span>Red</span>
1351
- </ContextMenuItem>
1352
- <ContextMenuItem
1353
- onClick={() => handleColorChange('BLUE')}
1354
- className="flex items-center gap-2"
1355
- >
1356
- <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-blue/80 bg-calendar-bg-blue"></div>
1357
- <span>Blue</span>
1358
- </ContextMenuItem>
1359
- <ContextMenuItem
1360
- onClick={() => handleColorChange('GREEN')}
1361
- className="flex items-center gap-2"
1362
- >
1363
- <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-green/80 bg-calendar-bg-green"></div>
1364
- <span>Green</span>
1365
- </ContextMenuItem>
1366
- <ContextMenuItem
1367
- onClick={() => handleColorChange('YELLOW')}
1368
- className="flex items-center gap-2"
1369
- >
1370
- <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-yellow/80 bg-calendar-bg-yellow"></div>
1371
- <span>Yellow</span>
1372
- </ContextMenuItem>
1347
+ {customContextMenu ??
1348
+ (!disableBuiltInEventUi && (
1349
+ <ContextMenuContent className="w-48">
1373
1350
  <ContextMenuItem
1374
- onClick={() => handleColorChange('PURPLE')}
1351
+ onClick={() => openModal(event._originalId || id)}
1375
1352
  className="flex items-center gap-2"
1376
1353
  >
1377
- <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-purple/80 bg-calendar-bg-purple"></div>
1378
- <span>Purple</span>
1379
- </ContextMenuItem>
1380
- <ContextMenuItem
1381
- onClick={() => handleColorChange('PINK')}
1382
- className="flex items-center gap-2"
1383
- >
1384
- <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-pink/80 bg-calendar-bg-pink"></div>
1385
- <span>Pink</span>
1386
- </ContextMenuItem>
1387
- <ContextMenuItem
1388
- onClick={() => handleColorChange('ORANGE')}
1389
- className="flex items-center gap-2"
1390
- >
1391
- <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-orange/80 bg-calendar-bg-orange"></div>
1392
- <span>Orange</span>
1393
- </ContextMenuItem>
1394
- <ContextMenuItem
1395
- onClick={() => handleColorChange('INDIGO')}
1396
- className="flex items-center gap-2"
1397
- >
1398
- <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-indigo/80 bg-calendar-bg-indigo"></div>
1399
- <span>Indigo</span>
1354
+ <Edit className="h-4 w-4" />
1355
+ <span>Edit Event</span>
1400
1356
  </ContextMenuItem>
1357
+
1401
1358
  <ContextMenuItem
1402
- onClick={() => handleColorChange('CYAN')}
1359
+ onClick={handleLockToggle}
1403
1360
  className="flex items-center gap-2"
1404
1361
  >
1405
- <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-cyan/80 bg-calendar-bg-cyan"></div>
1406
- <span>Cyan</span>
1362
+ {locked ? (
1363
+ <>
1364
+ <Unlock className="h-4 w-4" />
1365
+ <span>Unlock Event</span>
1366
+ </>
1367
+ ) : (
1368
+ <>
1369
+ <Lock className="h-4 w-4" />
1370
+ <span>Lock Event</span>
1371
+ </>
1372
+ )}
1407
1373
  </ContextMenuItem>
1374
+
1375
+ <ContextMenuSub>
1376
+ <ContextMenuSubTrigger className="flex items-center gap-2">
1377
+ <Palette className="h-4 w-4" />
1378
+ <span className="text-foreground">Change Color</span>
1379
+ </ContextMenuSubTrigger>
1380
+ <ContextMenuSubContent className="grid w-56 grid-cols-2 gap-1 p-2">
1381
+ <ContextMenuItem
1382
+ onClick={() => handleColorChange('RED')}
1383
+ className="flex items-center gap-2"
1384
+ >
1385
+ <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-red/80 bg-calendar-bg-red"></div>
1386
+ <span>Red</span>
1387
+ </ContextMenuItem>
1388
+ <ContextMenuItem
1389
+ onClick={() => handleColorChange('BLUE')}
1390
+ className="flex items-center gap-2"
1391
+ >
1392
+ <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-blue/80 bg-calendar-bg-blue"></div>
1393
+ <span>Blue</span>
1394
+ </ContextMenuItem>
1395
+ <ContextMenuItem
1396
+ onClick={() => handleColorChange('GREEN')}
1397
+ className="flex items-center gap-2"
1398
+ >
1399
+ <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-green/80 bg-calendar-bg-green"></div>
1400
+ <span>Green</span>
1401
+ </ContextMenuItem>
1402
+ <ContextMenuItem
1403
+ onClick={() => handleColorChange('YELLOW')}
1404
+ className="flex items-center gap-2"
1405
+ >
1406
+ <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-yellow/80 bg-calendar-bg-yellow"></div>
1407
+ <span>Yellow</span>
1408
+ </ContextMenuItem>
1409
+ <ContextMenuItem
1410
+ onClick={() => handleColorChange('PURPLE')}
1411
+ className="flex items-center gap-2"
1412
+ >
1413
+ <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-purple/80 bg-calendar-bg-purple"></div>
1414
+ <span>Purple</span>
1415
+ </ContextMenuItem>
1416
+ <ContextMenuItem
1417
+ onClick={() => handleColorChange('PINK')}
1418
+ className="flex items-center gap-2"
1419
+ >
1420
+ <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-pink/80 bg-calendar-bg-pink"></div>
1421
+ <span>Pink</span>
1422
+ </ContextMenuItem>
1423
+ <ContextMenuItem
1424
+ onClick={() => handleColorChange('ORANGE')}
1425
+ className="flex items-center gap-2"
1426
+ >
1427
+ <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-orange/80 bg-calendar-bg-orange"></div>
1428
+ <span>Orange</span>
1429
+ </ContextMenuItem>
1430
+ <ContextMenuItem
1431
+ onClick={() => handleColorChange('INDIGO')}
1432
+ className="flex items-center gap-2"
1433
+ >
1434
+ <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-indigo/80 bg-calendar-bg-indigo"></div>
1435
+ <span>Indigo</span>
1436
+ </ContextMenuItem>
1437
+ <ContextMenuItem
1438
+ onClick={() => handleColorChange('CYAN')}
1439
+ className="flex items-center gap-2"
1440
+ >
1441
+ <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-cyan/80 bg-calendar-bg-cyan"></div>
1442
+ <span>Cyan</span>
1443
+ </ContextMenuItem>
1444
+ <ContextMenuItem
1445
+ onClick={() => handleColorChange('GRAY')}
1446
+ className="flex items-center gap-2"
1447
+ >
1448
+ <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-gray/80 bg-calendar-bg-gray"></div>
1449
+ <span>Gray</span>
1450
+ </ContextMenuItem>
1451
+ </ContextMenuSubContent>
1452
+ </ContextMenuSub>
1453
+
1454
+ <ContextMenuSeparator />
1455
+
1408
1456
  <ContextMenuItem
1409
- onClick={() => handleColorChange('GRAY')}
1457
+ onClick={handleDelete}
1410
1458
  className="flex items-center gap-2"
1411
1459
  >
1412
- <div className="h-4 w-4 flex-none rounded-full border border-dynamic-light-gray/80 bg-calendar-bg-gray"></div>
1413
- <span>Gray</span>
1460
+ <Trash2 className="h-4 w-4" />
1461
+ <span>Delete Event</span>
1414
1462
  </ContextMenuItem>
1415
- </ContextMenuSubContent>
1416
- </ContextMenuSub>
1463
+ </ContextMenuContent>
1464
+ ))}
1465
+ </ContextMenu>
1466
+ );
1467
+ }
1417
1468
 
1418
- <ContextMenuSeparator />
1469
+ function datesEqual(left: Date[], right: Date[]) {
1470
+ if (left.length !== right.length) return false;
1471
+ return left.every(
1472
+ (date, index) => date.getTime() === right[index]?.getTime()
1473
+ );
1474
+ }
1419
1475
 
1420
- <ContextMenuItem
1421
- onClick={handleDelete}
1422
- className="flex items-center gap-2"
1423
- >
1424
- <Trash2 className="h-4 w-4" />
1425
- <span>Delete Event</span>
1426
- </ContextMenuItem>
1427
- </ContextMenuContent>
1428
- </ContextMenu>
1476
+ function eventCardPropsEqual(previous: EventCardProps, next: EventCardProps) {
1477
+ if (previous.wsId !== next.wsId || previous.level !== next.level) {
1478
+ return false;
1479
+ }
1480
+
1481
+ if (!datesEqual(previous.dates, next.dates)) return false;
1482
+
1483
+ const left = previous.event as CalendarEvent & {
1484
+ _optimisticStatus?: string;
1485
+ _warning?: string;
1486
+ };
1487
+ const right = next.event as CalendarEvent & {
1488
+ _optimisticStatus?: string;
1489
+ _warning?: string;
1490
+ };
1491
+
1492
+ return (
1493
+ left.id === right.id &&
1494
+ left.title === right.title &&
1495
+ left.description === right.description &&
1496
+ left.start_at === right.start_at &&
1497
+ left.end_at === right.end_at &&
1498
+ left.color === right.color &&
1499
+ left.locked === right.locked &&
1500
+ left.provider === right.provider &&
1501
+ left.external_event_id === right.external_event_id &&
1502
+ left.external_calendar_id === right.external_calendar_id &&
1503
+ left.google_event_id === right.google_event_id &&
1504
+ left.google_calendar_id === right.google_calendar_id &&
1505
+ left.location === right.location &&
1506
+ left._originalId === right._originalId &&
1507
+ left._isMultiDay === right._isMultiDay &&
1508
+ left._dayPosition === right._dayPosition &&
1509
+ left._overlapCount === right._overlapCount &&
1510
+ left._column === right._column &&
1511
+ left._calendarName === right._calendarName &&
1512
+ left._calendarColor === right._calendarColor &&
1513
+ left._level === right._level &&
1514
+ left._optimisticStatus === right._optimisticStatus &&
1515
+ left._warning === right._warning
1429
1516
  );
1430
1517
  }
1518
+
1519
+ export const EventCard = memo(EventCardComponent, eventCardPropsEqual);
@@ -73,7 +73,6 @@ import { format } from 'date-fns';
73
73
  import dayjs from 'dayjs';
74
74
  import ts from 'dayjs/plugin/timezone';
75
75
  import utc from 'dayjs/plugin/utc';
76
- import Image from 'next/image';
77
76
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
78
77
  import { z } from 'zod';
79
78
  import { Alert, AlertDescription, AlertTitle } from '../../alert';
@@ -89,6 +88,10 @@ import {
89
88
  EventToggleSwitch,
90
89
  OverlapWarning,
91
90
  } from './event-form-components';
91
+ import {
92
+ CalendarEventProviderIcon,
93
+ getCalendarEventProviderDisplay,
94
+ } from './event-provider-display';
92
95
  import { useCalendarSettings } from './settings/settings-context';
93
96
 
94
97
  dayjs.extend(ts);
@@ -955,28 +958,25 @@ export function EventModal() {
955
958
  }
956
959
  };
957
960
 
961
+ const providerDisplay = getCalendarEventProviderDisplay(event);
962
+
958
963
  return (
959
964
  <Dialog open={isModalOpen} onOpenChange={(open) => !open && closeModal()}>
960
965
  <DialogContent className="max-h-[90vh] max-w-3xl overflow-hidden p-0">
961
966
  <DialogHeader className="border-b px-6 pt-6 pb-4">
962
967
  <DialogTitle className="flex items-center gap-2 font-semibold text-xl">
963
968
  <span>{isEditing ? 'Edit Event' : 'Create Event'}</span>
964
- {event.google_event_id &&
965
- typeof event.google_event_id === 'string' &&
966
- event.google_event_id.trim() !== '' && (
967
- <div className="ml-3 flex items-center gap-2 rounded-md border bg-blue-50 px-3 py-1 text-sm dark:bg-blue-950/30">
968
- <Image
969
- src="/media/google-calendar-icon.png"
970
- alt="Google Calendar"
971
- className="inline-block h-4.5 w-4.5 align-middle"
972
- title="Synced from Google Calendar"
973
- data-testid="google-calendar-logo"
974
- width={18}
975
- height={18}
976
- />
977
- <span className="font-medium text-xs">Google Calendar</span>
978
- </div>
979
- )}
969
+ {providerDisplay && (
970
+ <div className="ml-3 flex items-center gap-2 rounded-md border bg-muted/40 px-3 py-1 text-sm">
971
+ <CalendarEventProviderIcon
972
+ event={event}
973
+ className="h-4.5 w-4.5"
974
+ />
975
+ <span className="font-medium text-xs">
976
+ {providerDisplay.label}
977
+ </span>
978
+ </div>
979
+ )}
980
980
  </DialogTitle>
981
981
  <DialogDescription>
982
982
  {isEditing
@@ -0,0 +1,69 @@
1
+ import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
2
+ import { cn } from '@tuturuuu/utils/format';
3
+ import Image from 'next/image';
4
+
5
+ type ExternalCalendarProvider = 'google' | 'microsoft';
6
+
7
+ const PROVIDER_DISPLAY: Record<
8
+ ExternalCalendarProvider,
9
+ {
10
+ alt: string;
11
+ label: string;
12
+ src: string;
13
+ testId: string;
14
+ title: string;
15
+ }
16
+ > = {
17
+ google: {
18
+ alt: 'Google Calendar',
19
+ label: 'Google Calendar',
20
+ src: '/media/google-calendar-icon.png',
21
+ testId: 'google-calendar-logo',
22
+ title: 'Synced from Google Calendar',
23
+ },
24
+ microsoft: {
25
+ alt: 'Microsoft Outlook',
26
+ label: 'Microsoft Outlook',
27
+ src: '/media/logos/microsoft.svg',
28
+ testId: 'microsoft-outlook-logo',
29
+ title: 'Synced from Microsoft Outlook',
30
+ },
31
+ };
32
+
33
+ export function getCalendarEventProvider(
34
+ event: Partial<
35
+ Pick<CalendarEvent, 'provider' | 'external_event_id' | 'google_event_id'>
36
+ >
37
+ ): ExternalCalendarProvider | null {
38
+ if (event.provider === 'google' || event.google_event_id) return 'google';
39
+ if (event.provider === 'microsoft') return 'microsoft';
40
+ return null;
41
+ }
42
+
43
+ export function getCalendarEventProviderDisplay(event: Partial<CalendarEvent>) {
44
+ const provider = getCalendarEventProvider(event);
45
+ return provider ? PROVIDER_DISPLAY[provider] : null;
46
+ }
47
+
48
+ export function CalendarEventProviderIcon({
49
+ className,
50
+ event,
51
+ }: {
52
+ className?: string;
53
+ event: Partial<CalendarEvent>;
54
+ }) {
55
+ const display = getCalendarEventProviderDisplay(event);
56
+ if (!display) return null;
57
+
58
+ return (
59
+ <Image
60
+ src={display.src}
61
+ alt={display.alt}
62
+ className={cn('inline-block align-middle', className)}
63
+ title={display.title}
64
+ data-testid={display.testId}
65
+ height={18}
66
+ width={18}
67
+ />
68
+ );
69
+ }