@tuturuuu/ui 0.22.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 +36 -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,67 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
Check,
|
|
5
|
-
ChevronDown,
|
|
6
|
-
Copy,
|
|
7
|
-
ExternalLink,
|
|
8
|
-
LinkIcon,
|
|
9
|
-
} from '@tuturuuu/icons';
|
|
3
|
+
import { Check, LinkIcon } from '@tuturuuu/icons';
|
|
10
4
|
import { Button } from '@tuturuuu/ui/button';
|
|
11
|
-
import {
|
|
12
|
-
DropdownMenu,
|
|
13
|
-
DropdownMenuContent,
|
|
14
|
-
DropdownMenuItem,
|
|
15
|
-
DropdownMenuTrigger,
|
|
16
|
-
} from '@tuturuuu/ui/dropdown-menu';
|
|
17
5
|
import { cn } from '@tuturuuu/utils/format';
|
|
18
6
|
import { useTranslations } from 'next-intl';
|
|
19
|
-
import {
|
|
7
|
+
import { useState } from 'react';
|
|
20
8
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
domain: string;
|
|
24
|
-
path: string;
|
|
9
|
+
export function generateCanonicalMeetUrl(planId: string) {
|
|
10
|
+
return `https://tuturuuu.com/meet/plans/${planId}`;
|
|
25
11
|
}
|
|
26
12
|
|
|
27
|
-
const LINK_FORMATS: LinkFormat[] = [
|
|
28
|
-
{ name: 'tumeet.me', domain: 'https://tumeet.me', path: '/' },
|
|
29
|
-
{ name: 'tumeet.us', domain: 'https://tumeet.us', path: '/' },
|
|
30
|
-
{
|
|
31
|
-
name: 'tuturuuu.com',
|
|
32
|
-
domain: 'https://tuturuuu.com',
|
|
33
|
-
path: '/meet-together/plans/',
|
|
34
|
-
},
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
// Extract the plan ID from the URL
|
|
38
|
-
const getPlanId = (url: string): string => {
|
|
39
|
-
try {
|
|
40
|
-
const urlObj = new URL(url);
|
|
41
|
-
const pathParts = urlObj.pathname.split('/');
|
|
42
|
-
// Handle /meet-together/plans/{planId} and /{planId} formats
|
|
43
|
-
let planIdIndex: number;
|
|
44
|
-
if (pathParts.includes('meet-together') && pathParts.includes('plans')) {
|
|
45
|
-
planIdIndex = pathParts.indexOf('plans') + 1;
|
|
46
|
-
} else if (pathParts.includes('r')) {
|
|
47
|
-
// Handle legacy /r/{planId} format
|
|
48
|
-
planIdIndex = pathParts.indexOf('r') + 1;
|
|
49
|
-
} else {
|
|
50
|
-
// Handle /{planId} format (tumeet.me, tumeet.us)
|
|
51
|
-
planIdIndex = pathParts.length - 1;
|
|
52
|
-
}
|
|
53
|
-
return pathParts[planIdIndex] || '';
|
|
54
|
-
} catch {
|
|
55
|
-
return '';
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Generate tumeet.me URL (default format)
|
|
60
|
-
export const generateTumeetMeUrl = (url: string): string => {
|
|
61
|
-
const planId = getPlanId(url);
|
|
62
|
-
return planId ? `https://tumeet.me/${planId}` : url;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
13
|
export default function CopyLinkButton({
|
|
66
14
|
url,
|
|
67
15
|
className,
|
|
@@ -71,148 +19,27 @@ export default function CopyLinkButton({
|
|
|
71
19
|
}) {
|
|
72
20
|
const t = useTranslations('meet-together-plan-details');
|
|
73
21
|
const [copied, setCopied] = useState(false);
|
|
74
|
-
const [selectedFormat] = useState<LinkFormat>(
|
|
75
|
-
LINK_FORMATS?.[0] ?? {
|
|
76
|
-
name: 'tumeet.me',
|
|
77
|
-
domain: 'https://tumeet.me',
|
|
78
|
-
path: '/r/',
|
|
79
|
-
}
|
|
80
|
-
); // Default to tumeet.me
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (copied) {
|
|
84
|
-
setTimeout(() => {
|
|
85
|
-
setCopied(false);
|
|
86
|
-
}, 3000);
|
|
87
|
-
}
|
|
88
|
-
}, [copied]);
|
|
89
|
-
|
|
90
|
-
const planId = getPlanId(url);
|
|
91
|
-
const formattedUrl = planId
|
|
92
|
-
? `${selectedFormat.domain}${selectedFormat.path}${planId}`
|
|
93
|
-
: url;
|
|
94
|
-
|
|
95
|
-
const handleCopy = async (linkFormat?: LinkFormat) => {
|
|
96
|
-
const urlToCopy = linkFormat
|
|
97
|
-
? `${linkFormat.domain}${linkFormat.path}${planId}`
|
|
98
|
-
: formattedUrl;
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
await navigator.clipboard.writeText(urlToCopy);
|
|
102
|
-
setCopied(true);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
console.error('Failed to copy to clipboard:', error);
|
|
105
|
-
// Fallback for older browsers
|
|
106
|
-
const textArea = document.createElement('textarea');
|
|
107
|
-
textArea.value = urlToCopy;
|
|
108
|
-
document.body.appendChild(textArea);
|
|
109
|
-
textArea.select();
|
|
110
|
-
document.execCommand('copy');
|
|
111
|
-
document.body.removeChild(textArea);
|
|
112
|
-
setCopied(true);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
22
|
|
|
116
23
|
return (
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
</Button>
|
|
138
|
-
</DropdownMenuTrigger>
|
|
139
|
-
<DropdownMenuContent
|
|
140
|
-
align="end"
|
|
141
|
-
className="w-80 border-dynamic-purple/20 p-4 shadow-xl backdrop-blur-sm"
|
|
142
|
-
>
|
|
143
|
-
<div className="mb-4 border-dynamic-purple/20 border-b px-1 py-2 pb-3 font-semibold text-dynamic-purple text-sm">
|
|
144
|
-
<div className="flex items-center gap-2">
|
|
145
|
-
<LinkIcon className="h-4 w-4" />
|
|
146
|
-
Choose link format
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
{LINK_FORMATS.map((format, index) => {
|
|
150
|
-
const isDefault = format.name === 'tumeet.me';
|
|
151
|
-
return (
|
|
152
|
-
<div key={format.name}>
|
|
153
|
-
{index > 0 && (
|
|
154
|
-
<div className="my-3 border-dynamic-purple/10 border-t" />
|
|
155
|
-
)}
|
|
156
|
-
<DropdownMenuItem
|
|
157
|
-
onClick={() => handleCopy(format)}
|
|
158
|
-
className={cn(
|
|
159
|
-
'group flex cursor-pointer flex-col items-start gap-3 rounded-lg p-4 transition-all duration-200 hover:bg-dynamic-purple/5 hover:shadow-md',
|
|
160
|
-
isDefault &&
|
|
161
|
-
'border border-dynamic-purple/30 bg-dynamic-purple/5 shadow-sm'
|
|
162
|
-
)}
|
|
163
|
-
>
|
|
164
|
-
<div className="flex w-full items-center justify-between">
|
|
165
|
-
<div className="flex items-center gap-2">
|
|
166
|
-
<div
|
|
167
|
-
className={cn(
|
|
168
|
-
'flex h-8 w-8 items-center justify-center rounded-full font-bold text-sm',
|
|
169
|
-
isDefault
|
|
170
|
-
? 'bg-dynamic-purple text-white'
|
|
171
|
-
: 'bg-dynamic-purple/10 text-dynamic-purple'
|
|
172
|
-
)}
|
|
173
|
-
>
|
|
174
|
-
{format.name.charAt(0).toUpperCase()}
|
|
175
|
-
</div>
|
|
176
|
-
<div className="flex items-center gap-2">
|
|
177
|
-
<span className="font-semibold text-foreground">
|
|
178
|
-
{format.name}
|
|
179
|
-
</span>
|
|
180
|
-
{isDefault && (
|
|
181
|
-
<span className="rounded-full border border-dynamic-purple/30 bg-dynamic-purple/20 px-2 py-0.5 font-medium text-dynamic-purple text-xs">
|
|
182
|
-
Default
|
|
183
|
-
</span>
|
|
184
|
-
)}
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
<Copy className="h-4 w-4 text-muted-foreground opacity-0 transition-opacity duration-200 group-hover:opacity-100" />
|
|
188
|
-
</div>
|
|
189
|
-
<div className="w-full">
|
|
190
|
-
<div className="mb-2 font-medium text-muted-foreground text-xs">
|
|
191
|
-
Full URL:
|
|
192
|
-
</div>
|
|
193
|
-
<div className="flex items-center gap-2 rounded-md border border-dynamic-purple/20 bg-muted/30 p-3 transition-colors duration-200 group-hover:border-dynamic-purple/40 group-hover:bg-muted/50">
|
|
194
|
-
<span className="break-all font-mono text-foreground text-xs">
|
|
195
|
-
{format.domain}
|
|
196
|
-
{format.path}
|
|
197
|
-
{planId}
|
|
198
|
-
</span>
|
|
199
|
-
<ExternalLink className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
<div className="w-full font-medium text-dynamic-purple text-xs">
|
|
203
|
-
Click to copy to clipboard
|
|
204
|
-
</div>
|
|
205
|
-
</DropdownMenuItem>
|
|
206
|
-
</div>
|
|
207
|
-
);
|
|
208
|
-
})}
|
|
209
|
-
<div className="mt-4 border-dynamic-purple/20 border-t pt-3">
|
|
210
|
-
<div className="flex items-center gap-2 px-1 text-muted-foreground text-xs">
|
|
211
|
-
<ExternalLink className="h-3 w-3" />
|
|
212
|
-
Links will open in a new tab when shared
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
</DropdownMenuContent>
|
|
216
|
-
</DropdownMenu>
|
|
24
|
+
<Button
|
|
25
|
+
className={cn(
|
|
26
|
+
'w-full transition active:scale-[0.98] sm:w-auto',
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
variant="outline"
|
|
30
|
+
disabled={!url}
|
|
31
|
+
onClick={async () => {
|
|
32
|
+
await navigator.clipboard.writeText(url);
|
|
33
|
+
setCopied(true);
|
|
34
|
+
window.setTimeout(() => setCopied(false), 2000);
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{copied ? (
|
|
38
|
+
<Check className="mr-2 h-4 w-4" />
|
|
39
|
+
) : (
|
|
40
|
+
<LinkIcon className="mr-2 h-4 w-4" />
|
|
41
|
+
)}
|
|
42
|
+
{copied ? t('copied') : t('copy_link')}
|
|
43
|
+
</Button>
|
|
217
44
|
);
|
|
218
45
|
}
|
|
@@ -32,7 +32,7 @@ function DatePlanner({
|
|
|
32
32
|
onBestTimesStatusByDateAction?: (status: Record<string, boolean>) => void;
|
|
33
33
|
}) {
|
|
34
34
|
const t = useTranslations('meet-together-plan-details');
|
|
35
|
-
const { user,
|
|
35
|
+
const { user, setPreviewDate } = useTimeBlocking();
|
|
36
36
|
const [tentativeMode, setTentativeMode] = useState(false);
|
|
37
37
|
const [currentPage, setCurrentPage] = useState(0);
|
|
38
38
|
const isMobile = useIsMobile();
|
|
@@ -125,35 +125,10 @@ function DatePlanner({
|
|
|
125
125
|
</div>
|
|
126
126
|
)}
|
|
127
127
|
|
|
128
|
-
<div
|
|
129
|
-
onMouseUp={
|
|
130
|
-
editable
|
|
131
|
-
? (e) => {
|
|
132
|
-
e.preventDefault();
|
|
133
|
-
endEditing();
|
|
134
|
-
}
|
|
135
|
-
: undefined
|
|
136
|
-
}
|
|
137
|
-
onMouseLeave={
|
|
138
|
-
editable
|
|
139
|
-
? (e) => {
|
|
140
|
-
e.preventDefault();
|
|
141
|
-
endEditing();
|
|
142
|
-
}
|
|
143
|
-
: undefined
|
|
144
|
-
}
|
|
145
|
-
onTouchEnd={
|
|
146
|
-
editable
|
|
147
|
-
? () => {
|
|
148
|
-
if (!editing.enabled) return;
|
|
149
|
-
endEditing();
|
|
150
|
-
}
|
|
151
|
-
: undefined
|
|
152
|
-
}
|
|
153
|
-
className="mt-4 flex items-start justify-center gap-2"
|
|
154
|
-
>
|
|
128
|
+
<div className="mt-4 flex items-start justify-center gap-2">
|
|
155
129
|
{dates && (
|
|
156
130
|
<div
|
|
131
|
+
data-meet-grid-scroller
|
|
157
132
|
className="flex flex-col items-start justify-start gap-4 overflow-x-auto"
|
|
158
133
|
onMouseLeave={
|
|
159
134
|
editable
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { BarChart3, CalendarDays, Percent, Users } from '@tuturuuu/icons';
|
|
4
|
+
import type {
|
|
5
|
+
MeetTogetherPlan,
|
|
6
|
+
PlanUser,
|
|
7
|
+
} from '@tuturuuu/types/primitives/MeetTogetherPlan';
|
|
8
|
+
import type { Timeblock } from '@tuturuuu/types/primitives/Timeblock';
|
|
9
|
+
import {
|
|
10
|
+
Accordion,
|
|
11
|
+
AccordionContent,
|
|
12
|
+
AccordionItem,
|
|
13
|
+
AccordionTrigger,
|
|
14
|
+
} from '@tuturuuu/ui/accordion';
|
|
15
|
+
import { Progress } from '@tuturuuu/ui/progress';
|
|
16
|
+
import { useTranslations } from 'next-intl';
|
|
17
|
+
import { calculateMeetPlanInsights } from '../lib/meet-insights';
|
|
18
|
+
|
|
19
|
+
export function MeetInsightsPanel({
|
|
20
|
+
plan,
|
|
21
|
+
users,
|
|
22
|
+
timeblocks,
|
|
23
|
+
}: {
|
|
24
|
+
plan: MeetTogetherPlan;
|
|
25
|
+
users: PlanUser[];
|
|
26
|
+
timeblocks: Timeblock[];
|
|
27
|
+
}) {
|
|
28
|
+
const t = useTranslations('meet-together-plan-details');
|
|
29
|
+
const insights = calculateMeetPlanInsights({ plan, users, timeblocks });
|
|
30
|
+
const metrics = [
|
|
31
|
+
{
|
|
32
|
+
icon: Users,
|
|
33
|
+
label: t('responses'),
|
|
34
|
+
value: `${insights.respondedCount}/${insights.participantCount}`,
|
|
35
|
+
detail: t('percent_responded', {
|
|
36
|
+
count: Math.round(insights.responsePercent),
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
icon: Percent,
|
|
41
|
+
label: t('peak_attendance'),
|
|
42
|
+
value: `${Math.round(insights.peakAttendancePercent)}%`,
|
|
43
|
+
detail: t('count_available', { count: insights.peakAttendance }),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
icon: BarChart3,
|
|
47
|
+
label: t('average_match'),
|
|
48
|
+
value: `${Math.round(insights.averageAvailabilityPercent)}%`,
|
|
49
|
+
detail: t('across_eligible_windows'),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
icon: CalendarDays,
|
|
53
|
+
label: t('candidate_dates'),
|
|
54
|
+
value: String(plan.dates?.length ?? 0),
|
|
55
|
+
detail: t('minute_meeting', { count: plan.duration_minutes ?? 60 }),
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<section className="space-y-5" aria-label={t('meeting_insights')}>
|
|
61
|
+
<div className="grid gap-px overflow-hidden rounded-2xl border bg-border sm:grid-cols-2 xl:grid-cols-4">
|
|
62
|
+
{metrics.map(({ icon: Icon, label, value, detail }) => (
|
|
63
|
+
<article key={label} className="bg-background p-5">
|
|
64
|
+
<div className="mb-5 flex items-center justify-between text-muted-foreground">
|
|
65
|
+
<span className="font-medium text-xs">{label}</span>
|
|
66
|
+
<Icon className="h-4 w-4" />
|
|
67
|
+
</div>
|
|
68
|
+
<p className="font-semibold text-2xl tabular-nums tracking-tight">
|
|
69
|
+
{value}
|
|
70
|
+
</p>
|
|
71
|
+
<p className="mt-1 text-muted-foreground text-xs">{detail}</p>
|
|
72
|
+
</article>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div className="rounded-2xl border bg-background/70 p-5">
|
|
77
|
+
<div className="mb-5">
|
|
78
|
+
<h3 className="font-semibold">{t('best_overlap_by_day')}</h3>
|
|
79
|
+
<p className="text-muted-foreground text-sm">
|
|
80
|
+
{t('best_overlap_by_day_description')}
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="space-y-4">
|
|
84
|
+
{insights.overlapByDate.map(({ date, peak }) => {
|
|
85
|
+
const percent = insights.participantCount
|
|
86
|
+
? (peak / insights.participantCount) * 100
|
|
87
|
+
: 0;
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
key={date}
|
|
91
|
+
className="grid grid-cols-[7rem_1fr_3rem] items-center gap-3"
|
|
92
|
+
>
|
|
93
|
+
<span className="text-sm tabular-nums">{date}</span>
|
|
94
|
+
<Progress value={percent} className="h-2" />
|
|
95
|
+
<span className="text-right text-muted-foreground text-xs tabular-nums">
|
|
96
|
+
{peak}/{insights.participantCount}
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<Accordion
|
|
104
|
+
type="single"
|
|
105
|
+
collapsible
|
|
106
|
+
className="rounded-2xl border bg-background/70 px-5"
|
|
107
|
+
>
|
|
108
|
+
<AccordionItem value="hourly" className="border-0">
|
|
109
|
+
<AccordionTrigger>{t('hourly_availability')}</AccordionTrigger>
|
|
110
|
+
<AccordionContent>
|
|
111
|
+
<p className="mb-4 text-muted-foreground text-sm">
|
|
112
|
+
{t('hourly_availability_description')}
|
|
113
|
+
</p>
|
|
114
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
115
|
+
{insights.availabilityByHour.map(({ hour, percent }) => (
|
|
116
|
+
<div
|
|
117
|
+
key={hour}
|
|
118
|
+
className="grid grid-cols-[4rem_1fr_3rem] items-center gap-3"
|
|
119
|
+
>
|
|
120
|
+
<span className="text-sm tabular-nums">
|
|
121
|
+
{String(hour).padStart(2, '0')}:00
|
|
122
|
+
</span>
|
|
123
|
+
<Progress value={percent} className="h-2" />
|
|
124
|
+
<span className="text-right text-muted-foreground text-xs tabular-nums">
|
|
125
|
+
{Math.round(percent)}%
|
|
126
|
+
</span>
|
|
127
|
+
</div>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
</AccordionContent>
|
|
131
|
+
</AccordionItem>
|
|
132
|
+
</Accordion>
|
|
133
|
+
</section>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { MeetPlanSnapshot } from '@tuturuuu/internal-api';
|
|
4
|
+
import type { User as PlatformUser } from '@tuturuuu/types/primitives/User';
|
|
5
|
+
import { TimeBlockingProvider } from '@tuturuuu/ui/hooks/time-blocking-provider';
|
|
6
|
+
import { useMeetPlanQuery } from './meet-query';
|
|
7
|
+
import PlanDetailsClient from './plan-details-client';
|
|
8
|
+
|
|
9
|
+
export function MeetPlanLiveRoot({
|
|
10
|
+
initialSnapshot,
|
|
11
|
+
platformUser,
|
|
12
|
+
baseUrl,
|
|
13
|
+
}: {
|
|
14
|
+
initialSnapshot: MeetPlanSnapshot;
|
|
15
|
+
platformUser: PlatformUser | null;
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
}) {
|
|
18
|
+
const query = useMeetPlanQuery(initialSnapshot);
|
|
19
|
+
const snapshot = query.data;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<TimeBlockingProvider
|
|
23
|
+
key={`${platformUser?.id || 'guest'}-${snapshot.plan.id}`}
|
|
24
|
+
platformUser={platformUser}
|
|
25
|
+
plan={snapshot.plan}
|
|
26
|
+
users={snapshot.users}
|
|
27
|
+
timeblocks={snapshot.timeblocks}
|
|
28
|
+
>
|
|
29
|
+
<PlanDetailsClient
|
|
30
|
+
plan={snapshot.plan}
|
|
31
|
+
polls={snapshot.polls as never}
|
|
32
|
+
users={snapshot.users}
|
|
33
|
+
timeblocks={snapshot.timeblocks}
|
|
34
|
+
finalizedTimeframes={snapshot.finalizedTimeframes}
|
|
35
|
+
isCreator={snapshot.viewer.isCreator}
|
|
36
|
+
isRefreshing={query.isFetching}
|
|
37
|
+
baseUrl={baseUrl}
|
|
38
|
+
/>
|
|
39
|
+
</TimeBlockingProvider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
4
|
+
import {
|
|
5
|
+
getMeetPlanSnapshot,
|
|
6
|
+
type MeetPlanSnapshot,
|
|
7
|
+
} from '@tuturuuu/internal-api';
|
|
8
|
+
|
|
9
|
+
export const meetPlanQueryKey = (planId: string) =>
|
|
10
|
+
['meet-plan', planId] as const;
|
|
11
|
+
|
|
12
|
+
export function useMeetPlanQuery(initialSnapshot: MeetPlanSnapshot) {
|
|
13
|
+
const planId = initialSnapshot.plan.id;
|
|
14
|
+
if (!planId) throw new Error('A Tuturuuu Meet plan id is required');
|
|
15
|
+
|
|
16
|
+
return useQuery({
|
|
17
|
+
queryKey: meetPlanQueryKey(planId),
|
|
18
|
+
queryFn: () => getMeetPlanSnapshot(planId),
|
|
19
|
+
initialData: initialSnapshot,
|
|
20
|
+
refetchInterval: (query) =>
|
|
21
|
+
query.state.data?.plan.is_confirmed ? 30_000 : 5_000,
|
|
22
|
+
refetchIntervalInBackground: false,
|
|
23
|
+
refetchOnReconnect: true,
|
|
24
|
+
refetchOnWindowFocus: true,
|
|
25
|
+
});
|
|
26
|
+
}
|