@tuturuuu/ui 0.23.0 → 0.24.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 +29 -0
- package/biome.json +1 -1
- package/package.json +78 -78
- package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.test.tsx +121 -0
- package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.tsx +37 -4
- package/src/components/ui/chat/ai-message-markdown.tsx +6 -1
- package/src/components/ui/chat/chat-sidebar-groups.test.ts +33 -0
- package/src/components/ui/chat/chat-sidebar-items.tsx +10 -2
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +29 -1
- package/src/components/ui/chat/chat-sidebar-panel.tsx +4 -3
- package/src/components/ui/chat/chat-sidebar-sections.ts +24 -10
- package/src/components/ui/chat/chat-utils.test.ts +107 -0
- package/src/components/ui/chat/chat-workspace-header.tsx +11 -5
- package/src/components/ui/chat/chat-workspace.tsx +66 -24
- package/src/components/ui/chat/hooks-conversations.ts +9 -1
- package/src/components/ui/chat/hooks-messages.ts +1 -0
- package/src/components/ui/chat/message-bubble.tsx +49 -4
- package/src/components/ui/chat/message-composer.tsx +29 -23
- package/src/components/ui/chat/message-markdown.tsx +4 -1
- package/src/components/ui/chat/query-keys.ts +7 -1
- package/src/components/ui/chat/utils.ts +85 -3
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +72 -0
- package/src/components/ui/custom/onboarding-settings-panel.tsx +131 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +55 -18
- package/src/components/ui/custom/workspace-select-helpers.ts +50 -0
- package/src/components/ui/custom/workspace-select.tsx +37 -38
- package/src/components/ui/finance/analytics/analytics-page.tsx +10 -0
- package/src/components/ui/finance/analytics/inventory-provider-summary.tsx +110 -0
- package/src/components/ui/finance/finance-overview-metrics.tsx +49 -0
- package/src/components/ui/finance/transactions/inventory-reconciliation-entry-row.tsx +122 -0
- package/src/components/ui/finance/transactions/inventory-reconciliation-panel.tsx +407 -0
- package/src/components/ui/finance/transactions/transaction-card.tsx +15 -0
- package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +31 -0
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +26 -4
- package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +22 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +8 -0
- package/src/components/ui/finance/wallets/walletId/inventory-wallet-contribution.tsx +84 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +9 -0
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +27 -10
- package/src/components/ui/legacy/meet/edit-plan-dialog.tsx +90 -26
- package/src/components/ui/legacy/meet/form.tsx +25 -1
- package/src/components/ui/legacy/meet/lib/meet-ics.test.ts +40 -0
- package/src/components/ui/legacy/meet/lib/meet-ics.ts +50 -0
- package/src/components/ui/legacy/meet/lib/meet-insights.test.ts +76 -0
- package/src/components/ui/legacy/meet/lib/meet-insights.ts +219 -0
- package/src/components/ui/legacy/meet/lib/meet-timezone.test.ts +53 -0
- package/src/components/ui/legacy/meet/lib/meet-timezone.ts +45 -0
- package/src/components/ui/legacy/meet/page.tsx +1 -0
- package/src/components/ui/legacy/meet/planId/agenda-details.tsx +46 -23
- package/src/components/ui/legacy/meet/planId/copy-link-button.tsx +24 -197
- package/src/components/ui/legacy/meet/planId/date-planner.tsx +3 -28
- package/src/components/ui/legacy/meet/planId/meet-insights-panel.tsx +135 -0
- package/src/components/ui/legacy/meet/planId/meet-plan-live-root.tsx +41 -0
- package/src/components/ui/legacy/meet/planId/meet-query.ts +26 -0
- package/src/components/ui/legacy/meet/planId/meet-suggestions-panel.tsx +463 -0
- package/src/components/ui/legacy/meet/planId/page.tsx +58 -18
- package/src/components/ui/legacy/meet/planId/plan-details-client.tsx +154 -140
- package/src/components/ui/legacy/meet/planId/selectable-day-time.tsx +88 -56
- package/src/components/ui/legacy/meet/planId/show-qr-button.tsx +3 -5
- package/src/components/ui/legacy/meet/planId/unified-availability.tsx +36 -13
- package/src/components/ui/legacy/meet/planId/utility-buttons.tsx +3 -24
- package/src/components/ui/legacy/meet/plans-grid.tsx +1 -1
- package/src/components/ui/legacy/meet/plans-list-view.tsx +1 -1
- package/src/components/ui/nuqs-adapter.tsx +1 -0
- package/src/hooks/time-blocking-provider.tsx +127 -211
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Activity,
|
|
5
|
+
CalendarClock,
|
|
6
|
+
CircleQuestionMark,
|
|
7
|
+
Users,
|
|
8
|
+
} from '@tuturuuu/icons';
|
|
4
9
|
import type {
|
|
10
|
+
MeetFinalizedTimeframe,
|
|
5
11
|
MeetTogetherPlan,
|
|
6
12
|
PlanUser,
|
|
7
13
|
} from '@tuturuuu/types/primitives/MeetTogetherPlan';
|
|
8
14
|
import type { GetPollsForPlanResult } from '@tuturuuu/types/primitives/Poll';
|
|
9
15
|
import type { Timeblock } from '@tuturuuu/types/primitives/Timeblock';
|
|
16
|
+
import { Badge } from '@tuturuuu/ui/badge';
|
|
10
17
|
import { useTimeBlocking } from '@tuturuuu/ui/hooks/time-blocking-provider';
|
|
11
18
|
import { Label } from '@tuturuuu/ui/label';
|
|
12
19
|
import EditPlanDialog from '@tuturuuu/ui/legacy/meet/edit-plan-dialog';
|
|
13
|
-
import { Separator } from '@tuturuuu/ui/separator';
|
|
14
20
|
import { Switch } from '@tuturuuu/ui/switch';
|
|
21
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
|
|
15
22
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
|
|
16
|
-
import {
|
|
23
|
+
import { useTranslations } from 'next-intl';
|
|
17
24
|
import { useTheme } from 'next-themes';
|
|
18
|
-
import { useCallback,
|
|
25
|
+
import { useCallback, useState } from 'react';
|
|
19
26
|
import AgendaDetails from './agenda-details';
|
|
27
|
+
import { MeetInsightsPanel } from './meet-insights-panel';
|
|
28
|
+
import { MeetSuggestionsPanel } from './meet-suggestions-panel';
|
|
20
29
|
import PlanLogin from './plan-login';
|
|
21
30
|
import SidebarDisplay from './sidebar-display';
|
|
22
31
|
import StickyBottomIndicator from './sticky-bottom-indicator';
|
|
@@ -26,179 +35,184 @@ import UtilityButtons from './utility-buttons';
|
|
|
26
35
|
interface PlanDetailsClientProps {
|
|
27
36
|
plan: MeetTogetherPlan;
|
|
28
37
|
polls: GetPollsForPlanResult | null;
|
|
29
|
-
// platformUser: User | null;
|
|
30
|
-
// isCreator: boolean;
|
|
31
38
|
users: PlanUser[];
|
|
32
39
|
timeblocks: Timeblock[];
|
|
40
|
+
finalizedTimeframes: MeetFinalizedTimeframe[];
|
|
41
|
+
isCreator: boolean;
|
|
42
|
+
isRefreshing: boolean;
|
|
33
43
|
baseUrl: string;
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
export default function PlanDetailsClient({
|
|
37
47
|
plan,
|
|
38
|
-
// platformUser,
|
|
39
|
-
// isCreator,
|
|
40
48
|
users,
|
|
41
49
|
polls,
|
|
42
50
|
timeblocks,
|
|
51
|
+
finalizedTimeframes,
|
|
52
|
+
isCreator,
|
|
53
|
+
isRefreshing,
|
|
43
54
|
baseUrl,
|
|
44
55
|
}: PlanDetailsClientProps) {
|
|
45
56
|
const { resolvedTheme } = useTheme();
|
|
57
|
+
const t = useTranslations('meet-together-plan-details');
|
|
46
58
|
const [showBestTimes, setShowBestTimes] = useState(false);
|
|
47
59
|
const { filteredUserIds, isDirty, resetLocalTimeblocks, user } =
|
|
48
60
|
useTimeBlocking();
|
|
49
|
-
|
|
50
|
-
// If user filter is active, force best times off
|
|
51
|
-
const isUserFilterActive = filteredUserIds && filteredUserIds.length > 0;
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (isUserFilterActive && showBestTimes) {
|
|
55
|
-
setShowBestTimes(false);
|
|
56
|
-
}
|
|
57
|
-
}, [isUserFilterActive, showBestTimes]);
|
|
58
|
-
|
|
59
|
-
// Best times status state
|
|
60
|
-
const [bestTimesStatusByDate, setBestTimesStatusByDate] = useState<
|
|
61
|
-
Record<string, boolean>
|
|
62
|
-
>({});
|
|
63
|
-
const allDates = plan.dates || [];
|
|
64
|
-
const noBestTimesFound =
|
|
65
|
-
showBestTimes &&
|
|
66
|
-
allDates.length > 0 &&
|
|
67
|
-
allDates.every((d) => bestTimesStatusByDate[d] === false);
|
|
61
|
+
const isUserFilterActive = filteredUserIds.length > 0;
|
|
68
62
|
|
|
69
63
|
const downloadAsPNG = useCallback(async () => {
|
|
70
64
|
const element = document.getElementById('plan-ref');
|
|
71
65
|
if (!element) throw new Error('Plan element not found');
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Array.from(clonedDoc.getElementsByTagName('link')).forEach(
|
|
85
|
-
(link: HTMLLinkElement) => {
|
|
86
|
-
link.removeAttribute('integrity');
|
|
87
|
-
link.removeAttribute('crossorigin');
|
|
88
|
-
}
|
|
89
|
-
);
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const link = document.createElement('a');
|
|
94
|
-
link.download = `plan-${plan.id}.png`;
|
|
95
|
-
link.href = canvas.toDataURL('image/png', 2.0);
|
|
96
|
-
link.click();
|
|
97
|
-
} catch (error) {
|
|
98
|
-
console.error('Error generating PNG:', error);
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
66
|
+
const html2canvas = (await import('html2canvas-pro')).default;
|
|
67
|
+
const canvas = await html2canvas(element, {
|
|
68
|
+
useCORS: true,
|
|
69
|
+
allowTaint: true,
|
|
70
|
+
backgroundColor: resolvedTheme === 'dark' ? '#0a0a0a' : '#ffffff',
|
|
71
|
+
scale: 1,
|
|
72
|
+
logging: false,
|
|
73
|
+
});
|
|
74
|
+
const link = document.createElement('a');
|
|
75
|
+
link.download = `tuturuuu-meet-${plan.id}.png`;
|
|
76
|
+
link.href = canvas.toDataURL('image/png', 2);
|
|
77
|
+
link.click();
|
|
101
78
|
}, [plan.id, resolvedTheme]);
|
|
102
79
|
|
|
103
80
|
return (
|
|
104
81
|
<>
|
|
105
|
-
<
|
|
106
|
-
<div className="
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
82
|
+
<main className="relative min-h-dvh w-full overflow-hidden bg-background text-foreground">
|
|
83
|
+
<div className="pointer-events-none absolute inset-x-0 top-0 -z-0 h-72 bg-[radial-gradient(circle_at_top_left,hsl(var(--primary)/0.10),transparent_52%)]" />
|
|
84
|
+
<div className="relative mx-auto flex w-full max-w-[1560px] flex-col gap-5 px-3 py-4 sm:px-5 lg:px-8">
|
|
85
|
+
<header className="rounded-2xl border bg-background/90 p-4 shadow-sm backdrop-blur-xl sm:p-5">
|
|
86
|
+
<div className="flex flex-col justify-between gap-5 xl:flex-row xl:items-start">
|
|
87
|
+
<div className="min-w-0">
|
|
88
|
+
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
89
|
+
<Badge variant={plan.is_confirmed ? 'default' : 'secondary'}>
|
|
90
|
+
{plan.is_confirmed
|
|
91
|
+
? t('finalized')
|
|
92
|
+
: t('collecting_availability')}
|
|
93
|
+
</Badge>
|
|
94
|
+
<Badge variant="outline" className="tabular-nums">
|
|
95
|
+
{t('minutes_short', { count: plan.duration_minutes ?? 60 })}
|
|
96
|
+
</Badge>
|
|
97
|
+
<Badge variant="outline">
|
|
98
|
+
{plan.timezone || t('legacy_fixed_offset')}
|
|
99
|
+
</Badge>
|
|
100
|
+
<span className="flex items-center gap-1.5 text-muted-foreground text-xs">
|
|
101
|
+
<span
|
|
102
|
+
className={
|
|
103
|
+
isRefreshing
|
|
104
|
+
? 'h-1.5 w-1.5 animate-pulse rounded-full bg-primary'
|
|
105
|
+
: 'h-1.5 w-1.5 rounded-full bg-muted-foreground/40'
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
{isRefreshing ? t('refreshing') : t('up_to_date')}
|
|
109
|
+
</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="flex items-center gap-2">
|
|
112
|
+
<h1 className="text-balance font-semibold text-2xl tracking-tight sm:text-3xl">
|
|
113
|
+
{plan.name || t('untitled_meeting')}
|
|
114
|
+
</h1>
|
|
115
|
+
{user?.id === plan.creator_id && !plan.is_confirmed ? (
|
|
116
|
+
<EditPlanDialog
|
|
117
|
+
plan={plan}
|
|
118
|
+
onSuccess={resetLocalTimeblocks}
|
|
119
|
+
/>
|
|
120
|
+
) : null}
|
|
121
|
+
</div>
|
|
122
|
+
<p className="mt-2 max-w-3xl text-pretty text-muted-foreground text-sm">
|
|
123
|
+
{plan.description || t('default_plan_description')}
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
<UtilityButtons plan={plan} handlePNG={downloadAsPNG} />
|
|
127
|
+
</div>
|
|
128
|
+
</header>
|
|
129
|
+
|
|
130
|
+
<div id="plan-ref">
|
|
131
|
+
<Tabs defaultValue="availability" className="space-y-5">
|
|
132
|
+
<TabsList className="grid h-auto w-full grid-cols-3 rounded-xl border bg-muted/30 p-1 sm:w-fit sm:min-w-[28rem]">
|
|
133
|
+
<TabsTrigger value="availability" className="gap-2 py-2.5">
|
|
134
|
+
<CalendarClock className="h-4 w-4" />
|
|
135
|
+
<span className="hidden sm:inline">{t('availability')}</span>
|
|
136
|
+
<span className="sm:hidden">{t('times')}</span>
|
|
137
|
+
</TabsTrigger>
|
|
138
|
+
<TabsTrigger value="suggestions" className="gap-2 py-2.5">
|
|
139
|
+
<Activity className="h-4 w-4" /> {t('suggestions')}
|
|
140
|
+
</TabsTrigger>
|
|
141
|
+
<TabsTrigger value="insights" className="gap-2 py-2.5">
|
|
142
|
+
<Users className="h-4 w-4" /> {t('insights')}
|
|
143
|
+
</TabsTrigger>
|
|
144
|
+
</TabsList>
|
|
145
|
+
|
|
146
|
+
<TabsContent value="availability" className="mt-0">
|
|
147
|
+
<div className="grid items-start gap-5 xl:grid-cols-[minmax(0,1fr)_22rem]">
|
|
148
|
+
<section className="min-w-0 rounded-2xl border bg-background/80 p-3 sm:p-5">
|
|
149
|
+
<div className="mb-4 flex flex-wrap items-center justify-between gap-3 border-b pb-4">
|
|
139
150
|
<div>
|
|
140
|
-
<
|
|
141
|
-
|
|
151
|
+
<h2 className="font-semibold">
|
|
152
|
+
{t('availability_canvas')}
|
|
153
|
+
</h2>
|
|
154
|
+
<p className="text-muted-foreground text-xs">
|
|
155
|
+
{t('availability_canvas_description')}
|
|
156
|
+
</p>
|
|
142
157
|
</div>
|
|
143
|
-
<
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
<div className="flex items-center gap-2">
|
|
159
|
+
<Label htmlFor="show-best-times" className="text-xs">
|
|
160
|
+
{t('peak_only')}
|
|
161
|
+
</Label>
|
|
162
|
+
<Tooltip>
|
|
163
|
+
<TooltipTrigger asChild>
|
|
164
|
+
<CircleQuestionMark className="h-3.5 w-3.5 text-muted-foreground" />
|
|
165
|
+
</TooltipTrigger>
|
|
166
|
+
<TooltipContent>
|
|
167
|
+
{t('peak_only_description')}
|
|
168
|
+
</TooltipContent>
|
|
169
|
+
</Tooltip>
|
|
170
|
+
<Switch
|
|
171
|
+
id="show-best-times"
|
|
172
|
+
checked={showBestTimes}
|
|
173
|
+
onCheckedChange={setShowBestTimes}
|
|
174
|
+
disabled={isUserFilterActive}
|
|
175
|
+
/>
|
|
157
176
|
</div>
|
|
158
177
|
</div>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
{noBestTimesFound && (
|
|
169
|
-
<div className="mt-2 w-full max-w-xl rounded bg-yellow-100 p-2 text-center text-xs text-yellow-900 dark:bg-yellow-900 dark:text-yellow-100">
|
|
170
|
-
<strong>No best times were found!</strong>
|
|
171
|
-
<br />
|
|
172
|
-
Encourage your group to sync up or adjust their availability.
|
|
178
|
+
<UnifiedAvailability
|
|
179
|
+
plan={plan}
|
|
180
|
+
timeblocks={timeblocks}
|
|
181
|
+
showBestTimes={showBestTimes}
|
|
182
|
+
/>
|
|
183
|
+
</section>
|
|
184
|
+
<aside className="rounded-2xl border bg-background/80 p-2 xl:sticky xl:top-4">
|
|
185
|
+
<SidebarDisplay plan={plan} polls={polls} users={users} />
|
|
186
|
+
</aside>
|
|
173
187
|
</div>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
'mt-8 grid w-full grid-cols-1 items-start justify-between gap-4 md:grid-cols-3'
|
|
179
|
-
)}
|
|
180
|
-
>
|
|
181
|
-
<div className={cn('md:col-span-2')}>
|
|
182
|
-
<UnifiedAvailability
|
|
188
|
+
</TabsContent>
|
|
189
|
+
|
|
190
|
+
<TabsContent value="suggestions" className="mt-0">
|
|
191
|
+
<MeetSuggestionsPanel
|
|
183
192
|
plan={plan}
|
|
193
|
+
users={users}
|
|
184
194
|
timeblocks={timeblocks}
|
|
185
|
-
|
|
186
|
-
|
|
195
|
+
finalizedTimeframes={finalizedTimeframes}
|
|
196
|
+
isCreator={isCreator}
|
|
187
197
|
/>
|
|
188
|
-
</
|
|
189
|
-
<SidebarDisplay plan={plan} polls={polls} users={users} />
|
|
190
|
-
</div>
|
|
198
|
+
</TabsContent>
|
|
191
199
|
|
|
192
|
-
|
|
200
|
+
<TabsContent value="insights" className="mt-0">
|
|
201
|
+
<MeetInsightsPanel
|
|
202
|
+
plan={plan}
|
|
203
|
+
users={users}
|
|
204
|
+
timeblocks={timeblocks}
|
|
205
|
+
/>
|
|
206
|
+
</TabsContent>
|
|
207
|
+
</Tabs>
|
|
208
|
+
</div>
|
|
193
209
|
|
|
210
|
+
<section className="rounded-2xl border bg-background/80 p-4 sm:p-5">
|
|
194
211
|
<AgendaDetails plan={plan} />
|
|
195
|
-
</
|
|
212
|
+
</section>
|
|
196
213
|
</div>
|
|
197
|
-
</
|
|
198
|
-
|
|
199
|
-
{/* Discord-style sticky bottom indicator for unsaved changes */}
|
|
214
|
+
</main>
|
|
200
215
|
{isDirty && <StickyBottomIndicator />}
|
|
201
|
-
|
|
202
216
|
<PlanLogin plan={plan} baseUrl={baseUrl} />
|
|
203
217
|
</>
|
|
204
218
|
);
|
|
@@ -2,6 +2,7 @@ import { useTimeBlocking } from '@tuturuuu/ui/hooks/time-blocking-provider';
|
|
|
2
2
|
import { timetzToTime } from '@tuturuuu/utils/date-helper';
|
|
3
3
|
import { cn } from '@tuturuuu/utils/format';
|
|
4
4
|
import dayjs from 'dayjs';
|
|
5
|
+
import type { PointerEvent as ReactPointerEvent } from 'react';
|
|
5
6
|
|
|
6
7
|
export default function SelectableDayTime({
|
|
7
8
|
date,
|
|
@@ -19,6 +20,36 @@ export default function SelectableDayTime({
|
|
|
19
20
|
const { editing, selectedTimeBlocks, edit, setPreviewDate, endEditing } =
|
|
20
21
|
useTimeBlocking();
|
|
21
22
|
|
|
23
|
+
const editCellAtPointer = (event: ReactPointerEvent<HTMLElement>) => {
|
|
24
|
+
const edge = 72;
|
|
25
|
+
const scrollStep = 18;
|
|
26
|
+
if (event.clientY < edge) window.scrollBy({ top: -scrollStep });
|
|
27
|
+
else if (event.clientY > window.innerHeight - edge)
|
|
28
|
+
window.scrollBy({ top: scrollStep });
|
|
29
|
+
|
|
30
|
+
const horizontalScroller = event.currentTarget.closest<HTMLElement>(
|
|
31
|
+
'[data-meet-grid-scroller]'
|
|
32
|
+
);
|
|
33
|
+
if (horizontalScroller) {
|
|
34
|
+
const bounds = horizontalScroller.getBoundingClientRect();
|
|
35
|
+
if (event.clientX < bounds.left + edge)
|
|
36
|
+
horizontalScroller.scrollBy({ left: -scrollStep });
|
|
37
|
+
else if (event.clientX > bounds.right - edge)
|
|
38
|
+
horizontalScroller.scrollBy({ left: scrollStep });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const target = document
|
|
42
|
+
.elementFromPoint(event.clientX, event.clientY)
|
|
43
|
+
?.closest<HTMLElement>('[data-meet-slot]');
|
|
44
|
+
const isoDate = target?.dataset.meetSlot;
|
|
45
|
+
if (!isoDate) return;
|
|
46
|
+
edit({
|
|
47
|
+
mode: (target.dataset.meetMode as 'add' | 'remove') || 'add',
|
|
48
|
+
date: new Date(isoDate),
|
|
49
|
+
tentativeMode: target.dataset.meetTentative === 'true',
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
22
53
|
const hourBlocks = Array.from(Array(Math.floor(end + 1 - start)).keys());
|
|
23
54
|
const hourSplits = 4;
|
|
24
55
|
|
|
@@ -153,67 +184,68 @@ export default function SelectableDayTime({
|
|
|
153
184
|
} as const;
|
|
154
185
|
|
|
155
186
|
return (
|
|
156
|
-
|
|
157
|
-
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
158
189
|
key={`${date}-${i}`}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
: undefined
|
|
190
|
+
data-meet-slot={currentDate.toISOString()}
|
|
191
|
+
data-meet-mode={editData.mode}
|
|
192
|
+
data-meet-tentative={String(editData.tentativeMode ?? false)}
|
|
193
|
+
disabled={!isSelectable}
|
|
194
|
+
aria-label={
|
|
195
|
+
isSelectable
|
|
196
|
+
? `${date} ${dayjs(currentDate).format('HH:mm')}`
|
|
197
|
+
: undefined
|
|
168
198
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
: (e) => {
|
|
179
|
-
e.preventDefault();
|
|
180
|
-
setPreviewDate(editData.date);
|
|
181
|
-
}
|
|
199
|
+
aria-pressed={isSelectable ? isSelected : undefined}
|
|
200
|
+
onPointerDown={
|
|
201
|
+
isSelectable
|
|
202
|
+
? (event) => {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
205
|
+
edit(editData);
|
|
206
|
+
}
|
|
207
|
+
: undefined
|
|
182
208
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
?
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
: (e) => {
|
|
193
|
-
e.preventDefault();
|
|
194
|
-
setPreviewDate(editData.date);
|
|
195
|
-
}
|
|
209
|
+
onPointerMove={
|
|
210
|
+
isSelectable
|
|
211
|
+
? (event) => {
|
|
212
|
+
if (!editing.enabled) return;
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
editCellAtPointer(event);
|
|
215
|
+
}
|
|
216
|
+
: undefined
|
|
196
217
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
?
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
onPointerUp={
|
|
219
|
+
isSelectable
|
|
220
|
+
? (event) => {
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
if (
|
|
223
|
+
event.currentTarget.hasPointerCapture(event.pointerId)
|
|
224
|
+
)
|
|
225
|
+
event.currentTarget.releasePointerCapture(
|
|
226
|
+
event.pointerId
|
|
227
|
+
);
|
|
228
|
+
endEditing();
|
|
229
|
+
}
|
|
230
|
+
: undefined
|
|
231
|
+
}
|
|
232
|
+
onPointerCancel={isSelectable ? endEditing : undefined}
|
|
233
|
+
onPointerEnter={
|
|
234
|
+
isSelectable
|
|
235
|
+
? () => {
|
|
236
|
+
if (editing.enabled) edit(editData);
|
|
237
|
+
}
|
|
238
|
+
: () => setPreviewDate(editData.date)
|
|
207
239
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
?
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
240
|
+
onKeyDown={
|
|
241
|
+
isSelectable
|
|
242
|
+
? (event) => {
|
|
243
|
+
if (event.key !== ' ' && event.key !== 'Enter') return;
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
edit(editData);
|
|
246
|
+
queueMicrotask(endEditing);
|
|
216
247
|
}
|
|
248
|
+
: undefined
|
|
217
249
|
}
|
|
218
250
|
className={`${
|
|
219
251
|
i + hourSplits < array.length
|
|
@@ -229,7 +261,7 @@ export default function SelectableDayTime({
|
|
|
229
261
|
? 'bg-dynamic-red/50'
|
|
230
262
|
: 'bg-dynamic-red/20'
|
|
231
263
|
: ''
|
|
232
|
-
} relative h-3 w-full ${cn(
|
|
264
|
+
} relative h-3 w-full touch-none focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-primary ${cn(
|
|
233
265
|
hideBorder
|
|
234
266
|
? ''
|
|
235
267
|
: (i + 1) % hourSplits === 0
|
|
@@ -13,12 +13,10 @@ import {
|
|
|
13
13
|
} from '@tuturuuu/ui/dialog';
|
|
14
14
|
import { useTranslations } from 'next-intl';
|
|
15
15
|
import { QRCodeCanvas } from 'qrcode.react';
|
|
16
|
-
import CopyLinkButton
|
|
16
|
+
import CopyLinkButton from './copy-link-button';
|
|
17
17
|
|
|
18
18
|
export default function ShowQRButton({ url }: { url: string }) {
|
|
19
19
|
const t = useTranslations();
|
|
20
|
-
const tumeetMeUrl = generateTumeetMeUrl(url);
|
|
21
|
-
|
|
22
20
|
return (
|
|
23
21
|
<Dialog>
|
|
24
22
|
<DialogTrigger asChild>
|
|
@@ -37,7 +35,7 @@ export default function ShowQRButton({ url }: { url: string }) {
|
|
|
37
35
|
|
|
38
36
|
<div className="flex items-center justify-center">
|
|
39
37
|
<QRCodeCanvas
|
|
40
|
-
value={
|
|
38
|
+
value={url}
|
|
41
39
|
size={256}
|
|
42
40
|
marginSize={2}
|
|
43
41
|
className="rounded-lg"
|
|
@@ -45,7 +43,7 @@ export default function ShowQRButton({ url }: { url: string }) {
|
|
|
45
43
|
</div>
|
|
46
44
|
|
|
47
45
|
<DialogFooter>
|
|
48
|
-
<CopyLinkButton url={
|
|
46
|
+
<CopyLinkButton url={url} className="md:w-full" />
|
|
49
47
|
</DialogFooter>
|
|
50
48
|
</DialogContent>
|
|
51
49
|
</Dialog>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Calendar, Save, Users } from '@tuturuuu/icons';
|
|
3
|
+
import { Calendar, Redo2, Save, Undo2, Users } from '@tuturuuu/icons';
|
|
4
4
|
import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
|
|
5
5
|
import type { Timeblock } from '@tuturuuu/types/primitives/Timeblock';
|
|
6
6
|
import { Button } from '@tuturuuu/ui/button';
|
|
7
7
|
import { useTimeBlocking } from '@tuturuuu/ui/hooks/time-blocking-provider';
|
|
8
|
+
import { useTranslations } from 'next-intl';
|
|
8
9
|
import { useEffect, useState } from 'react';
|
|
9
10
|
import AllAvailabilities from './all-availabilities';
|
|
10
11
|
import AvailabilityPlanner from './availability-planner';
|
|
@@ -22,7 +23,9 @@ export default function UnifiedAvailability({
|
|
|
22
23
|
showBestTimes = false,
|
|
23
24
|
onBestTimesStatusByDateAction,
|
|
24
25
|
}: UnifiedAvailabilityProps) {
|
|
25
|
-
const
|
|
26
|
+
const t = useTranslations('meet-together-plan-details');
|
|
27
|
+
const { isDirty, handleSave, isSaving, canUndo, canRedo, undo, redo } =
|
|
28
|
+
useTimeBlocking();
|
|
26
29
|
const [isEditing, setIsEditing] = useState(false);
|
|
27
30
|
const { user, setDisplayMode } = useTimeBlocking();
|
|
28
31
|
|
|
@@ -56,25 +59,45 @@ export default function UnifiedAvailability({
|
|
|
56
59
|
{isEditing ? (
|
|
57
60
|
<>
|
|
58
61
|
<Users size={16} />
|
|
59
|
-
|
|
62
|
+
{t('view_everyone')}
|
|
60
63
|
</>
|
|
61
64
|
) : (
|
|
62
65
|
<>
|
|
63
66
|
<Calendar size={16} />
|
|
64
|
-
|
|
67
|
+
{t('add_availability')}
|
|
65
68
|
</>
|
|
66
69
|
)}
|
|
67
70
|
</Button>
|
|
68
71
|
{isEditing && (
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
<div className="flex items-center gap-1">
|
|
73
|
+
<Button
|
|
74
|
+
variant="outline"
|
|
75
|
+
size="icon"
|
|
76
|
+
onClick={undo}
|
|
77
|
+
disabled={!canUndo}
|
|
78
|
+
aria-label={t('undo')}
|
|
79
|
+
>
|
|
80
|
+
<Undo2 size={16} />
|
|
81
|
+
</Button>
|
|
82
|
+
<Button
|
|
83
|
+
variant="outline"
|
|
84
|
+
size="icon"
|
|
85
|
+
onClick={redo}
|
|
86
|
+
disabled={!canRedo}
|
|
87
|
+
aria-label={t('redo')}
|
|
88
|
+
>
|
|
89
|
+
<Redo2 size={16} />
|
|
90
|
+
</Button>
|
|
91
|
+
<Button
|
|
92
|
+
variant="default"
|
|
93
|
+
size="lg"
|
|
94
|
+
onClick={handleSave}
|
|
95
|
+
disabled={!isDirty || isSaving}
|
|
96
|
+
>
|
|
97
|
+
<Save size={16} />
|
|
98
|
+
{isSaving ? t('saving') : t('save_changes')}
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
78
101
|
)}
|
|
79
102
|
</div>
|
|
80
103
|
|