@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/biome.json +1 -1
  3. package/package.json +78 -78
  4. package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.test.tsx +121 -0
  5. package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.tsx +37 -4
  6. package/src/components/ui/chat/ai-message-markdown.tsx +6 -1
  7. package/src/components/ui/chat/chat-sidebar-groups.test.ts +33 -0
  8. package/src/components/ui/chat/chat-sidebar-items.tsx +10 -2
  9. package/src/components/ui/chat/chat-sidebar-panel.test.tsx +29 -1
  10. package/src/components/ui/chat/chat-sidebar-panel.tsx +4 -3
  11. package/src/components/ui/chat/chat-sidebar-sections.ts +24 -10
  12. package/src/components/ui/chat/chat-utils.test.ts +107 -0
  13. package/src/components/ui/chat/chat-workspace-header.tsx +11 -5
  14. package/src/components/ui/chat/chat-workspace.tsx +66 -24
  15. package/src/components/ui/chat/hooks-conversations.ts +9 -1
  16. package/src/components/ui/chat/hooks-messages.ts +1 -0
  17. package/src/components/ui/chat/message-bubble.tsx +49 -4
  18. package/src/components/ui/chat/message-composer.tsx +29 -23
  19. package/src/components/ui/chat/message-markdown.tsx +4 -1
  20. package/src/components/ui/chat/query-keys.ts +7 -1
  21. package/src/components/ui/chat/utils.ts +85 -3
  22. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +72 -0
  23. package/src/components/ui/custom/onboarding-settings-panel.tsx +131 -0
  24. package/src/components/ui/custom/settings-dialog-shell.tsx +55 -18
  25. package/src/components/ui/custom/workspace-select-helpers.ts +50 -0
  26. package/src/components/ui/custom/workspace-select.tsx +37 -38
  27. package/src/components/ui/finance/analytics/analytics-page.tsx +10 -0
  28. package/src/components/ui/finance/analytics/inventory-provider-summary.tsx +110 -0
  29. package/src/components/ui/finance/finance-overview-metrics.tsx +49 -0
  30. package/src/components/ui/finance/transactions/inventory-reconciliation-entry-row.tsx +122 -0
  31. package/src/components/ui/finance/transactions/inventory-reconciliation-panel.tsx +407 -0
  32. package/src/components/ui/finance/transactions/transaction-card.tsx +15 -0
  33. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +31 -0
  34. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +26 -4
  35. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +22 -0
  36. package/src/components/ui/finance/transactions/transactions-page.tsx +8 -0
  37. package/src/components/ui/finance/wallets/walletId/inventory-wallet-contribution.tsx +84 -0
  38. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +4 -0
  39. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +9 -0
  40. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +27 -10
  41. package/src/components/ui/legacy/meet/edit-plan-dialog.tsx +90 -26
  42. package/src/components/ui/legacy/meet/form.tsx +25 -1
  43. package/src/components/ui/legacy/meet/lib/meet-ics.test.ts +40 -0
  44. package/src/components/ui/legacy/meet/lib/meet-ics.ts +50 -0
  45. package/src/components/ui/legacy/meet/lib/meet-insights.test.ts +76 -0
  46. package/src/components/ui/legacy/meet/lib/meet-insights.ts +219 -0
  47. package/src/components/ui/legacy/meet/lib/meet-timezone.test.ts +53 -0
  48. package/src/components/ui/legacy/meet/lib/meet-timezone.ts +45 -0
  49. package/src/components/ui/legacy/meet/page.tsx +1 -0
  50. package/src/components/ui/legacy/meet/planId/agenda-details.tsx +46 -23
  51. package/src/components/ui/legacy/meet/planId/copy-link-button.tsx +24 -197
  52. package/src/components/ui/legacy/meet/planId/date-planner.tsx +3 -28
  53. package/src/components/ui/legacy/meet/planId/meet-insights-panel.tsx +135 -0
  54. package/src/components/ui/legacy/meet/planId/meet-plan-live-root.tsx +41 -0
  55. package/src/components/ui/legacy/meet/planId/meet-query.ts +26 -0
  56. package/src/components/ui/legacy/meet/planId/meet-suggestions-panel.tsx +463 -0
  57. package/src/components/ui/legacy/meet/planId/page.tsx +58 -18
  58. package/src/components/ui/legacy/meet/planId/plan-details-client.tsx +154 -140
  59. package/src/components/ui/legacy/meet/planId/selectable-day-time.tsx +88 -56
  60. package/src/components/ui/legacy/meet/planId/show-qr-button.tsx +3 -5
  61. package/src/components/ui/legacy/meet/planId/unified-availability.tsx +36 -13
  62. package/src/components/ui/legacy/meet/planId/utility-buttons.tsx +3 -24
  63. package/src/components/ui/legacy/meet/plans-grid.tsx +1 -1
  64. package/src/components/ui/legacy/meet/plans-list-view.tsx +1 -1
  65. package/src/components/ui/nuqs-adapter.tsx +1 -0
  66. 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 { useEffect, useState } from 'react';
7
+ import { useState } from 'react';
20
8
 
21
- interface LinkFormat {
22
- name: string;
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
- <DropdownMenu>
118
- <DropdownMenuTrigger asChild>
119
- <Button
120
- className={cn(
121
- 'group relative w-full border-dynamic-purple/50 text-dynamic-purple transition-all duration-200 hover:border-dynamic-purple hover:bg-dynamic-purple/10 md:w-auto',
122
- copied && 'border-dynamic-purple bg-dynamic-purple/20',
123
- className
124
- )}
125
- variant="outline"
126
- disabled={copied || !url}
127
- >
128
- {copied ? (
129
- <Check className="mr-2 h-4 w-4 animate-pulse" />
130
- ) : (
131
- <LinkIcon className="mr-2 h-4 w-4 transition-transform duration-200 group-hover:scale-110" />
132
- )}
133
- <span className="transition-all duration-200">
134
- {copied ? 'Copied!' : t('copy_link')}
135
- </span>
136
- <ChevronDown className="ml-2 h-4 w-4 transition-transform duration-200 group-hover:rotate-180" />
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, editing, endEditing, setPreviewDate } = useTimeBlocking();
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
+ }