@tuturuuu/ui 0.30.2 → 0.31.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 +41 -0
- package/biome.json +1 -1
- package/package.json +95 -49
- package/src/components/ui/calendar-app/calendar-client-page.test.tsx +57 -0
- package/src/components/ui/calendar-app/calendar-client-page.tsx +13 -4
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +7 -1
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +47 -39
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +14 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-sync-attention-button.tsx +47 -0
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.test.tsx +112 -0
- package/src/components/ui/calendar-app/components/calendar-sync-recovery.tsx +206 -0
- package/src/components/ui/calendar-app/components/calendar-types.ts +1 -1
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +86 -90
- package/src/components/ui/custom/language-dropdown-item.tsx +6 -14
- package/src/components/ui/custom/nav-link.tsx +7 -7
- package/src/components/ui/custom/nav.tsx +15 -63
- package/src/components/ui/custom/notification-card.tsx +361 -0
- package/src/components/ui/custom/notification-list.tsx +178 -0
- package/src/components/ui/custom/notification-popover-client.tsx +12 -780
- package/src/components/ui/custom/notification-runtime.tsx +23 -0
- package/src/components/ui/custom/onboarding-settings-panel.tsx +8 -55
- package/src/components/ui/custom/satellite-account-switcher-menu.tsx +212 -0
- package/src/components/ui/custom/satellite-brand.tsx +122 -0
- package/src/components/ui/custom/satellite-content.tsx +148 -0
- package/src/components/ui/custom/satellite-footer-actions.tsx +350 -0
- package/src/components/ui/custom/satellite-language-item.tsx +26 -0
- package/src/components/ui/custom/satellite-navigation.tsx +86 -0
- package/src/components/ui/custom/satellite-notification-popover.tsx +272 -0
- package/src/components/ui/custom/satellite-onboarding-settings-panel.tsx +80 -0
- package/src/components/ui/custom/satellite-settings-dialog-shell.tsx +621 -0
- package/src/components/ui/custom/satellite-shell-utils.ts +163 -0
- package/src/components/ui/custom/satellite-shell.test.tsx +169 -0
- package/src/components/ui/custom/satellite-shell.tsx +50 -0
- package/src/components/ui/custom/satellite-theme-dropdown-items.tsx +92 -0
- package/src/components/ui/custom/satellite-user-menu-items.tsx +190 -0
- package/src/components/ui/custom/satellite-user-menu.tsx +93 -0
- package/src/components/ui/custom/satellite-workspace-section.tsx +46 -0
- package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +16 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +21 -600
- package/src/components/ui/custom/sidebar-context.tsx +5 -14
- package/src/components/ui/custom/sidebar-footer-actions.tsx +19 -322
- package/src/components/ui/custom/structure.tsx +14 -0
- package/src/components/ui/custom/system-language-dropdown-item.tsx +7 -14
- package/src/components/ui/custom/theme-dropdown-items.tsx +2 -85
- package/src/components/ui/custom/use-satellite-shell.tsx +165 -0
- package/src/components/ui/legacy/calendar/agenda-view.tsx +139 -47
- package/src/components/ui/legacy/calendar/calendar-content.test.tsx +116 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +38 -74
- package/src/components/ui/legacy/calendar/calendar-event-tone.ts +20 -0
- package/src/components/ui/legacy/calendar/calendar-meeting-link.test.ts +27 -0
- package/src/components/ui/legacy/calendar/calendar-meeting-link.ts +34 -0
- package/src/components/ui/legacy/calendar/calendar-period.ts +28 -0
- package/src/components/ui/legacy/calendar/calendar-shortcuts.test.tsx +107 -0
- package/src/components/ui/legacy/calendar/calendar-views.test.tsx +169 -0
- package/src/components/ui/legacy/calendar/event-modal-header.tsx +52 -0
- package/src/components/ui/legacy/calendar/event-modal.tsx +107 -156
- package/src/components/ui/legacy/calendar/month-calendar.tsx +242 -933
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +25 -0
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +32 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +3 -0
- package/src/components/ui/legacy/calendar/use-calendar-view-shortcuts.ts +54 -0
- package/src/components/ui/legacy/calendar/use-event-draft-session.test.tsx +59 -0
- package/src/components/ui/legacy/calendar/use-event-draft-session.ts +20 -0
- package/src/components/ui/legacy/calendar/year-calendar.tsx +34 -16
- package/src/components/ui/pwa-status.test.tsx +53 -0
- package/src/components/ui/pwa-status.tsx +113 -0
- package/src/components/ui/report-problem-dialog-content.tsx +350 -0
- package/src/components/ui/report-problem-dialog.tsx +6 -975
- package/src/components/ui/report-problem-model.ts +133 -0
- package/src/components/ui/report-problem-mutation.ts +122 -0
- package/src/components/ui/save-notification.test.tsx +36 -0
- package/src/components/ui/save-notification.tsx +17 -0
- package/src/components/ui/use-report-problem.ts +488 -0
- package/src/declarations.d.ts +2 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +46 -4
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +24 -0
- package/src/hooks/calendar-date-utils.ts +11 -0
- package/src/hooks/calendar-event-lookup.test.ts +77 -0
- package/src/hooks/calendar-event-lookup.ts +47 -0
- package/src/hooks/calendar-range-cache.ts +52 -0
- package/src/hooks/pwa-install-prompt.ts +44 -0
- package/src/hooks/use-calendar-sync.tsx +23 -100
- package/src/hooks/use-calendar.tsx +21 -77
- package/src/hooks/use-notifications.ts +9 -24
- package/src/hooks/use-open-initial-calendar-event.ts +27 -0
- package/src/hooks/use-settings-dialog-shortcut.test.tsx +41 -0
- package/src/hooks/use-settings-dialog-shortcut.ts +12 -14
- package/src/hooks/use-workspace-presence.ts +0 -18
- package/src/lib/tasks-app-url.test.ts +25 -0
- package/src/lib/tasks-app-url.ts +8 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import timezone from 'dayjs/plugin/timezone';
|
|
3
|
+
import utc from 'dayjs/plugin/utc';
|
|
4
|
+
|
|
5
|
+
dayjs.extend(utc);
|
|
6
|
+
dayjs.extend(timezone);
|
|
7
|
+
|
|
8
|
+
/** Endpoints are inclusive days; the event query expands the final day. */
|
|
9
|
+
export function calendarPeriodDates(
|
|
10
|
+
date: Date,
|
|
11
|
+
view: 'year' | 'agenda'
|
|
12
|
+
): Date[] {
|
|
13
|
+
const start = new Date(date);
|
|
14
|
+
start.setHours(0, 0, 0, 0);
|
|
15
|
+
if (view === 'year') start.setMonth(0, 1);
|
|
16
|
+
const end = new Date(start);
|
|
17
|
+
if (view === 'year') end.setMonth(11, 31);
|
|
18
|
+
else end.setDate(end.getDate() + 29);
|
|
19
|
+
return [start, end];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Create at 9 AM on the selected calendar day, in its configured zone. */
|
|
23
|
+
export function calendarDraftDate(date: Date, zone?: string): Date {
|
|
24
|
+
const wallTime = `${dayjs(date).format('YYYY-MM-DD')}T09:00:00`;
|
|
25
|
+
return (
|
|
26
|
+
zone && zone !== 'auto' ? dayjs.tz(wallTime, zone) : dayjs(wallTime)
|
|
27
|
+
).toDate();
|
|
28
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import type { CalendarView } from '../../../../hooks/use-view-transition';
|
|
5
|
+
import { calendarDraftDate, calendarPeriodDates } from './calendar-period';
|
|
6
|
+
import {
|
|
7
|
+
calendarShortcutView,
|
|
8
|
+
useCalendarViewShortcuts,
|
|
9
|
+
} from './use-calendar-view-shortcuts';
|
|
10
|
+
|
|
11
|
+
function ControlledViews({ disabledWeek = false }: { disabledWeek?: boolean }) {
|
|
12
|
+
const [view, setView] = useState<CalendarView>('week');
|
|
13
|
+
useCalendarViewShortcuts({
|
|
14
|
+
enabled: true,
|
|
15
|
+
availableViews: disabledWeek
|
|
16
|
+
? [
|
|
17
|
+
{ value: 'week', disabled: true },
|
|
18
|
+
{ value: '4-days', disabled: true },
|
|
19
|
+
{ value: 'month' },
|
|
20
|
+
]
|
|
21
|
+
: undefined,
|
|
22
|
+
day: () => setView('day'),
|
|
23
|
+
'4-days': () => setView('4-days'),
|
|
24
|
+
week: () => setView('week'),
|
|
25
|
+
month: () => setView('month'),
|
|
26
|
+
year: () => setView('year'),
|
|
27
|
+
agenda: () => setView('agenda'),
|
|
28
|
+
});
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<output>{view}</output>
|
|
32
|
+
<input aria-label="Event title" />
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
describe('calendar keyboard navigation', () => {
|
|
37
|
+
it('switches views with M and preserves typing', () => {
|
|
38
|
+
render(<ControlledViews />);
|
|
39
|
+
fireEvent.keyDown(window, { key: 'M' });
|
|
40
|
+
expect(screen.getByText('month')).toBeVisible();
|
|
41
|
+
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'y' });
|
|
42
|
+
expect(screen.getByText('month')).toBeVisible();
|
|
43
|
+
fireEvent.keyDown(window, { key: 'a' });
|
|
44
|
+
expect(screen.getByText('agenda')).toBeVisible();
|
|
45
|
+
});
|
|
46
|
+
it.each([
|
|
47
|
+
{ ctrlKey: true },
|
|
48
|
+
{ metaKey: true },
|
|
49
|
+
{ altKey: true },
|
|
50
|
+
{ shiftKey: true },
|
|
51
|
+
{ isComposing: true },
|
|
52
|
+
{ repeat: true },
|
|
53
|
+
])('ignores competing commands %j', (options) => {
|
|
54
|
+
expect(
|
|
55
|
+
calendarShortcutView(
|
|
56
|
+
new KeyboardEvent('keydown', { key: 'm', ...options })
|
|
57
|
+
)
|
|
58
|
+
).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
it('does not invoke views disabled for the current screen', () => {
|
|
61
|
+
render(<ControlledViews disabledWeek />);
|
|
62
|
+
fireEvent.keyDown(window, { key: 'm' });
|
|
63
|
+
fireEvent.keyDown(window, { key: 'w' });
|
|
64
|
+
fireEvent.keyDown(window, { key: '4' });
|
|
65
|
+
expect(screen.getByText('month')).toBeVisible();
|
|
66
|
+
});
|
|
67
|
+
it('does not change views behind a modal', () => {
|
|
68
|
+
render(
|
|
69
|
+
<div role="dialog" data-state="open">
|
|
70
|
+
Settings
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
expect(
|
|
74
|
+
calendarShortcutView(new KeyboardEvent('keydown', { key: 'm' }))
|
|
75
|
+
).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('event ranges', () => {
|
|
79
|
+
it('creates 9 AM drafts in the selected zone without caller plugin setup', () => {
|
|
80
|
+
expect(
|
|
81
|
+
calendarDraftDate(new Date(2026, 8, 7), 'Asia/Ho_Chi_Minh').toISOString()
|
|
82
|
+
).toBe('2026-09-07T02:00:00.000Z');
|
|
83
|
+
});
|
|
84
|
+
it('fetches the entire leap year', () => {
|
|
85
|
+
const dates = calendarPeriodDates(new Date(2028, 8, 7), 'year');
|
|
86
|
+
expect(
|
|
87
|
+
dates.map((date) => [date.getFullYear(), date.getMonth(), date.getDate()])
|
|
88
|
+
).toEqual([
|
|
89
|
+
[2028, 0, 1],
|
|
90
|
+
[2028, 11, 31],
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
it('fetches all 30 agenda days across a year boundary', () => {
|
|
94
|
+
const dates = calendarPeriodDates(new Date(2026, 11, 20, 15), 'agenda');
|
|
95
|
+
expect(
|
|
96
|
+
dates.map((date) => [
|
|
97
|
+
date.getFullYear(),
|
|
98
|
+
date.getMonth(),
|
|
99
|
+
date.getDate(),
|
|
100
|
+
date.getHours(),
|
|
101
|
+
])
|
|
102
|
+
).toEqual([
|
|
103
|
+
[2026, 11, 20, 0],
|
|
104
|
+
[2027, 0, 18, 0],
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { fireEvent, render, screen, within } from '@testing-library/react';
|
|
2
|
+
import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const state = vi.hoisted(() => ({
|
|
6
|
+
openModal: vi.fn(),
|
|
7
|
+
addEmptyEvent: vi.fn(),
|
|
8
|
+
events: [] as CalendarEvent[],
|
|
9
|
+
weekStartsOn: 0,
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('@tuturuuu/ui/hooks/use-calendar', () => ({
|
|
12
|
+
useCalendar: () => ({
|
|
13
|
+
...state,
|
|
14
|
+
getCurrentEvents: (day: Date) =>
|
|
15
|
+
state.events.filter(
|
|
16
|
+
(event) =>
|
|
17
|
+
new Date(event.start_at).toDateString() === day.toDateString()
|
|
18
|
+
),
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
vi.mock('@tuturuuu/ui/hooks/use-calendar-preferences', () => ({
|
|
22
|
+
useCalendarPreferences: () => ({
|
|
23
|
+
timeFormat: '24h',
|
|
24
|
+
weekStartsOn: state.weekStartsOn,
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
vi.mock('@tuturuuu/ui/hooks/use-user-config', () => ({
|
|
28
|
+
useUserBooleanConfig: () => ({ value: false }),
|
|
29
|
+
}));
|
|
30
|
+
vi.mock('./settings/settings-context', () => ({
|
|
31
|
+
useCalendarSettings: () => ({
|
|
32
|
+
settings: {
|
|
33
|
+
appearance: { showWeekends: true },
|
|
34
|
+
timezone: { timezone: 'America/New_York' },
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
vi.mock('next-intl', () => ({
|
|
39
|
+
useTranslations: () => (key: string, values?: Record<string, unknown>) =>
|
|
40
|
+
`${key}${values ? ` ${Object.values(values).join(' ')}` : ''}`,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
import { AgendaView } from './agenda-view';
|
|
44
|
+
import { MonthCalendar } from './month-calendar';
|
|
45
|
+
import { YearCalendar } from './year-calendar';
|
|
46
|
+
|
|
47
|
+
const date = new Date(2026, 8, 7);
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
state.weekStartsOn = 0;
|
|
50
|
+
state.openModal.mockClear();
|
|
51
|
+
state.addEmptyEvent.mockClear();
|
|
52
|
+
state.events = Array.from(
|
|
53
|
+
{ length: 5 },
|
|
54
|
+
(_, i) =>
|
|
55
|
+
({
|
|
56
|
+
id: String(i),
|
|
57
|
+
title: `Event ${i}`,
|
|
58
|
+
start_at: new Date(2026, 8, 7, 9 + i).toISOString(),
|
|
59
|
+
end_at: new Date(2026, 8, 7, 10 + i).toISOString(),
|
|
60
|
+
color: 'BLUE',
|
|
61
|
+
location: i === 4 ? 'Library' : undefined,
|
|
62
|
+
}) as CalendarEvent
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
describe('calendar view interactions', () => {
|
|
66
|
+
it('month overflow opens every event and creates on the chosen calendar date and timezone', () => {
|
|
67
|
+
render(<MonthCalendar date={date} viewedMonth={date} locale="en" />);
|
|
68
|
+
const day = screen.getByRole('region', {
|
|
69
|
+
name: date.toLocaleDateString('en', { dateStyle: 'full' }),
|
|
70
|
+
});
|
|
71
|
+
expect(within(day).getAllByRole('button', { name: /Event/ })).toHaveLength(
|
|
72
|
+
3
|
|
73
|
+
);
|
|
74
|
+
fireEvent.click(
|
|
75
|
+
within(day).getByRole('button', { name: /views.open_day/ })
|
|
76
|
+
);
|
|
77
|
+
fireEvent.click(screen.getByRole('button', { name: /Event 4/ }));
|
|
78
|
+
expect(state.openModal).toHaveBeenCalledWith('4');
|
|
79
|
+
fireEvent.click(
|
|
80
|
+
within(day).getByRole('button', { name: /views.create_on_date/ })
|
|
81
|
+
);
|
|
82
|
+
expect(state.addEmptyEvent.mock.calls[0]?.[0].toISOString()).toBe(
|
|
83
|
+
'2026-09-07T13:00:00.000Z'
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
it('uses the preferred first weekday unless visible dates specify one', () => {
|
|
87
|
+
const { rerender } = render(<MonthCalendar date={date} locale="en" />);
|
|
88
|
+
expect(screen.getAllByRole('region')[0]).toHaveAccessibleName(
|
|
89
|
+
'Sunday, August 30, 2026'
|
|
90
|
+
);
|
|
91
|
+
state.weekStartsOn = 1;
|
|
92
|
+
rerender(<MonthCalendar date={date} locale="en" />);
|
|
93
|
+
expect(screen.getAllByRole('region')[0]).toHaveAccessibleName(
|
|
94
|
+
'Monday, August 31, 2026'
|
|
95
|
+
);
|
|
96
|
+
rerender(
|
|
97
|
+
<MonthCalendar
|
|
98
|
+
date={date}
|
|
99
|
+
locale="en"
|
|
100
|
+
visibleDates={[new Date(2026, 8, 6)]}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
expect(screen.getAllByRole('region')[0]).toHaveAccessibleName(
|
|
104
|
+
'Sunday, September 6, 2026'
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
it('formats month events in the selected timezone', () => {
|
|
108
|
+
state.events = [
|
|
109
|
+
{
|
|
110
|
+
id: 'zoned',
|
|
111
|
+
title: 'Zoned',
|
|
112
|
+
start_at: '2026-09-07T13:00:00Z',
|
|
113
|
+
end_at: '2026-09-07T14:00:00Z',
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
render(<MonthCalendar date={date} locale="en" />);
|
|
117
|
+
expect(screen.getByRole('button', { name: /Zoned/ })).toHaveTextContent(
|
|
118
|
+
'09:00'
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
it('disables read-only creation and uses 9am calendar time for agenda drafts', () => {
|
|
122
|
+
const { rerender } = render(<AgendaView startDate={date} readOnly />);
|
|
123
|
+
const create = screen.getByRole('button', { name: 'views.create_event' });
|
|
124
|
+
expect(create).toBeDisabled();
|
|
125
|
+
fireEvent.click(create);
|
|
126
|
+
expect(state.addEmptyEvent).not.toHaveBeenCalled();
|
|
127
|
+
rerender(<AgendaView startDate={date} />);
|
|
128
|
+
fireEvent.click(screen.getByRole('button', { name: 'views.create_event' }));
|
|
129
|
+
expect(state.addEmptyEvent.mock.calls[0]?.[0].toISOString()).toBe(
|
|
130
|
+
'2026-09-07T13:00:00.000Z'
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
it('agenda filters by location, opens events, and clears a no-match search', () => {
|
|
134
|
+
render(<AgendaView startDate={date} locale="vi" />);
|
|
135
|
+
fireEvent.change(screen.getByRole('textbox'), {
|
|
136
|
+
target: { value: 'Library' },
|
|
137
|
+
});
|
|
138
|
+
expect(screen.getByText('Event 4')).toBeVisible();
|
|
139
|
+
expect(screen.queryByText('Event 0')).toBeNull();
|
|
140
|
+
fireEvent.click(screen.getByText('Event 4'));
|
|
141
|
+
expect(state.openModal).toHaveBeenCalledWith('4');
|
|
142
|
+
fireEvent.change(screen.getByRole('textbox'), {
|
|
143
|
+
target: { value: 'No match' },
|
|
144
|
+
});
|
|
145
|
+
expect(screen.getByText('views.no_matches')).toBeVisible();
|
|
146
|
+
fireEvent.click(screen.getByRole('button', { name: 'views.clear_search' }));
|
|
147
|
+
expect(screen.getByText('Event 0')).toBeVisible();
|
|
148
|
+
});
|
|
149
|
+
it('year localizes month labels and supports month and day drill-down', () => {
|
|
150
|
+
const onMonthClick = vi.fn();
|
|
151
|
+
const onDayClick = vi.fn();
|
|
152
|
+
render(
|
|
153
|
+
<YearCalendar
|
|
154
|
+
year={2026}
|
|
155
|
+
locale="vi"
|
|
156
|
+
onMonthClick={onMonthClick}
|
|
157
|
+
onDayClick={onDayClick}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
fireEvent.click(screen.getByRole('button', { name: /^Tháng 9/i }));
|
|
161
|
+
expect(onMonthClick.mock.calls[0]?.[0].getMonth()).toBe(8);
|
|
162
|
+
fireEvent.click(
|
|
163
|
+
screen.getByRole('button', {
|
|
164
|
+
name: new RegExp(date.toLocaleDateString('vi', { dateStyle: 'full' })),
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
expect(onDayClick.mock.calls[0]?.[0].getDate()).toBe(7);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ExternalLink, Video } from '@tuturuuu/icons';
|
|
2
|
+
import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
|
|
3
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogHeader,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
} from '@tuturuuu/ui/dialog';
|
|
9
|
+
import { getCalendarMeetingUrl } from './calendar-meeting-link';
|
|
10
|
+
import {
|
|
11
|
+
CalendarEventProviderIcon,
|
|
12
|
+
getCalendarEventProviderDisplay,
|
|
13
|
+
} from './event-provider-display';
|
|
14
|
+
|
|
15
|
+
export function EventModalHeader({
|
|
16
|
+
event,
|
|
17
|
+
isEditing,
|
|
18
|
+
}: {
|
|
19
|
+
event: Partial<CalendarEvent>;
|
|
20
|
+
isEditing: boolean;
|
|
21
|
+
}) {
|
|
22
|
+
const providerDisplay = getCalendarEventProviderDisplay(event);
|
|
23
|
+
const meetingUrl = getCalendarMeetingUrl(event);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<DialogHeader className="border-b px-6 pt-6 pb-4">
|
|
27
|
+
<DialogTitle className="flex flex-wrap items-center gap-2 font-semibold text-xl">
|
|
28
|
+
<span>{isEditing ? 'Edit Event' : 'Create Event'}</span>
|
|
29
|
+
{providerDisplay && (
|
|
30
|
+
<span className="ml-3 flex items-center gap-2 rounded-md border bg-muted/40 px-3 py-1 text-sm">
|
|
31
|
+
<CalendarEventProviderIcon className="h-4.5 w-4.5" event={event} />
|
|
32
|
+
<span className="font-medium text-xs">{providerDisplay.label}</span>
|
|
33
|
+
</span>
|
|
34
|
+
)}
|
|
35
|
+
{meetingUrl && (
|
|
36
|
+
<Button asChild className="ml-auto" size="sm" variant="outline">
|
|
37
|
+
<a href={meetingUrl} rel="noreferrer" target="_blank">
|
|
38
|
+
<Video className="size-4" />
|
|
39
|
+
Tuturuuu Meet
|
|
40
|
+
<ExternalLink className="size-3.5" />
|
|
41
|
+
</a>
|
|
42
|
+
</Button>
|
|
43
|
+
)}
|
|
44
|
+
</DialogTitle>
|
|
45
|
+
<DialogDescription>
|
|
46
|
+
{isEditing
|
|
47
|
+
? 'Make changes to your existing event'
|
|
48
|
+
: 'Add a new event to your calendar'}
|
|
49
|
+
</DialogDescription>
|
|
50
|
+
</DialogHeader>
|
|
51
|
+
);
|
|
52
|
+
}
|