@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.
- package/CHANGELOG.md +88 -0
- package/biome.json +1 -1
- package/package.json +75 -73
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +1 -1
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
- package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
- package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
- package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/carousel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
- package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
- package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/command.tsx +1 -1
- package/src/components/ui/context-menu.tsx +5 -1
- package/src/components/ui/currency-input.test.tsx +43 -0
- package/src/components/ui/currency-input.tsx +1 -1
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
- package/src/components/ui/custom/combobox.test.tsx +195 -0
- package/src/components/ui/custom/combobox.tsx +273 -156
- package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
- package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
- package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
- package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
- package/src/components/ui/custom/theme-toggle.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
- package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
- package/src/components/ui/custom/workspace-select.tsx +8 -3
- package/src/components/ui/dialog.test.tsx +52 -0
- package/src/components/ui/dialog.tsx +6 -2
- package/src/components/ui/dropdown-menu.tsx +5 -1
- package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
- package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
- package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
- package/src/components/ui/finance/debts/debts-page.tsx +15 -2
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
- package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
- package/src/components/ui/finance/transactions/form-types.ts +3 -0
- package/src/components/ui/finance/transactions/form.tsx +2 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
- package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
- package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
- package/src/components/ui/finance/wallets/form.test.tsx +51 -3
- package/src/components/ui/finance/wallets/form.tsx +15 -4
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
- package/src/components/ui/input-otp.tsx +1 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
- package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
- package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
- package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
- package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
- package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
- package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
- package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
- package/src/components/ui/money-input.test.tsx +64 -0
- package/src/components/ui/money-input.tsx +63 -0
- package/src/components/ui/navigation-menu.tsx +1 -1
- package/src/components/ui/pagination.tsx +1 -1
- package/src/components/ui/radio-group.tsx +1 -1
- package/src/components/ui/select.tsx +5 -1
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +61 -0
- package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
- package/src/components/ui/storefront/cart-summary.tsx +104 -80
- package/src/components/ui/storefront/checkout-overlay.tsx +26 -0
- package/src/components/ui/storefront/hero-panel.tsx +2 -8
- package/src/components/ui/storefront/image-panel.tsx +6 -0
- package/src/components/ui/storefront/index.ts +11 -0
- package/src/components/ui/storefront/listing-card.tsx +84 -22
- package/src/components/ui/storefront/merch-sections.tsx +70 -0
- package/src/components/ui/storefront/product-detail.tsx +289 -0
- package/src/components/ui/storefront/product-dialog.tsx +72 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +221 -3
- package/src/components/ui/storefront/storefront-surface.tsx +288 -153
- package/src/components/ui/storefront/types.ts +27 -1
- package/src/components/ui/storefront/utils.ts +117 -27
- package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
- package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
- package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
- package/src/components/ui/text-editor/background-color-extension.ts +62 -0
- package/src/components/ui/text-editor/color-controls.tsx +284 -0
- package/src/components/ui/text-editor/content-migration.ts +41 -18
- package/src/components/ui/text-editor/editor.tsx +69 -14
- package/src/components/ui/text-editor/extensions.ts +9 -3
- package/src/components/ui/text-editor/highlight-extension.ts +22 -0
- package/src/components/ui/text-editor/image-extension.ts +40 -18
- package/src/components/ui/text-editor/tool-bar.tsx +9 -16
- package/src/components/ui/text-editor/video-extension.ts +11 -2
- package/src/components/ui/toast.tsx +1 -1
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
- package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
- package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
- package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +113 -46
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +51 -9
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +127 -38
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +410 -4
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +106 -14
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +186 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +59 -2
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
- package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
- package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
- package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
- package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
- package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
- package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
- package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
- package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
- package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +237 -3
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
- package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
- package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
- package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +465 -937
- package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
- package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
- package/src/components/ui/tu-do/shared/board-views.tsx +596 -82
- package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
- package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
- package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
- package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
- package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +44 -15
- package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
- package/src/declarations.d.ts +1 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
- package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
- package/src/hooks/use-calendar-sync.tsx +247 -243
- package/src/hooks/use-calendar.tsx +323 -138
- package/src/hooks/use-task-actions.ts +24 -0
- package/src/hooks/use-user-workspace-config.ts +75 -0
- package/src/hooks/use-workspace-currency.ts +8 -3
- package/src/hooks/useBoardRealtime.ts +6 -3
- package/src/hooks/useBoardRealtime.types.ts +11 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
- package/src/hooks/useCursorTracking.ts +91 -27
- package/src/hooks/useTaskUserRealtime.ts +5 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AlertTriangle,
|
|
2
3
|
CalendarIcon,
|
|
4
|
+
Check,
|
|
3
5
|
ChevronLeft,
|
|
4
6
|
ChevronRight,
|
|
5
7
|
Moon,
|
|
@@ -81,7 +83,7 @@ export function CalendarHeader({
|
|
|
81
83
|
return newDate;
|
|
82
84
|
});
|
|
83
85
|
|
|
84
|
-
const {
|
|
86
|
+
const { syncStatus } = useCalendarSync();
|
|
85
87
|
const selectToday = () => setDate(new Date());
|
|
86
88
|
const isTodaySelected = () => dayjs(date).isSame(dayjs(), 'day');
|
|
87
89
|
const isCurrentMonth = () =>
|
|
@@ -112,6 +114,14 @@ export function CalendarHeader({
|
|
|
112
114
|
};
|
|
113
115
|
|
|
114
116
|
const LunarIcon = showLunar ? MoonStar : Moon;
|
|
117
|
+
const statusLabel =
|
|
118
|
+
syncStatus.state === 'error'
|
|
119
|
+
? t('failed_to_load_events')
|
|
120
|
+
: syncStatus.lastSyncTime
|
|
121
|
+
? `${t('sync_completed')} ${dayjs(syncStatus.lastSyncTime)
|
|
122
|
+
.locale(locale)
|
|
123
|
+
.format('HH:mm')}`
|
|
124
|
+
: null;
|
|
115
125
|
|
|
116
126
|
return (
|
|
117
127
|
<div className="mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
@@ -121,8 +131,18 @@ export function CalendarHeader({
|
|
|
121
131
|
</div>
|
|
122
132
|
<div className="flex flex-col gap-2 md:flex-row md:items-center">
|
|
123
133
|
<div className="flex items-center gap-2">
|
|
124
|
-
{
|
|
125
|
-
<div
|
|
134
|
+
{statusLabel && (
|
|
135
|
+
<div
|
|
136
|
+
aria-live="polite"
|
|
137
|
+
className="hidden min-w-0 items-center gap-1.5 rounded-full border bg-background/80 px-2 py-1 text-muted-foreground text-xs shadow-xs sm:inline-flex"
|
|
138
|
+
>
|
|
139
|
+
{syncStatus.state === 'error' ? (
|
|
140
|
+
<AlertTriangle className="h-3.5 w-3.5 text-dynamic-red" />
|
|
141
|
+
) : (
|
|
142
|
+
<Check className="h-3.5 w-3.5 text-dynamic-green" />
|
|
143
|
+
)}
|
|
144
|
+
<span className="truncate">{statusLabel}</span>
|
|
145
|
+
</div>
|
|
126
146
|
)}
|
|
127
147
|
<div className="flex flex-none items-center justify-center gap-2 md:justify-start">
|
|
128
148
|
<Button
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
2
|
+
import type { CalendarView } from '../../../../hooks/use-view-transition';
|
|
3
|
+
import { Skeleton } from '../../skeleton';
|
|
4
|
+
import { DAY_HEIGHT, HOUR_HEIGHT, MIN_COLUMN_WIDTH } from './config';
|
|
5
|
+
|
|
6
|
+
function TimedCalendarSkeleton({ columns }: { columns: number }) {
|
|
7
|
+
const eventPlaceholders = [
|
|
8
|
+
{ column: 0, hour: 2.2, span: 1.4, width: 0.82 },
|
|
9
|
+
{ column: Math.min(1, columns - 1), hour: 5.1, span: 1.1, width: 0.74 },
|
|
10
|
+
{
|
|
11
|
+
column: Math.max(0, columns - 2),
|
|
12
|
+
hour: 8.4,
|
|
13
|
+
span: 1.8,
|
|
14
|
+
width: 0.78,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className="flex h-full overflow-hidden rounded-b-lg border-border border-b border-l bg-background/50 text-center dark:border-zinc-800"
|
|
21
|
+
style={{ minWidth: `${columns * MIN_COLUMN_WIDTH}px` }}
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
>
|
|
24
|
+
<div className="w-16 shrink-0 border-r bg-muted/20">
|
|
25
|
+
{Array.from({ length: 8 }).map((_, index) => (
|
|
26
|
+
<Skeleton key={index} className="mx-auto mt-7 h-3 w-9 rounded-sm" />
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
29
|
+
<div
|
|
30
|
+
className="relative grid flex-1"
|
|
31
|
+
style={{
|
|
32
|
+
gridTemplateColumns: `repeat(${columns}, minmax(${MIN_COLUMN_WIDTH}px, 1fr))`,
|
|
33
|
+
height: `${DAY_HEIGHT}px`,
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{Array.from({ length: columns }).map((_, index) => (
|
|
37
|
+
<div
|
|
38
|
+
key={index}
|
|
39
|
+
className="relative border-border/70 border-r last:border-r-0"
|
|
40
|
+
>
|
|
41
|
+
{Array.from({ length: 24 }).map((_, hour) => (
|
|
42
|
+
<div
|
|
43
|
+
key={hour}
|
|
44
|
+
className="border-border/50 border-b"
|
|
45
|
+
style={{ height: `${HOUR_HEIGHT}px` }}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
{eventPlaceholders.map((placeholder, index) => (
|
|
51
|
+
<Skeleton
|
|
52
|
+
key={index}
|
|
53
|
+
className="absolute rounded-md"
|
|
54
|
+
style={{
|
|
55
|
+
left: `calc(${(placeholder.column * 100) / columns}% + 6px)`,
|
|
56
|
+
top: `${placeholder.hour * HOUR_HEIGHT}px`,
|
|
57
|
+
width: `calc(${(placeholder.width * 100) / columns}% - 12px)`,
|
|
58
|
+
height: `${placeholder.span * HOUR_HEIGHT}px`,
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function MonthCalendarSkeleton({ columns }: { columns: number }) {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
className="grid h-full min-h-[28rem] gap-px overflow-hidden rounded-lg border bg-border"
|
|
71
|
+
style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
>
|
|
74
|
+
{Array.from({ length: columns * 5 }).map((_, index) => (
|
|
75
|
+
<div key={index} className="space-y-2 bg-background p-2">
|
|
76
|
+
<Skeleton className="h-3 w-8 rounded-sm" />
|
|
77
|
+
<Skeleton className="h-4 w-4/5 rounded-sm" />
|
|
78
|
+
<Skeleton className="h-4 w-3/5 rounded-sm" />
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function AgendaCalendarSkeleton() {
|
|
86
|
+
return (
|
|
87
|
+
<div className="space-y-3 rounded-lg border bg-background p-4" aria-hidden>
|
|
88
|
+
{Array.from({ length: 6 }).map((_, index) => (
|
|
89
|
+
<div key={index} className="flex items-center gap-3">
|
|
90
|
+
<Skeleton className="h-10 w-14 rounded-md" />
|
|
91
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
92
|
+
<Skeleton className="h-4 w-2/5 rounded-sm" />
|
|
93
|
+
<Skeleton className="h-3 w-3/5 rounded-sm" />
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function CalendarLoadingSkeleton({
|
|
102
|
+
dates,
|
|
103
|
+
view,
|
|
104
|
+
}: {
|
|
105
|
+
dates: Date[];
|
|
106
|
+
view: CalendarView;
|
|
107
|
+
}) {
|
|
108
|
+
const columns = Math.max(1, dates.length || (view === 'day' ? 1 : 7));
|
|
109
|
+
|
|
110
|
+
if (view === 'month' || view === 'year') {
|
|
111
|
+
return <MonthCalendarSkeleton columns={view === 'year' ? 4 : 7} />;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (view === 'agenda') return <AgendaCalendarSkeleton />;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="h-full min-h-0 space-y-2" aria-busy="true">
|
|
118
|
+
<div
|
|
119
|
+
className={cn(
|
|
120
|
+
'grid rounded-lg border bg-background/70 p-2',
|
|
121
|
+
columns === 1 && 'max-w-lg'
|
|
122
|
+
)}
|
|
123
|
+
style={{
|
|
124
|
+
gridTemplateColumns: `4rem repeat(${columns}, minmax(${MIN_COLUMN_WIDTH}px, 1fr))`,
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<Skeleton className="h-5 w-5 self-center justify-self-center rounded-sm" />
|
|
128
|
+
{Array.from({ length: columns }).map((_, index) => (
|
|
129
|
+
<Skeleton key={index} className="mx-1 h-5 rounded-sm" />
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
<TimedCalendarSkeleton columns={columns} />
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -4,6 +4,7 @@ import { useCalendarSync } from '@tuturuuu/ui/hooks/use-calendar-sync';
|
|
|
4
4
|
import dayjs from 'dayjs';
|
|
5
5
|
import timezone from 'dayjs/plugin/timezone';
|
|
6
6
|
import { useParams } from 'next/navigation';
|
|
7
|
+
import { useMemo } from 'react';
|
|
7
8
|
import { CalendarColumn } from './calendar-column';
|
|
8
9
|
import { DAY_HEIGHT, MAX_LEVEL } from './config';
|
|
9
10
|
import { EventCard } from './event-card';
|
|
@@ -12,6 +13,146 @@ import { useCalendarSettings } from './settings/settings-context';
|
|
|
12
13
|
|
|
13
14
|
dayjs.extend(timezone);
|
|
14
15
|
|
|
16
|
+
type LayoutCalendarEvent = CalendarEvent & {
|
|
17
|
+
_dayKey: string;
|
|
18
|
+
_endMs: number;
|
|
19
|
+
_startMs: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getDayKeyFromDate(date: Date, tz?: string) {
|
|
23
|
+
return (tz === 'auto' ? dayjs(date) : dayjs(date).tz(tz)).format(
|
|
24
|
+
'YYYY-MM-DD'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getDayKeyFromIso(value: string, tz?: string) {
|
|
29
|
+
return (tz === 'auto' ? dayjs(value) : dayjs(value).tz(tz)).format(
|
|
30
|
+
'YYYY-MM-DD'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function withLayoutMetadata(
|
|
35
|
+
event: CalendarEvent,
|
|
36
|
+
tz?: string
|
|
37
|
+
): LayoutCalendarEvent {
|
|
38
|
+
return {
|
|
39
|
+
...event,
|
|
40
|
+
_dayKey: getDayKeyFromIso(event.start_at, tz),
|
|
41
|
+
_endMs: new Date(event.end_at).getTime(),
|
|
42
|
+
_startMs: new Date(event.start_at).getTime(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assignEventLayout(
|
|
47
|
+
visibleEvents: CalendarEvent[],
|
|
48
|
+
tz?: string
|
|
49
|
+
): CalendarEvent[] {
|
|
50
|
+
const sortedEvents = visibleEvents
|
|
51
|
+
.map((event) => withLayoutMetadata(event, tz))
|
|
52
|
+
.sort((left, right) => left._startMs - right._startMs);
|
|
53
|
+
|
|
54
|
+
const eventLevels = new Map<string, number>();
|
|
55
|
+
const eventOverlapCounts = new Map<string, number>();
|
|
56
|
+
const eventOverlapGroups = new Map<string, string[]>();
|
|
57
|
+
const eventColumns = new Map<string, number>();
|
|
58
|
+
const eventsByDay = new Map<string, LayoutCalendarEvent[]>();
|
|
59
|
+
|
|
60
|
+
for (const event of sortedEvents) {
|
|
61
|
+
const dayEvents = eventsByDay.get(event._dayKey) ?? [];
|
|
62
|
+
dayEvents.push(event);
|
|
63
|
+
eventsByDay.set(event._dayKey, dayEvents);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const dayEvents of eventsByDay.values()) {
|
|
67
|
+
const sortedDayEvents = [...dayEvents].sort(
|
|
68
|
+
(left, right) => left._startMs - right._startMs
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const overlapGroups: LayoutCalendarEvent[][] = [];
|
|
72
|
+
let activeGroup: LayoutCalendarEvent[] = [];
|
|
73
|
+
let activeGroupEnd = Number.NEGATIVE_INFINITY;
|
|
74
|
+
|
|
75
|
+
for (const event of sortedDayEvents) {
|
|
76
|
+
if (activeGroup.length === 0 || event._startMs < activeGroupEnd) {
|
|
77
|
+
activeGroup.push(event);
|
|
78
|
+
activeGroupEnd = Math.max(activeGroupEnd, event._endMs);
|
|
79
|
+
} else {
|
|
80
|
+
overlapGroups.push(activeGroup);
|
|
81
|
+
activeGroup = [event];
|
|
82
|
+
activeGroupEnd = event._endMs;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (activeGroup.length > 0) overlapGroups.push(activeGroup);
|
|
87
|
+
|
|
88
|
+
for (const group of overlapGroups) {
|
|
89
|
+
const sortedGroup = [...group].sort((left, right) => {
|
|
90
|
+
if (left._startMs !== right._startMs)
|
|
91
|
+
return left._startMs - right._startMs;
|
|
92
|
+
return right._endMs - right._startMs - (left._endMs - left._startMs);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const groupEventColumns = new Map<string, number>();
|
|
96
|
+
const columnEndTimes: number[] = [];
|
|
97
|
+
|
|
98
|
+
for (const event of sortedGroup) {
|
|
99
|
+
let column = columnEndTimes.findIndex(
|
|
100
|
+
(columnEndTime) => event._startMs >= columnEndTime
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (column === -1) column = columnEndTimes.length;
|
|
104
|
+
|
|
105
|
+
groupEventColumns.set(event.id, column);
|
|
106
|
+
columnEndTimes[column] = event._endMs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const maxColumn = Math.max(0, ...groupEventColumns.values());
|
|
110
|
+
const orderedEventIds: string[] = [];
|
|
111
|
+
|
|
112
|
+
for (let column = 0; column <= maxColumn; column++) {
|
|
113
|
+
const columnEvents = sortedGroup
|
|
114
|
+
.filter((event) => groupEventColumns.get(event.id) === column)
|
|
115
|
+
.sort(
|
|
116
|
+
(left, right) =>
|
|
117
|
+
right._endMs - right._startMs - (left._endMs - left._startMs)
|
|
118
|
+
);
|
|
119
|
+
orderedEventIds.push(...columnEvents.map((event) => event.id));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const event of sortedGroup) {
|
|
123
|
+
eventOverlapCounts.set(event.id, sortedGroup.length);
|
|
124
|
+
eventOverlapGroups.set(event.id, orderedEventIds);
|
|
125
|
+
eventColumns.set(event.id, groupEventColumns.get(event.id) ?? 0);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const levelEndTimes: number[] = [];
|
|
130
|
+
|
|
131
|
+
for (const event of sortedDayEvents) {
|
|
132
|
+
let level = 0;
|
|
133
|
+
while (
|
|
134
|
+
level < MAX_LEVEL &&
|
|
135
|
+
levelEndTimes[level] !== undefined &&
|
|
136
|
+
event._startMs < levelEndTimes[level]!
|
|
137
|
+
) {
|
|
138
|
+
level++;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
level = Math.min(level, MAX_LEVEL - 1);
|
|
142
|
+
eventLevels.set(event.id, level);
|
|
143
|
+
levelEndTimes[level] = event._endMs;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return sortedEvents.map((event) => ({
|
|
148
|
+
...event,
|
|
149
|
+
_column: eventColumns.get(event.id) ?? 0,
|
|
150
|
+
_level: eventLevels.get(event.id) ?? 0,
|
|
151
|
+
_overlapCount: eventOverlapCounts.get(event.id) ?? 1,
|
|
152
|
+
_overlapGroup: eventOverlapGroups.get(event.id) ?? [event.id],
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
15
156
|
export const CalendarMatrix = ({
|
|
16
157
|
dates,
|
|
17
158
|
overlay,
|
|
@@ -55,250 +196,47 @@ export const CalendarEventMatrix = ({ dates }: { dates: Date[] }) => {
|
|
|
55
196
|
useCalendar();
|
|
56
197
|
const tz = settings?.timezone?.timezone;
|
|
57
198
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
? eventsWithoutAllDays.filter(
|
|
62
|
-
(e) => !affectedEventIds.has(e.id) || e._isPreview
|
|
63
|
-
)
|
|
64
|
-
: eventsWithoutAllDays;
|
|
65
|
-
|
|
66
|
-
// Merge real events with preview events for visual demo
|
|
67
|
-
// Deduplicate by ID: if a preview event has the same ID as a real event,
|
|
68
|
-
// prefer the real event (this happens when locked events are included in preview)
|
|
69
|
-
const realEventIds = new Set(filteredRealEvents.map((e) => e.id));
|
|
70
|
-
const filteredPreviewEvents = previewEvents.filter(
|
|
71
|
-
(e) => !realEventIds.has(e.id)
|
|
199
|
+
const visibleDayKeys = useMemo(
|
|
200
|
+
() => new Set(dates.map((date) => getDayKeyFromDate(date, tz))),
|
|
201
|
+
[dates, tz]
|
|
72
202
|
);
|
|
73
|
-
const allEvents = [...filteredRealEvents, ...filteredPreviewEvents];
|
|
74
203
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
204
|
+
const filteredRealEvents = useMemo(
|
|
205
|
+
() =>
|
|
206
|
+
hideNonPreviewEvents
|
|
207
|
+
? eventsWithoutAllDays.filter(
|
|
208
|
+
(event) => !affectedEventIds.has(event.id) || event._isPreview
|
|
209
|
+
)
|
|
210
|
+
: eventsWithoutAllDays,
|
|
211
|
+
[affectedEventIds, eventsWithoutAllDays, hideNonPreviewEvents]
|
|
79
212
|
);
|
|
80
213
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Simple algorithm to assign levels to events
|
|
98
|
-
const assignLevels = () => {
|
|
99
|
-
// Sort events by start time
|
|
100
|
-
const sortedEvents = [...visibleEvents].sort((a, b) => {
|
|
101
|
-
const aStart = new Date(a.start_at).getTime();
|
|
102
|
-
const bStart = new Date(b.start_at).getTime();
|
|
103
|
-
return aStart - bStart;
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Create maps to store event data
|
|
107
|
-
const eventLevels = new Map<string, number>();
|
|
108
|
-
const eventOverlapCounts = new Map<string, number>();
|
|
109
|
-
const eventOverlapGroups = new Map<string, string[]>();
|
|
110
|
-
const eventColumns = new Map<string, number>();
|
|
111
|
-
|
|
112
|
-
// Group events by day
|
|
113
|
-
const eventsByDay = new Map<string, CalendarEvent[]>();
|
|
114
|
-
|
|
115
|
-
// Populate the day groups
|
|
116
|
-
sortedEvents.forEach((event) => {
|
|
117
|
-
const date = new Date(event.start_at);
|
|
118
|
-
const dateKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
119
|
-
|
|
120
|
-
if (!eventsByDay.has(dateKey)) {
|
|
121
|
-
eventsByDay.set(dateKey, []);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const dayEvents = eventsByDay.get(dateKey);
|
|
125
|
-
if (dayEvents) {
|
|
126
|
-
dayEvents.push(event);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Process each day's events
|
|
131
|
-
eventsByDay.forEach((dayEvents) => {
|
|
132
|
-
// Sort by start time
|
|
133
|
-
const sortedDayEvents = [...dayEvents].sort((a, b) => {
|
|
134
|
-
const aStart = new Date(a.start_at).getTime();
|
|
135
|
-
const bStart = new Date(b.start_at).getTime();
|
|
136
|
-
return aStart - bStart;
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Track end times for each level
|
|
140
|
-
const levelEndTimes: number[] = [];
|
|
141
|
-
|
|
142
|
-
// Create a more accurate grouping of overlapping events
|
|
143
|
-
// Helper function to check if two events overlap
|
|
144
|
-
const eventsOverlap = (event1: CalendarEvent, event2: CalendarEvent) => {
|
|
145
|
-
const event1Start = new Date(event1.start_at).getTime();
|
|
146
|
-
const event1End = new Date(event1.end_at).getTime();
|
|
147
|
-
const event2Start = new Date(event2.start_at).getTime();
|
|
148
|
-
const event2End = new Date(event2.end_at).getTime();
|
|
149
|
-
|
|
150
|
-
return event1Start < event2End && event1End > event2Start;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// Create overlap groups
|
|
154
|
-
const overlapGroups: CalendarEvent[][] = [];
|
|
155
|
-
|
|
156
|
-
// Process each event to find its overlap group
|
|
157
|
-
sortedDayEvents.forEach((event) => {
|
|
158
|
-
// Find all groups this event overlaps with
|
|
159
|
-
const overlappingGroupIndices: number[] = [];
|
|
160
|
-
|
|
161
|
-
for (let i = 0; i < overlapGroups.length; i++) {
|
|
162
|
-
const group = overlapGroups[i];
|
|
163
|
-
// Check if event overlaps with any event in this group
|
|
164
|
-
if (group?.some((groupEvent) => eventsOverlap(event, groupEvent))) {
|
|
165
|
-
overlappingGroupIndices.push(i);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (overlappingGroupIndices.length === 0) {
|
|
170
|
-
// No overlapping groups, create a new one
|
|
171
|
-
overlapGroups.push([event]);
|
|
172
|
-
} else {
|
|
173
|
-
// Merge all overlapping groups and add this event
|
|
174
|
-
const newGroup = [event];
|
|
175
|
-
|
|
176
|
-
// Sort indices in descending order to safely remove from array
|
|
177
|
-
overlappingGroupIndices.sort((a, b) => b - a);
|
|
178
|
-
|
|
179
|
-
// Merge all overlapping groups
|
|
180
|
-
overlappingGroupIndices.forEach((index) => {
|
|
181
|
-
newGroup.push(...(overlapGroups[index] ?? []));
|
|
182
|
-
overlapGroups.splice(index, 1);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Add the merged group
|
|
186
|
-
overlapGroups.push(newGroup);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// Now assign column positions using a graph coloring approach
|
|
191
|
-
overlapGroups.forEach((group) => {
|
|
192
|
-
// For each group, assign column positions
|
|
193
|
-
// Column 0 = all non-overlapping events with each other
|
|
194
|
-
// Column 1+ = events that overlap with column 0
|
|
195
|
-
|
|
196
|
-
// Sort group by start time, then duration (longest first)
|
|
197
|
-
const sortedGroup = [...group].sort((a, b) => {
|
|
198
|
-
const aStart = new Date(a.start_at).getTime();
|
|
199
|
-
const bStart = new Date(b.start_at).getTime();
|
|
200
|
-
if (aStart !== bStart) return aStart - bStart;
|
|
201
|
-
|
|
202
|
-
const aDuration =
|
|
203
|
-
new Date(a.end_at).getTime() - new Date(a.start_at).getTime();
|
|
204
|
-
const bDuration =
|
|
205
|
-
new Date(b.end_at).getTime() - new Date(b.start_at).getTime();
|
|
206
|
-
|
|
207
|
-
// Longer events first
|
|
208
|
-
return bDuration - aDuration;
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Assign columns using greedy coloring
|
|
212
|
-
const groupEventColumns = new Map<string, number>();
|
|
213
|
-
const columnEndTimes: number[] = [];
|
|
214
|
-
|
|
215
|
-
sortedGroup.forEach((event) => {
|
|
216
|
-
const eventStart = new Date(event.start_at).getTime();
|
|
217
|
-
const eventEnd = new Date(event.end_at).getTime();
|
|
218
|
-
|
|
219
|
-
// Find the first column where this event can fit
|
|
220
|
-
let column = -1;
|
|
221
|
-
for (let i = 0; i < columnEndTimes.length; i++) {
|
|
222
|
-
if (eventStart >= columnEndTimes[i]!) {
|
|
223
|
-
column = i;
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// If no existing column works, create a new one
|
|
229
|
-
if (column === -1) {
|
|
230
|
-
column = columnEndTimes.length;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
groupEventColumns.set(event.id, column);
|
|
234
|
-
columnEndTimes[column] = eventEnd;
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// Now create the ordered list based on column assignment
|
|
238
|
-
// Column 0 events first (sorted by duration), then column 1, etc.
|
|
239
|
-
const maxColumn = Math.max(...Array.from(groupEventColumns.values()));
|
|
240
|
-
const orderedEventIds: string[] = [];
|
|
241
|
-
|
|
242
|
-
for (let col = 0; col <= maxColumn; col++) {
|
|
243
|
-
const colEvents = sortedGroup
|
|
244
|
-
.filter((e) => groupEventColumns.get(e.id) === col)
|
|
245
|
-
.sort((a, b) => {
|
|
246
|
-
const aDuration =
|
|
247
|
-
new Date(a.end_at).getTime() - new Date(a.start_at).getTime();
|
|
248
|
-
const bDuration =
|
|
249
|
-
new Date(b.end_at).getTime() - new Date(b.start_at).getTime();
|
|
250
|
-
return bDuration - aDuration; // Longest first
|
|
251
|
-
});
|
|
252
|
-
orderedEventIds.push(...colEvents.map((e) => e.id));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// For each event in the group, store the ordered group and column number
|
|
256
|
-
sortedGroup.forEach((event) => {
|
|
257
|
-
eventOverlapCounts.set(event.id, sortedGroup.length);
|
|
258
|
-
eventOverlapGroups.set(event.id, orderedEventIds);
|
|
259
|
-
eventColumns.set(event.id, groupEventColumns.get(event.id) || 0);
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Assign levels (for fallback positioning) using the original algorithm
|
|
264
|
-
sortedDayEvents.forEach((event) => {
|
|
265
|
-
const eventStart = new Date(event.start_at).getTime();
|
|
266
|
-
|
|
267
|
-
// Find the first level where this event can fit
|
|
268
|
-
let level = 0;
|
|
269
|
-
while (level < MAX_LEVEL) {
|
|
270
|
-
if (
|
|
271
|
-
!levelEndTimes[level] ||
|
|
272
|
-
eventStart >= (levelEndTimes?.[level] ?? 0)
|
|
273
|
-
) {
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
level++;
|
|
214
|
+
const allEvents = useMemo(() => {
|
|
215
|
+
const realEventIds = new Set(filteredRealEvents.map((event) => event.id));
|
|
216
|
+
const filteredPreviewEvents = previewEvents.filter(
|
|
217
|
+
(event) => !realEventIds.has(event.id)
|
|
218
|
+
);
|
|
219
|
+
return [...filteredRealEvents, ...filteredPreviewEvents];
|
|
220
|
+
}, [filteredRealEvents, previewEvents]);
|
|
221
|
+
|
|
222
|
+
const visibleEvents = useMemo(() => {
|
|
223
|
+
const nextVisibleEvents: CalendarEvent[] = [];
|
|
224
|
+
|
|
225
|
+
for (const event of allEvents) {
|
|
226
|
+
for (const processedEvent of processCalendarEvent(event, tz)) {
|
|
227
|
+
if (visibleDayKeys.has(getDayKeyFromIso(processedEvent.start_at, tz))) {
|
|
228
|
+
nextVisibleEvents.push(processedEvent);
|
|
277
229
|
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
278
232
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// Store the level for this event
|
|
283
|
-
eventLevels.set(event.id, level);
|
|
233
|
+
return nextVisibleEvents;
|
|
234
|
+
}, [allEvents, tz, visibleDayKeys]);
|
|
284
235
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// Return events with assigned levels and overlap information
|
|
291
|
-
return sortedEvents.map((event) => ({
|
|
292
|
-
...event,
|
|
293
|
-
_level: eventLevels.get(event.id) || 0,
|
|
294
|
-
_overlapCount: eventOverlapCounts.get(event.id) || 1,
|
|
295
|
-
_overlapGroup: eventOverlapGroups.get(event.id) || [event.id],
|
|
296
|
-
_column: eventColumns.get(event.id) || 0,
|
|
297
|
-
}));
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// Get events with levels assigned
|
|
301
|
-
const eventsWithLevels = assignLevels();
|
|
236
|
+
const eventsWithLevels = useMemo(
|
|
237
|
+
() => assignEventLayout(visibleEvents, tz),
|
|
238
|
+
[visibleEvents, tz]
|
|
239
|
+
);
|
|
302
240
|
|
|
303
241
|
const columns = dates.length;
|
|
304
242
|
|