@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
@@ -0,0 +1,219 @@
1
+ import type {
2
+ MeetTogetherPlan,
3
+ PlanUser,
4
+ } from '@tuturuuu/types/primitives/MeetTogetherPlan';
5
+ import type { Timeblock } from '@tuturuuu/types/primitives/Timeblock';
6
+
7
+ export const MEET_SLOT_MINUTES = 15;
8
+
9
+ export interface MeetRankedTimeframe {
10
+ date: string;
11
+ startMinute: number;
12
+ endMinute: number;
13
+ confirmedUserIds: string[];
14
+ tentativeUserIds: string[];
15
+ unavailableUserIds: string[];
16
+ confirmedPercent: number;
17
+ weightedPercent: number;
18
+ }
19
+
20
+ export interface MeetPlanInsights {
21
+ participantCount: number;
22
+ respondedCount: number;
23
+ responsePercent: number;
24
+ peakAttendance: number;
25
+ peakAttendancePercent: number;
26
+ averageAvailabilityPercent: number;
27
+ overlapByDate: Array<{ date: string; peak: number }>;
28
+ availabilityByHour: Array<{ hour: number; percent: number }>;
29
+ }
30
+
31
+ function minuteFromTime(value?: string) {
32
+ const match = value?.match(/^(\d{1,2}):(\d{2})/u);
33
+ if (!match) return 0;
34
+ return Number(match[1]) * 60 + Number(match[2]);
35
+ }
36
+
37
+ function cellKey(date: string, minute: number) {
38
+ return `${date}:${minute}`;
39
+ }
40
+
41
+ function buildUserStatusMap(timeblocks: Timeblock[]) {
42
+ const status = new Map<string, 'confirmed' | 'tentative'>();
43
+
44
+ for (const block of timeblocks) {
45
+ if (!block.user_id || !block.date) continue;
46
+ const start = minuteFromTime(block.start_time);
47
+ const end = minuteFromTime(block.end_time);
48
+
49
+ for (let minute = start; minute < end; minute += MEET_SLOT_MINUTES) {
50
+ const key = `${block.user_id}:${cellKey(block.date, minute)}`;
51
+ if (!block.tentative || !status.has(key)) {
52
+ status.set(key, block.tentative ? 'tentative' : 'confirmed');
53
+ }
54
+ }
55
+ }
56
+
57
+ return status;
58
+ }
59
+
60
+ export function rankMeetTimeframes({
61
+ plan,
62
+ users,
63
+ timeblocks,
64
+ includeTentative = true,
65
+ }: {
66
+ plan: MeetTogetherPlan;
67
+ users: PlanUser[];
68
+ timeblocks: Timeblock[];
69
+ includeTentative?: boolean;
70
+ }): MeetRankedTimeframe[] {
71
+ const userIds = users.flatMap((user) => (user.id ? [user.id] : []));
72
+ const participantCount = userIds.length;
73
+ if (participantCount === 0) return [];
74
+
75
+ const startMinute = minuteFromTime(plan.start_time);
76
+ const endMinute = minuteFromTime(plan.end_time);
77
+ const duration = Math.max(MEET_SLOT_MINUTES, plan.duration_minutes ?? 60);
78
+ const status = buildUserStatusMap(timeblocks);
79
+ const ranked: MeetRankedTimeframe[] = [];
80
+
81
+ for (const date of plan.dates ?? []) {
82
+ for (
83
+ let candidateStart = startMinute;
84
+ candidateStart + duration <= endMinute;
85
+ candidateStart += MEET_SLOT_MINUTES
86
+ ) {
87
+ const confirmedUserIds: string[] = [];
88
+ const tentativeUserIds: string[] = [];
89
+ const unavailableUserIds: string[] = [];
90
+
91
+ for (const userId of userIds) {
92
+ let confirmed = true;
93
+ let available = true;
94
+
95
+ for (
96
+ let minute = candidateStart;
97
+ minute < candidateStart + duration;
98
+ minute += MEET_SLOT_MINUTES
99
+ ) {
100
+ const cellStatus = status.get(`${userId}:${cellKey(date, minute)}`);
101
+ confirmed &&= cellStatus === 'confirmed';
102
+ available &&=
103
+ cellStatus === 'confirmed' || cellStatus === 'tentative';
104
+ }
105
+
106
+ if (confirmed) confirmedUserIds.push(userId);
107
+ else if (available && includeTentative) tentativeUserIds.push(userId);
108
+ else unavailableUserIds.push(userId);
109
+ }
110
+
111
+ const confirmedPercent =
112
+ (confirmedUserIds.length / participantCount) * 100;
113
+ const weightedPercent =
114
+ ((confirmedUserIds.length + tentativeUserIds.length * 0.5) /
115
+ participantCount) *
116
+ 100;
117
+
118
+ ranked.push({
119
+ date,
120
+ startMinute: candidateStart,
121
+ endMinute: candidateStart + duration,
122
+ confirmedUserIds,
123
+ tentativeUserIds,
124
+ unavailableUserIds,
125
+ confirmedPercent,
126
+ weightedPercent,
127
+ });
128
+ }
129
+ }
130
+
131
+ return ranked.sort(
132
+ (a, b) =>
133
+ b.confirmedUserIds.length - a.confirmedUserIds.length ||
134
+ b.weightedPercent - a.weightedPercent ||
135
+ a.date.localeCompare(b.date) ||
136
+ a.startMinute - b.startMinute
137
+ );
138
+ }
139
+
140
+ export function calculateMeetPlanInsights({
141
+ plan,
142
+ users,
143
+ timeblocks,
144
+ }: {
145
+ plan: MeetTogetherPlan;
146
+ users: PlanUser[];
147
+ timeblocks: Timeblock[];
148
+ }): MeetPlanInsights {
149
+ const participantCount = users.filter((user) => user.id).length;
150
+ const respondedIds = new Set(
151
+ timeblocks.flatMap((block) => (block.user_id ? [block.user_id] : []))
152
+ );
153
+ const ranked = rankMeetTimeframes({ plan, users, timeblocks });
154
+ const overlapByDate = (plan.dates ?? []).map((date) => ({
155
+ date,
156
+ peak: Math.max(
157
+ 0,
158
+ ...ranked
159
+ .filter((candidate) => candidate.date === date)
160
+ .map((candidate) => candidate.confirmedUserIds.length)
161
+ ),
162
+ }));
163
+ const peakAttendance = Math.max(0, ...overlapByDate.map(({ peak }) => peak));
164
+ const averageAvailabilityPercent = ranked.length
165
+ ? ranked.reduce((sum, candidate) => sum + candidate.weightedPercent, 0) /
166
+ ranked.length
167
+ : 0;
168
+ const status = buildUserStatusMap(timeblocks);
169
+ const userIds = users.flatMap((user) => (user.id ? [user.id] : []));
170
+ const dates = plan.dates ?? [];
171
+ const startHour = Math.floor(minuteFromTime(plan.start_time) / 60);
172
+ const endHour = Math.ceil(minuteFromTime(plan.end_time) / 60);
173
+ const availabilityByHour = Array.from(
174
+ { length: Math.max(0, endHour - startHour) },
175
+ (_, index) => {
176
+ const hour = startHour + index;
177
+ let weightedCells = 0;
178
+ for (const date of dates) {
179
+ for (const userId of userIds) {
180
+ for (let minute = hour * 60; minute < hour * 60 + 60; minute += 15) {
181
+ const cellStatus = status.get(`${userId}:${cellKey(date, minute)}`);
182
+ weightedCells +=
183
+ cellStatus === 'confirmed'
184
+ ? 1
185
+ : cellStatus === 'tentative'
186
+ ? 0.5
187
+ : 0;
188
+ }
189
+ }
190
+ }
191
+ const possibleCells = dates.length * userIds.length * 4;
192
+ return {
193
+ hour,
194
+ percent: possibleCells ? (weightedCells / possibleCells) * 100 : 0,
195
+ };
196
+ }
197
+ );
198
+
199
+ return {
200
+ participantCount,
201
+ respondedCount: respondedIds.size,
202
+ responsePercent: participantCount
203
+ ? (respondedIds.size / participantCount) * 100
204
+ : 0,
205
+ peakAttendance,
206
+ peakAttendancePercent: participantCount
207
+ ? (peakAttendance / participantCount) * 100
208
+ : 0,
209
+ averageAvailabilityPercent,
210
+ overlapByDate,
211
+ availabilityByHour,
212
+ };
213
+ }
214
+
215
+ export function formatMinuteOfDay(minute: number) {
216
+ const hours = Math.floor(minute / 60);
217
+ const minutes = minute % 60;
218
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
219
+ }
@@ -0,0 +1,53 @@
1
+ import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
2
+ import { describe, expect, it } from 'vitest';
3
+ import type { MeetRankedTimeframe } from './meet-insights';
4
+ import { candidateToAbsoluteRange } from './meet-timezone';
5
+
6
+ const candidate: MeetRankedTimeframe = {
7
+ date: '2026-08-04',
8
+ startMinute: 540,
9
+ endMinute: 600,
10
+ confirmedUserIds: [],
11
+ tentativeUserIds: [],
12
+ unavailableUserIds: [],
13
+ confirmedPercent: 0,
14
+ weightedPercent: 0,
15
+ };
16
+
17
+ describe('Tuturuuu Meet timezone conversion', () => {
18
+ it('preserves the fixed offset of a legacy plan', () => {
19
+ const plan: MeetTogetherPlan = {
20
+ duration_minutes: 60,
21
+ start_time: '09:00:00+07',
22
+ };
23
+ expect(candidateToAbsoluteRange(candidate, plan)).toEqual({
24
+ startAt: '2026-08-04T02:00:00.000Z',
25
+ endAt: '2026-08-04T03:00:00.000Z',
26
+ });
27
+ });
28
+
29
+ it('uses IANA daylight-saving rules', () => {
30
+ const plan: MeetTogetherPlan = {
31
+ duration_minutes: 60,
32
+ start_time: '09:00:00-04',
33
+ timezone: 'America/New_York',
34
+ };
35
+ expect(candidateToAbsoluteRange(candidate, plan)?.startAt).toBe(
36
+ '2026-08-04T13:00:00.000Z'
37
+ );
38
+ });
39
+
40
+ it('rejects a nonexistent DST window', () => {
41
+ const plan: MeetTogetherPlan = {
42
+ duration_minutes: 60,
43
+ start_time: '01:00:00-05',
44
+ timezone: 'America/New_York',
45
+ };
46
+ expect(
47
+ candidateToAbsoluteRange(
48
+ { ...candidate, date: '2026-03-08', startMinute: 120, endMinute: 180 },
49
+ plan
50
+ )
51
+ ).toBeNull();
52
+ });
53
+ });
@@ -0,0 +1,45 @@
1
+ import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
2
+ import dayjs from 'dayjs';
3
+ import timezone from 'dayjs/plugin/timezone';
4
+ import utc from 'dayjs/plugin/utc';
5
+ import { formatMinuteOfDay, type MeetRankedTimeframe } from './meet-insights';
6
+
7
+ dayjs.extend(utc);
8
+ dayjs.extend(timezone);
9
+
10
+ function legacyOffset(plan: MeetTogetherPlan) {
11
+ const offset = plan.start_time?.match(/([+-]\d{2}(?::?\d{2})?)$/u)?.[1];
12
+ if (!offset) return 'Z';
13
+ if (/^[+-]\d{2}$/u.test(offset)) return `${offset}:00`;
14
+ if (/^[+-]\d{4}$/u.test(offset))
15
+ return `${offset.slice(0, 3)}:${offset.slice(3)}`;
16
+ return offset;
17
+ }
18
+
19
+ function localTimestamp(date: string, minute: number) {
20
+ return `${date}T${formatMinuteOfDay(minute)}:00`;
21
+ }
22
+
23
+ export function candidateToAbsoluteRange(
24
+ candidate: MeetRankedTimeframe,
25
+ plan: MeetTogetherPlan
26
+ ) {
27
+ const startLocal = localTimestamp(candidate.date, candidate.startMinute);
28
+ const endLocal = localTimestamp(candidate.date, candidate.endMinute);
29
+ const start = plan.timezone
30
+ ? dayjs.tz(startLocal, plan.timezone)
31
+ : dayjs(`${startLocal}${legacyOffset(plan)}`);
32
+ const end = plan.timezone
33
+ ? dayjs.tz(endLocal, plan.timezone)
34
+ : dayjs(`${endLocal}${legacyOffset(plan)}`);
35
+ const expectedDuration = plan.duration_minutes ?? 60;
36
+
37
+ if (
38
+ !start.isValid() ||
39
+ !end.isValid() ||
40
+ end.diff(start, 'minute') !== expectedDuration
41
+ )
42
+ return null;
43
+
44
+ return { startAt: start.toISOString(), endAt: end.toISOString() };
45
+ }
@@ -154,6 +154,7 @@ export async function MeetTogetherPage({
154
154
  <CardContent className="p-6 md:p-8">
155
155
  <Form
156
156
  wsId={wsId}
157
+ planBasePath={path || '/meet/plans'}
157
158
  user={
158
159
  user
159
160
  ? {
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { updatePlan } from '@tuturuuu/apis/meet/actions';
3
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
4
4
  import { ClipboardList, Pencil, Plus } from '@tuturuuu/icons';
5
+ import { type MeetPlanSnapshot, updateMeetPlan } from '@tuturuuu/internal-api';
5
6
  import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
6
7
  import type { JSONContent } from '@tuturuuu/types/tiptap';
7
8
  import { Button } from '@tuturuuu/ui/button';
@@ -16,14 +17,41 @@ interface AgendaDetailsProps {
16
17
  }
17
18
 
18
19
  export default function AgendaDetails({ plan }: AgendaDetailsProps) {
19
- const t = useTranslations('meet-together');
20
+ const t = useTranslations();
20
21
  const router = useRouter();
22
+ const queryClient = useQueryClient();
21
23
  const { user } = useTimeBlocking();
22
24
  const [editContent, setEditContent] = useState<JSONContent | null>(
23
25
  plan.agenda_content || null
24
26
  );
25
27
  const [isEditing, setIsEditing] = useState(false);
26
28
  const [isLoading, setIsLoading] = useState(false);
29
+ const mutation = useMutation({
30
+ mutationFn: (agendaContent: JSONContent | null) =>
31
+ updateMeetPlan(plan.id!, { agenda_content: agendaContent }),
32
+ onMutate: async (agendaContent) => {
33
+ if (!plan.id) return {};
34
+ const queryKey = ['meet-plan', plan.id] as const;
35
+ await queryClient.cancelQueries({ queryKey });
36
+ const previous = queryClient.getQueryData<MeetPlanSnapshot>(queryKey);
37
+ if (previous)
38
+ queryClient.setQueryData<MeetPlanSnapshot>(queryKey, {
39
+ ...previous,
40
+ plan: {
41
+ ...previous.plan,
42
+ agenda_content: agendaContent ?? undefined,
43
+ },
44
+ });
45
+ return { previous };
46
+ },
47
+ onError: (_error, _agenda, context) => {
48
+ if (plan.id && context?.previous)
49
+ queryClient.setQueryData(['meet-plan', plan.id], context.previous);
50
+ },
51
+ onSuccess: (snapshot) => {
52
+ if (plan.id) queryClient.setQueryData(['meet-plan', plan.id], snapshot);
53
+ },
54
+ });
27
55
 
28
56
  const handleEdit = useCallback(() => {
29
57
  setIsEditing(true);
@@ -40,23 +68,15 @@ export default function AgendaDetails({ plan }: AgendaDetailsProps) {
40
68
 
41
69
  setIsLoading(true);
42
70
  try {
43
- const result = await updatePlan(plan.id, {
44
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
- agenda_content: editContent as any,
46
- });
47
-
48
- if (result.data) {
49
- setIsEditing(false);
50
- router.refresh();
51
- } else {
52
- console.error('Failed to save agenda:', result.error);
53
- }
71
+ await mutation.mutateAsync(editContent);
72
+ setIsEditing(false);
73
+ router.refresh();
54
74
  } catch (error) {
55
75
  console.error('Error saving agenda:', error);
56
76
  } finally {
57
77
  setIsLoading(false);
58
78
  }
59
- }, [plan.id, editContent, router]);
79
+ }, [editContent, mutation, plan.id, router]);
60
80
 
61
81
  const handleContentChange = useCallback((content: JSONContent | null) => {
62
82
  setEditContent(content || null);
@@ -69,9 +89,9 @@ export default function AgendaDetails({ plan }: AgendaDetailsProps) {
69
89
  <div className="w-full space-y-8">
70
90
  <div className="flex flex-col items-start justify-between gap-4 md:flex-row">
71
91
  <div className="space-y-4">
72
- <p className="font-semibold text-4xl">{t('agenda')}</p>
92
+ <p className="font-semibold text-4xl">{t('meet-together.agenda')}</p>
73
93
  <p className="text-md text-muted-foreground">
74
- {t('agenda_description')}
94
+ {t('meet-together.agenda_description')}
75
95
  </p>
76
96
  </div>
77
97
  {isEditing ? (
@@ -82,22 +102,24 @@ export default function AgendaDetails({ plan }: AgendaDetailsProps) {
82
102
  size="lg"
83
103
  disabled={isLoading}
84
104
  >
85
- Cancel
105
+ {t('common.cancel')}
86
106
  </Button>
87
107
  <Button onClick={handleSave} size="lg" disabled={isLoading}>
88
- {isLoading ? 'Saving...' : 'Save'}
108
+ {isLoading
109
+ ? t('meet-together-plan-details.saving')
110
+ : t('common.save')}
89
111
  </Button>
90
112
  </div>
91
113
  ) : canEdit ? (
92
114
  plan.agenda_content ? (
93
115
  <Button onClick={handleEdit} variant="outline" size="lg">
94
116
  <Pencil size={16} />
95
- Edit
117
+ {t('common.edit')}
96
118
  </Button>
97
119
  ) : (
98
120
  <Button onClick={handleEdit} variant="default" size="lg">
99
121
  <Plus size={16} />
100
- Add Agenda
122
+ {t('meet-together-plan-details.add_agenda')}
101
123
  </Button>
102
124
  )
103
125
  ) : null}
@@ -117,10 +139,11 @@ export default function AgendaDetails({ plan }: AgendaDetailsProps) {
117
139
  <ClipboardList size={48} className="text-muted-foreground" />
118
140
  </div>
119
141
  <div className="space-y-2">
120
- <h3 className="font-medium text-lg">No Agenda Content Yet</h3>
142
+ <h3 className="font-medium text-lg">
143
+ {t('meet-together-plan-details.no_agenda_title')}
144
+ </h3>
121
145
  <p className="max-w-md text-muted-foreground text-sm">
122
- Start organizing your meeting by creating a structured outline
123
- to keep everyone on track to make your meeting more productive.
146
+ {t('meet-together-plan-details.no_agenda_description')}
124
147
  </p>
125
148
  </div>
126
149
  </div>