Skip to content

Commit 9f84671

Browse files
committed
feat: enhance availability planning with best times feature
- Added a toggle to show only the best available times based on user availability. - Updated components to support best times status tracking and display. - Refactored related components to integrate the new feature seamlessly. - Improved user experience with tooltips and alerts for better clarity on best times functionality.
1 parent cd34622 commit 9f84671

File tree

9 files changed

+236
-46
lines changed

9 files changed

+236
-46
lines changed

apps/web/src/app/[locale]/(marketing)/meet-together/plans/[planId]/account-badge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function AccountBadge({ type }: { type: 'GUEST' | 'PLATFORM' }) {
1818
} mt-2 rounded px-2 py-1 text-sm font-semibold`}
1919
>
2020
<span
21-
className={`bg-linear-to-r bg-clip-text text-transparent text-white`}
21+
className={`bg-linear-to-r bg-clip-text text-white`}
2222
>
2323
{t(type === 'GUEST' ? 'guest_account' : 'tuturuuu_account')}
2424
</span>

apps/web/src/app/[locale]/(marketing)/meet-together/plans/[planId]/all-availabilities.tsx

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
import DatePlanner from './date-planner';
44
import { useTimeBlocking } from './time-blocking-provider';
5-
import { getBestMeetingTimes } from '@/utils/timeblock-helper';
65
import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
76
import type { Timeblock } from '@tuturuuu/types/primitives/Timeblock';
87
import { useTranslations } from 'next-intl';
98

109
export default function AllAvailabilities({
1110
plan,
1211
timeblocks,
12+
showBestTimes = false,
13+
onBestTimesStatusByDateAction,
1314
}: {
1415
plan: MeetTogetherPlan;
1516
timeblocks: Timeblock[];
17+
showBestTimes?: boolean;
18+
onBestTimesStatusByDateAction?: (status: Record<string, boolean>) => void;
1619
}) {
1720
const t = useTranslations('meet-together-plan-details');
1821
const { user, planUsers, selectedTimeBlocks, filteredUserIds } =
@@ -31,31 +34,8 @@ export default function AllAvailabilities({
3134
})),
3235
];
3336

34-
// --- Best time calculation ---
35-
const bestTimes = getBestMeetingTimes({
36-
timeblocks: localTimeblocks,
37-
users: planUsers.map((u) => ({ id: u.id ?? null })),
38-
dates: plan.dates || [],
39-
start: plan.start_time || '08:00:00+00:00',
40-
end: plan.end_time || '18:00:00+00:00',
41-
slotMinutes: 15,
42-
});
43-
4437
return (
4538
<div className="flex flex-col gap-2 text-center">
46-
{/* Best time display */}
47-
{bestTimes.length > 0 && (
48-
<div className="mb-2 rounded bg-green-100 p-2 text-green-900 dark:bg-green-900 dark:text-green-100">
49-
<div className="font-semibold">Best time:</div>
50-
{bestTimes.map((slot, idx) => (
51-
<div key={idx} className="text-sm">
52-
{slot.date} {slot.start_time.slice(0, 5)} -{' '}
53-
{slot.end_time.slice(0, 5)} ({slot.availableUserIds.length}/
54-
{totalUserCount} {t('available')})
55-
</div>
56-
))}
57-
</div>
58-
)}
5939
{/* Existing UI */}
6040
<div className="font-semibold">{t('everyone_availability')}</div>
6141
<div className="flex items-center justify-center gap-2 text-sm">
@@ -93,6 +73,8 @@ export default function AllAvailabilities({
9373
dates={plan.dates}
9474
start={plan.start_time}
9575
end={plan.end_time}
76+
showBestTimes={showBestTimes}
77+
onBestTimesStatusByDateAction={onBestTimesStatusByDateAction}
9678
/>
9779
</div>
9880
);

apps/web/src/app/[locale]/(marketing)/meet-together/plans/[planId]/date-planner.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ export default function DatePlanner({
1313
end,
1414
editable = false,
1515
disabled = false,
16+
showBestTimes = false,
17+
onBestTimesStatusByDateAction,
1618
}: {
1719
timeblocks: Timeblock[];
1820
dates?: string[];
1921
start?: string;
2022
end?: string;
2123
editable?: boolean;
2224
disabled?: boolean;
25+
showBestTimes?: boolean;
26+
onBestTimesStatusByDateAction?: (status: Record<string, boolean>) => void;
2327
}) {
2428
const { user, editing, endEditing, setPreviewDate } = useTimeBlocking();
2529

@@ -82,6 +86,8 @@ export default function DatePlanner({
8286
end={endHour}
8387
editable={editable}
8488
disabled={editable ? (user ? disabled : true) : disabled}
89+
showBestTimes={showBestTimes}
90+
onBestTimesStatusByDateAction={onBestTimesStatusByDateAction}
8591
/>
8692
</div>
8793
)}

apps/web/src/app/[locale]/(marketing)/meet-together/plans/[planId]/day-planner.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ export default function DayPlanner({
1111
end,
1212
editable,
1313
disabled,
14+
showBestTimes = false,
15+
onBestTimesStatus,
1416
}: {
1517
timeblocks: Timeblock[];
1618
date: string;
1719
start: number;
1820
end: number;
1921
editable: boolean;
2022
disabled: boolean;
23+
showBestTimes?: boolean;
24+
onBestTimesStatus?: (hasBestTimes: boolean) => void;
2125
}) {
2226
const locale = useLocale();
2327
dayjs.locale(locale);
@@ -42,6 +46,8 @@ export default function DayPlanner({
4246
end={end}
4347
editable={editable}
4448
disabled={disabled}
49+
showBestTimes={showBestTimes}
50+
onBestTimesStatus={onBestTimesStatus}
4551
/>
4652
</div>
4753
);

apps/web/src/app/[locale]/(marketing)/meet-together/plans/[planId]/day-planners.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import DayPlanner from './day-planner';
22
import { useTimeBlocking } from './time-blocking-provider';
33
import type { Timeblock } from '@tuturuuu/types/primitives/Timeblock';
4-
import { useEffect } from 'react';
4+
import { useEffect, useState } from 'react';
55

66
export default function DayPlanners({
77
timeblocks,
@@ -10,16 +10,38 @@ export default function DayPlanners({
1010
end,
1111
editable,
1212
disabled,
13+
showBestTimes = false,
14+
onBestTimesStatusByDateAction,
1315
}: {
1416
timeblocks: Timeblock[];
1517
dates: string[];
1618
start: number;
1719
end: number;
1820
editable: boolean;
1921
disabled: boolean;
22+
showBestTimes?: boolean;
23+
onBestTimesStatusByDateAction?: (status: Record<string, boolean>) => void;
2024
}) {
2125
const { editing } = useTimeBlocking();
2226

27+
const [bestTimesStatus, setBestTimesStatus] = useState<
28+
Record<string, boolean>
29+
>({});
30+
31+
useEffect(() => {
32+
if (onBestTimesStatusByDateAction) {
33+
onBestTimesStatusByDateAction(bestTimesStatus);
34+
}
35+
// eslint-disable-next-line react-hooks/exhaustive-deps
36+
}, [JSON.stringify(bestTimesStatus)]);
37+
38+
function handleBestTimesStatus(date: string, hasBestTimes: boolean) {
39+
setBestTimesStatus((prev) => {
40+
if (prev[date] === hasBestTimes) return prev;
41+
return { ...prev, [date]: hasBestTimes };
42+
});
43+
}
44+
2345
function preventScroll(e: any) {
2446
e.preventDefault();
2547
return false;
@@ -59,6 +81,10 @@ export default function DayPlanners({
5981
editable={editable}
6082
disabled={disabled}
6183
timeblocks={timeblocks.filter((tb) => tb.date === d)}
84+
showBestTimes={showBestTimes}
85+
onBestTimesStatus={(hasBestTimes) =>
86+
handleBestTimesStatus(d, hasBestTimes)
87+
}
6288
/>
6389
))}
6490
</div>

apps/web/src/app/[locale]/(marketing)/meet-together/plans/[planId]/day-time.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Timeblock } from '@tuturuuu/types/primitives/Timeblock';
44

55
export default function DayTime({
66
editable,
7+
onBestTimesStatus,
78
...props
89
}: {
910
timeblocks: Timeblock[];
@@ -12,7 +13,9 @@ export default function DayTime({
1213
end: number;
1314
editable: boolean;
1415
disabled: boolean;
16+
showBestTimes?: boolean;
17+
onBestTimesStatus?: (hasBestTimes: boolean) => void;
1518
}) {
1619
if (editable) return <SelectableDayTime {...props} />;
17-
return <PreviewDayTime {...props} />;
20+
return <PreviewDayTime {...props} onBestTimesStatus={onBestTimesStatus} />;
1821
}

apps/web/src/app/[locale]/(marketing)/meet-together/plans/[planId]/edit-plan-dialog.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export default function EditPlanDialog({ plan }: Props) {
7171
const isSubmitting = form.formState.isSubmitting;
7272

7373
const disabled = !isValid || isSubmitting;
74+
const watchedName = form.watch('name');
7475

7576
const handleSubmit = async () => {
7677
setUpdating(true);
@@ -177,7 +178,7 @@ export default function EditPlanDialog({ plan }: Props) {
177178
type="submit"
178179
className="w-full"
179180
disabled={
180-
plan.name === form.getValues('name') ||
181+
plan.name === watchedName ||
181182
disabled ||
182183
updating ||
183184
deleting
@@ -191,7 +192,7 @@ export default function EditPlanDialog({ plan }: Props) {
191192
<Separator />
192193

193194
<AlertDialog>
194-
<AlertDialogTrigger>
195+
<AlertDialogTrigger asChild>
195196
<Button
196197
type="button"
197198
className="w-full"

apps/web/src/app/[locale]/(marketing)/meet-together/plans/[planId]/plan-details-client.tsx

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import AllAvailabilities from './all-availabilities';
44
import EditPlanDialog from './edit-plan-dialog';
55
import PlanLogin from './plan-login';
66
import PlanUserFilter from './plan-user-filter';
7+
import { useTimeBlocking } from './time-blocking-provider';
78
import UtilityButtons from './utility-buttons';
89
import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
910
import type { User } from '@tuturuuu/types/primitives/User';
11+
import { Label } from '@tuturuuu/ui/label';
1012
import { Separator } from '@tuturuuu/ui/separator';
13+
import { Switch } from '@tuturuuu/ui/switch';
14+
import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
1115
import html2canvas from 'html2canvas-pro';
16+
import { CircleQuestionMark } from 'lucide-react';
1217
import { useTheme } from 'next-themes';
13-
import { useCallback } from 'react';
18+
import React, { useCallback, useState } from 'react';
1419

1520
interface PlanDetailsClientProps {
1621
plan: MeetTogetherPlan;
@@ -40,6 +45,24 @@ export default function PlanDetailsClient({
4045
timeblocks,
4146
}: PlanDetailsClientProps) {
4247
const { resolvedTheme } = useTheme();
48+
const [showBestTimes, setShowBestTimes] = useState(false);
49+
const { filteredUserIds } = useTimeBlocking();
50+
51+
// If user filter is active, force best times off
52+
const isUserFilterActive = filteredUserIds && filteredUserIds.length > 0;
53+
if (isUserFilterActive && showBestTimes) {
54+
setShowBestTimes(false);
55+
}
56+
57+
// Best times status state
58+
const [bestTimesStatusByDate, setBestTimesStatusByDate] = useState<
59+
Record<string, boolean>
60+
>({});
61+
const allDates = plan.dates || [];
62+
const noBestTimesFound =
63+
showBestTimes &&
64+
allDates.length > 0 &&
65+
allDates.every((d) => bestTimesStatusByDate[d] === false);
4366

4467
const downloadAsPNG = useCallback(async () => {
4568
const element = document.getElementById('plan-ref');
@@ -143,13 +166,71 @@ export default function PlanDetailsClient({
143166
<p className="my-4 flex max-w-xl items-center gap-2 text-center text-2xl leading-tight! font-semibold md:mb-4 lg:text-3xl">
144167
{plan.name} <EditPlanDialog plan={plan} />
145168
</p>
169+
<div className="mb-4 flex flex-col items-center justify-center gap-2">
170+
<div className="flex items-center justify-center gap-2">
171+
<Label
172+
htmlFor="show-best-times-toggle"
173+
className="flex cursor-pointer items-center gap-1 text-sm"
174+
>
175+
Show Only Best Times
176+
</Label>
177+
<Tooltip>
178+
<TooltipTrigger asChild>
179+
<span className="ml-1 inline-flex cursor-pointer items-center justify-center">
180+
<CircleQuestionMark size={16} />
181+
</span>
182+
</TooltipTrigger>
183+
<TooltipContent side="top">
184+
<div>
185+
<div>
186+
<b>Show Only Best Times</b> highlights the time slots that
187+
work best for the most people.
188+
</div>
189+
<ul className="mt-2 list-disc pl-4 text-xs">
190+
<li>
191+
Only time slots where <b>more than 2 people</b> are
192+
available are highlighted.
193+
</li>
194+
<li>
195+
If you filter by user, this feature is <b>disabled</b>{' '}
196+
(since &quot;best time&quot; only makes sense for
197+
groups).
198+
</li>
199+
</ul>
200+
<div className="mt-2 text-xs text-muted-foreground">
201+
We&apos;re always tweaking this feature for clarity and
202+
usefulness. Let us know if you have feedback!
203+
</div>
204+
</div>
205+
</TooltipContent>
206+
</Tooltip>
207+
<Switch
208+
id="show-best-times-toggle"
209+
checked={showBestTimes}
210+
onCheckedChange={() => setShowBestTimes((v) => !v)}
211+
disabled={isUserFilterActive}
212+
/>
213+
</div>
214+
{noBestTimesFound && (
215+
<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">
216+
<strong>No best times were found!</strong>
217+
<br />
218+
Encourage your group to sync up or adjust their availability.
219+
</div>
220+
)}
221+
</div>
146222
<div className="mt-8 grid w-full items-center justify-between gap-4 md:grid-cols-2">
147223
<PlanLogin
148224
plan={plan}
149225
timeblocks={[]}
150226
platformUser={platformUser}
151227
/>
152-
<AllAvailabilities plan={plan} timeblocks={timeblocks} />
228+
<AllAvailabilities
229+
plan={plan}
230+
timeblocks={timeblocks}
231+
showBestTimes={showBestTimes}
232+
onBestTimesStatusByDateAction={setBestTimesStatusByDate}
233+
/>
153234
</div>
154235
</div>
155236
</div>

0 commit comments

Comments
 (0)