Skip to content

Commit d4ea78b

Browse files
committed
Enhance UI, add save button
1 parent 60d90bd commit d4ea78b

File tree

4 files changed

+138
-121
lines changed

4 files changed

+138
-121
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,13 @@ export default function AllAvailabilities({
3131
];
3232

3333
return (
34-
<div className="flex flex-col gap-2 text-center">
35-
<div className="font-semibold">{t('everyone_availability')}</div>
34+
<div
35+
className="flex flex-col items-center gap-2 text-center"
36+
style={{ minWidth: '260px' }}
37+
>
38+
<div className="text-center font-semibold">
39+
{t('everyone_availability')}
40+
</div>
3641

3742
<div className="flex items-center justify-center gap-2 text-sm">
3843
<div>
Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,57 @@
11
import DatePlanner from './date-planner';
2+
import { useTimeBlocking } from './time-blocking-provider';
23
import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
34
import type { Timeblock } from '@tuturuuu/types/primitives/Timeblock';
45
import { useTranslations } from 'next-intl';
6+
import { useEffect, useState } from 'react';
57

68
export default function AvailabilityPlanner({
79
plan,
810
timeblocks,
911
disabled,
12+
className = '', // allow parent to control alignment
1013
}: {
1114
plan: MeetTogetherPlan;
1215
timeblocks: Timeblock[];
1316
disabled?: boolean;
17+
className?: string;
1418
}) {
1519
const t = useTranslations('meet-together-plan-details');
20+
const { selectedTimeBlocks, syncTimeBlocks, editing } = useTimeBlocking();
21+
const [isSaving, setIsSaving] = useState(false);
22+
const [dirty, setDirty] = useState(false);
1623

17-
return (
18-
<div className="flex flex-col gap-2 text-center">
19-
<div className="font-semibold">{t('your_availability')}</div>
24+
// Mark as dirty when editing ends
25+
useEffect(() => {
26+
if (!editing.enabled) {
27+
setDirty(true);
28+
}
29+
}, [editing.enabled]);
30+
31+
// Optionally, reset dirty flag after save
32+
const handleSave = async () => {
33+
setIsSaving(true);
34+
await syncTimeBlocks();
35+
setIsSaving(false);
36+
setDirty(false);
37+
};
2038

21-
<div className="flex items-center justify-center gap-4 text-sm">
39+
return (
40+
<div
41+
className={`flex flex-col items-center ${className}`}
42+
style={{ minWidth: '260px' }}
43+
>
44+
<div className="text-center font-semibold">{t('your_availability')}</div>
45+
<div className="mt-1 mb-2 flex items-center justify-center gap-4 text-sm">
2246
<div className="flex items-center gap-2">
2347
<div>{t('unavailable')}</div>
2448
<div className="h-4 w-8 border border-foreground/50 bg-red-500/20" />
2549
</div>
26-
2750
<div className="flex items-center gap-2">
2851
<div>{t('available')}</div>
2952
<div className="h-4 w-8 border border-foreground/50 bg-green-500/70" />
3053
</div>
3154
</div>
32-
3355
<DatePlanner
3456
timeblocks={timeblocks}
3557
dates={plan.dates}
@@ -38,6 +60,18 @@ export default function AvailabilityPlanner({
3860
disabled={disabled}
3961
editable
4062
/>
63+
<button
64+
className="mx-auto mt-8 block rounded-full bg-gradient-to-r from-indigo-500 via-sky-500 to-emerald-500 px-8 py-3 text-lg font-semibold text-white shadow-md transition-all duration-150 ease-in-out hover:scale-105 hover:from-indigo-600 hover:via-sky-600 hover:to-emerald-600 focus:ring-2 focus:ring-sky-400 focus:ring-offset-2 focus:outline-none active:scale-100 disabled:cursor-not-allowed disabled:from-gray-400 disabled:via-gray-400 disabled:to-gray-400"
65+
style={{
66+
minWidth: '120px',
67+
minHeight: '48px',
68+
letterSpacing: '0.01em',
69+
}}
70+
onClick={handleSave}
71+
disabled={!dirty || isSaving}
72+
>
73+
{isSaving ? 'Saving...' : 'Save'}
74+
</button>
4175
</div>
4276
);
4377
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,10 @@ export default function PlanDetailsClient({
140140
handlePNG={downloadAsPNG}
141141
/>
142142
<div id="plan-ref" className="flex w-full flex-col items-center">
143-
<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">
143+
<p className="mx-auto my-4 flex max-w-xl items-center justify-center gap-2 text-center text-2xl leading-tight! font-semibold md:mb-4 lg:text-3xl">
144144
{plan.name} <EditPlanDialog plan={plan} />
145145
</p>
146-
<div className="mt-8 grid w-full items-center justify-between gap-4 md:grid-cols-2">
146+
<div className="mx-auto mt-8 flex w-full flex-col items-center justify-center gap-8 md:flex-row md:gap-16 lg:gap-32 xl:gap-80">
147147
<PlanLogin
148148
plan={plan}
149149
timeblocks={[]}

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

Lines changed: 89 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const TimeBlockContext = createContext({
7373
prev: 'login' | 'account-switcher' | undefined
7474
) => 'login' | 'account-switcher' | undefined)
7575
) => {},
76+
syncTimeBlocks: () => Promise.resolve(),
7677
});
7778

7879
const TimeBlockingProvider = ({
@@ -272,16 +273,29 @@ const TimeBlockingProvider = ({
272273
});
273274
}, [plan.id, editing]);
274275

275-
useEffect(() => {
276+
// --- Move syncTimeBlocks outside useEffect and expose it via context ---
277+
const syncTimeBlocks = useCallback(async () => {
278+
if (!plan.id || !user?.id) return;
279+
280+
const fetchCurrentTimeBlocks = async (planId: string) => {
281+
const res = await fetch(`/api/meet-together/plans/${planId}/timeblocks`);
282+
if (!res.ok) return [];
283+
const timeblocks = (await res.json()) as Timeblock[];
284+
return timeblocks
285+
?.flat()
286+
.filter(
287+
(tb: Timeblock) =>
288+
tb.user_id === user?.id && tb.is_guest === (user?.is_guest ?? false)
289+
);
290+
};
291+
276292
const addTimeBlock = async (timeblock: Timeblock) => {
277293
if (plan.id !== selectedTimeBlocks.planId) return;
278-
279294
const data = {
280295
user_id: user?.id,
281296
password_hash: user?.password_hash,
282297
timeblock,
283298
};
284-
285299
await fetch(`/api/meet-together/plans/${plan.id}/timeblocks`, {
286300
method: 'POST',
287301
body: JSON.stringify(data),
@@ -290,12 +304,10 @@ const TimeBlockingProvider = ({
290304

291305
const removeTimeBlock = async (timeblock: Timeblock) => {
292306
if (plan.id !== selectedTimeBlocks.planId) return;
293-
294307
const data = {
295308
user_id: user?.id,
296309
password_hash: user?.password_hash,
297310
};
298-
299311
await fetch(
300312
`/api/meet-together/plans/${plan.id}/timeblocks/${timeblock.id}`,
301313
{
@@ -305,115 +317,80 @@ const TimeBlockingProvider = ({
305317
);
306318
};
307319

308-
const fetchCurrentTimeBlocks = async (planId: string) => {
309-
const res = await fetch(`/api/meet-together/plans/${planId}/timeblocks`);
310-
if (!res.ok) return [];
311-
312-
const timeblocks = (await res.json()) as Timeblock[];
313-
return timeblocks
314-
?.flat()
315-
.filter(
316-
(tb: Timeblock) =>
317-
tb.user_id === user?.id && tb.is_guest === (user?.is_guest ?? false)
318-
);
319-
};
320-
321-
const syncTimeBlocks = async () => {
322-
if (!plan.id || !user?.id) return;
323-
324-
const serverTimeblocks = await fetchCurrentTimeBlocks(plan?.id);
325-
const localTimeblocks = selectedTimeBlocks.data;
326-
327-
if (!serverTimeblocks || !localTimeblocks) return;
328-
if (serverTimeblocks.length === 0 && localTimeblocks.length === 0) return;
329-
330-
// If server timeblocks are empty and local timeblocks are not,
331-
// add all local timeblocks to server
332-
if (serverTimeblocks.length === 0 && localTimeblocks.length > 0) {
333-
await Promise.all(
334-
localTimeblocks.map((timeblock) => addTimeBlock(timeblock))
335-
);
336-
return;
337-
}
338-
339-
// If local timeblocks are empty, remove all server timeblocks
340-
if (serverTimeblocks.length > 0 && localTimeblocks.length === 0) {
341-
await Promise.all(
342-
serverTimeblocks.map((timeblock) => removeTimeBlock(timeblock))
343-
);
344-
return;
345-
}
346-
347-
// If there are no timeblocks to sync (both local and server have
348-
// the same timeblocks), return early
349-
if (
350-
serverTimeblocks.every((serverTimeblock: Timeblock) =>
351-
localTimeblocks.some(
352-
(localTimeblock: Timeblock) =>
353-
localTimeblock.date === serverTimeblock.date &&
354-
localTimeblock.start_time === serverTimeblock.start_time &&
355-
localTimeblock.end_time === serverTimeblock.end_time
356-
)
357-
) &&
358-
localTimeblocks.every((localTimeblock: Timeblock) =>
359-
serverTimeblocks.some(
360-
(serverTimeblock: Timeblock) =>
361-
serverTimeblock.date === localTimeblock.date &&
362-
serverTimeblock.start_time === localTimeblock.start_time &&
363-
serverTimeblock.end_time === localTimeblock.end_time
364-
)
365-
) &&
366-
serverTimeblocks.length === localTimeblocks.length
367-
)
368-
return;
369-
370-
// For each time block, remove timeblocks that are not on local
371-
// and add timeblocks that are not on server
372-
const timeblocksToRemove = serverTimeblocks.filter(
373-
(serverTimeblock: Timeblock) =>
374-
!localTimeblocks.some(
375-
(localTimeblock: Timeblock) =>
376-
localTimeblock.date === serverTimeblock.date &&
377-
localTimeblock.start_time === serverTimeblock.start_time &&
378-
localTimeblock.end_time === serverTimeblock.end_time
379-
)
320+
const serverTimeblocks = await fetchCurrentTimeBlocks(plan?.id);
321+
const localTimeblocks = selectedTimeBlocks.data;
322+
if (!serverTimeblocks || !localTimeblocks) return;
323+
if (serverTimeblocks.length === 0 && localTimeblocks.length === 0) return;
324+
if (serverTimeblocks.length === 0 && localTimeblocks.length > 0) {
325+
await Promise.all(
326+
localTimeblocks.map((timeblock) => addTimeBlock(timeblock))
380327
);
381-
382-
const timeblocksToAdd = localTimeblocks.filter(
383-
(localTimeblock: Timeblock) =>
384-
!serverTimeblocks?.some(
385-
(serverTimeblock: Timeblock) =>
386-
serverTimeblock.date === localTimeblock.date &&
387-
serverTimeblock.start_time === localTimeblock.start_time &&
388-
serverTimeblock.end_time === localTimeblock.end_time
389-
)
328+
return;
329+
}
330+
if (serverTimeblocks.length > 0 && localTimeblocks.length === 0) {
331+
await Promise.all(
332+
serverTimeblocks.map((timeblock) => removeTimeBlock(timeblock))
390333
);
334+
return;
335+
}
336+
if (
337+
serverTimeblocks.every((serverTimeblock: Timeblock) =>
338+
localTimeblocks.some(
339+
(localTimeblock: Timeblock) =>
340+
localTimeblock.date === serverTimeblock.date &&
341+
localTimeblock.start_time === serverTimeblock.start_time &&
342+
localTimeblock.end_time === serverTimeblock.end_time
343+
)
344+
) &&
345+
localTimeblocks.every((localTimeblock: Timeblock) =>
346+
serverTimeblocks.some(
347+
(serverTimeblock: Timeblock) =>
348+
serverTimeblock.date === localTimeblock.date &&
349+
serverTimeblock.start_time === localTimeblock.start_time &&
350+
serverTimeblock.end_time === localTimeblock.end_time
351+
)
352+
) &&
353+
serverTimeblocks.length === localTimeblocks.length
354+
)
355+
return;
356+
const timeblocksToRemove = serverTimeblocks.filter(
357+
(serverTimeblock: Timeblock) =>
358+
!localTimeblocks.some(
359+
(localTimeblock: Timeblock) =>
360+
localTimeblock.date === serverTimeblock.date &&
361+
localTimeblock.start_time === serverTimeblock.start_time &&
362+
localTimeblock.end_time === serverTimeblock.end_time
363+
)
364+
);
365+
const timeblocksToAdd = localTimeblocks.filter(
366+
(localTimeblock: Timeblock) =>
367+
!serverTimeblocks?.some(
368+
(serverTimeblock: Timeblock) =>
369+
serverTimeblock.date === localTimeblock.date &&
370+
serverTimeblock.start_time === localTimeblock.start_time &&
371+
serverTimeblock.end_time === localTimeblock.end_time
372+
)
373+
);
374+
if (timeblocksToRemove.length === 0 && timeblocksToAdd.length === 0) return;
375+
if (timeblocksToRemove.length > 0)
376+
await Promise.all(
377+
timeblocksToRemove.map((timeblock) =>
378+
timeblock.id ? removeTimeBlock(timeblock) : null
379+
)
380+
);
381+
if (timeblocksToAdd.length > 0)
382+
await Promise.all(
383+
timeblocksToAdd.map((timeblock) => addTimeBlock(timeblock))
384+
);
385+
const syncedServerTimeblocks = await fetchCurrentTimeBlocks(plan?.id);
386+
setSelectedTimeBlocks({
387+
planId: plan.id,
388+
data: syncedServerTimeblocks,
389+
});
390+
}, [plan.id, user, selectedTimeBlocks]);
391391

392-
if (timeblocksToRemove.length === 0 && timeblocksToAdd.length === 0)
393-
return;
394-
395-
if (timeblocksToRemove.length > 0)
396-
await Promise.all(
397-
timeblocksToRemove.map((timeblock) =>
398-
timeblock.id ? removeTimeBlock(timeblock) : null
399-
)
400-
);
401-
402-
if (timeblocksToAdd.length > 0)
403-
await Promise.all(
404-
timeblocksToAdd.map((timeblock) => addTimeBlock(timeblock))
405-
);
406-
407-
const syncedServerTimeblocks = await fetchCurrentTimeBlocks(plan?.id);
408-
setSelectedTimeBlocks({
409-
planId: plan.id,
410-
data: syncedServerTimeblocks,
411-
});
412-
};
413-
414-
if (editing.enabled) return;
415-
syncTimeBlocks();
416-
}, [plan.id, user, selectedTimeBlocks, editing.enabled]);
392+
// --- Remove the auto-sync useEffect ---
393+
// useEffect(() => { ... if (editing.enabled) return; syncTimeBlocks(); }, [plan.id, user, selectedTimeBlocks, editing.enabled]);
417394

418395
return (
419396
<TimeBlockContext.Provider
@@ -436,6 +413,7 @@ const TimeBlockingProvider = ({
436413
edit,
437414
endEditing,
438415
setDisplayMode,
416+
syncTimeBlocks, // Expose syncTimeBlocks in context
439417
}}
440418
>
441419
{children}

0 commit comments

Comments
 (0)