Skip to content

Commit 2ea33e6

Browse files
wip(calendar): add recur support to front-end
1 parent d9cbf22 commit 2ea33e6

File tree

11 files changed

+111
-15
lines changed

11 files changed

+111
-15
lines changed

components/availability-select/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ function AvailabilitySelect({
165165
const onClick = useCallback(
166166
(event: MouseEvent) => {
167167
const position = { x: event.clientX - x, y: event.clientY - y };
168-
updateTimeslot(-1, getTimeslot(48, position, nanoid()));
168+
const original = new Timeslot({ id: nanoid() });
169+
updateTimeslot(-1, getTimeslot(48, position, original));
169170
},
170171
[x, y, updateTimeslot]
171172
);

components/availability-select/timeslot-rnd.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ export default function TimeslotRnd({
5050
const remove = useCallback(() => onChange(undefined), [onChange]);
5151
const update = useCallback(
5252
(newHeight: number, newPosition: Position) => {
53-
onChange(getTimeslot(newHeight, newPosition, value.id, width));
53+
onChange(getTimeslot(newHeight, newPosition, value, width));
5454
},
55-
[width, onChange, value.id]
55+
[width, onChange, value]
5656
);
5757

5858
const onClick = useCallback((e: ReactMouseEvent) => e.stopPropagation(), []);

components/availability-select/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function getHeight(timeslot: Timeslot): number {
2020
export function getTimeslot(
2121
height: number,
2222
position: Position,
23-
timeslotId?: string,
23+
original?: Timeslot,
2424
width: number = WIDTH,
2525
reference: Date = new Date(0)
2626
): Timeslot {
@@ -43,5 +43,5 @@ export function getTimeslot(
4343
const start = getDate(weekday, hours, mins, 0, 0, reference);
4444
const end = new Date(start.valueOf() + minsDuration * 60000);
4545

46-
return new Timeslot({ from: start, to: end, id: timeslotId });
46+
return new Timeslot({ ...original, from: start, to: end });
4747
}

components/calendar/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { Meeting, MeetingsQuery } from 'lib/model';
55
export interface CalendarContextValue {
66
startingDate: Date;
77
mutateMeeting: (mutated: Meeting, hasBeenUpdated?: boolean) => Promise<void>;
8-
removeMeeting: (meetingId: string) => Promise<void>;
8+
removeMeeting: (meetingId: string, hasBeenDeleted?: boolean) => Promise<void>;
99
}
1010

1111
export const CalendarContext = createContext<CalendarContextValue>({
1212
startingDate: new MeetingsQuery().from,
1313
mutateMeeting: async (mutated: Meeting, hasBeenUpdated?: boolean) => {},
14-
removeMeeting: async (meetingId: string) => {},
14+
removeMeeting: async (meetingId: string, hasBeenDeleted?: boolean) => {},
1515
});
1616

1717
export const useCalendar = (): CalendarContextValue =>

components/calendar/dialogs/create/create-page.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import useTranslation from 'next-translate/useTranslation';
55

66
import Button from 'components/button';
77
import MatchSelect from 'components/match-select';
8+
import RecurSelect from 'components/recur-select';
89
import SubjectSelect from 'components/subject-select';
910
import TimeSelect from 'components/time-select';
1011
import { useNav } from 'components/dialog/context';
@@ -39,6 +40,7 @@ export default function CreatePage({
3940
setLoading,
4041
setChecked,
4142
}: CreatePageProps): JSX.Element {
43+
// TODO: Revalidate local data after creation to account for recur rules.
4244
const updateRemote = useCallback(async (updated: Meeting) => {
4345
const created = new Meeting(clone({ ...updated, id: '' })).toJSON();
4446
const { data } = await axios.post<MeetingJSON>('/api/meetings', created);
@@ -89,7 +91,18 @@ export default function CreatePage({
8991
[setMeeting]
9092
);
9193
const onTimeChange = useCallback(
92-
(time: Timeslot) => setMeeting((prev) => new Meeting({ ...prev, time })),
94+
(time: Timeslot) => {
95+
setMeeting((prev) => new Meeting({ ...prev, time }));
96+
},
97+
[setMeeting]
98+
);
99+
const onRecurChange = useCallback(
100+
(recur: string) => {
101+
setMeeting((prev) => {
102+
const time = new Timeslot({ ...prev.time, recur });
103+
return new Meeting({ ...prev, time });
104+
});
105+
},
93106
[setMeeting]
94107
);
95108
const onNotesChange = useCallback(
@@ -141,6 +154,13 @@ export default function CreatePage({
141154
renderToPortal
142155
outlined
143156
/>
157+
<RecurSelect
158+
label='Select recurrence'
159+
className={styles.field}
160+
onChange={onRecurChange}
161+
value={meeting.time.recur}
162+
outlined
163+
/>
144164
<TextField
145165
outlined
146166
textarea

components/calendar/dialogs/edit/display-page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,15 @@ export default function DisplayPage({
3939
setError('');
4040
setChecked(false);
4141
setLoading(true);
42-
const [err] = await to(axios.delete(`/api/meetings/${meeting.id}`));
42+
const endpoint = `/api/meetings/${meeting.parentId || meeting.id}`;
43+
const [err] = await to(axios.delete(endpoint));
4344
if (err) {
4445
const e = (err as AxiosError<APIErrorJSON>).response?.data || err;
4546
setLoading(false);
4647
setError(e.message);
4748
} else {
4849
setChecked(true);
49-
setTimeout(() => removeMeeting(meeting.id), 1000);
50+
setTimeout(() => removeMeeting(meeting.id, true), 1000);
5051
}
5152
}, [setLoading, setChecked, removeMeeting, meeting.id]);
5253

components/calendar/dialogs/edit/edit-page.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import axios from 'axios';
44
import useTranslation from 'next-translate/useTranslation';
55

66
import Button from 'components/button';
7+
import RecurSelect from 'components/recur-select';
78
import SubjectSelect from 'components/subject-select';
89
import TimeSelect from 'components/time-select';
910
import { useNav } from 'components/dialog/context';
@@ -39,7 +40,8 @@ export default function EditPage({
3940
setChecked,
4041
}: EditPageProps): JSX.Element {
4142
const updateRemote = useCallback(async (updated: Meeting) => {
42-
const url = `/api/meetings/${updated.id}`;
43+
// TODO: The REST API URL that we use actually doesn't matter.
44+
const url = `/api/meetings/${updated.parentId || updated.id}`;
4345
const { data } = await axios.put<MeetingJSON>(url, updated.toJSON());
4446
return Meeting.fromJSON(data);
4547
}, []);
@@ -84,6 +86,15 @@ export default function EditPage({
8486
},
8587
[setMeeting]
8688
);
89+
const onRecurChange = useCallback(
90+
(recur: string) => {
91+
setMeeting((prev) => {
92+
const time = new Timeslot({ ...prev.time, recur });
93+
return new Meeting({ ...prev, time });
94+
});
95+
},
96+
[setMeeting]
97+
);
8798
const onNotesChange = useCallback(
8899
(evt: FormEvent<HTMLInputElement>) => {
89100
const notes = evt.currentTarget.value;
@@ -137,6 +148,13 @@ export default function EditPage({
137148
renderToPortal
138149
outlined
139150
/>
151+
<RecurSelect
152+
label='Recurrence'
153+
className={styles.field}
154+
onChange={onRecurChange}
155+
value={meeting.time.recur}
156+
outlined
157+
/>
140158
<TextField
141159
outlined
142160
textarea

components/calendar/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,17 @@ export default function Calendar({
8080
// Note: If we ever need to use the `hits` property, we'll have to update
8181
// this callback function to properly cache and reuse the previous value.
8282
const json = updated.map((m) => m.toJSON());
83-
await mutate(query.endpoint, { meetings: json }, false);
83+
await mutate(query.endpoint, { meetings: json }, hasBeenUpdated);
8484
},
8585
[query.endpoint, meetings]
8686
);
8787
const removeMeeting = useCallback(
88-
async (meetingId: string) => {
88+
async (meetingId: string, hasBeenDeleted = false) => {
8989
const idx = meetings.findIndex((m) => m.id === meetingId);
9090
if (idx < 0) return;
9191
const updated = [...meetings.slice(0, idx), ...meetings.slice(idx + 1)];
9292
const json = updated.map((m) => m.toJSON());
93-
await mutate(query.endpoint, { meetings: json }, false);
93+
await mutate(query.endpoint, { meetings: json }, hasBeenDeleted);
9494
},
9595
[query.endpoint, meetings]
9696
);

components/calendar/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function getMeeting(
99
width: number,
1010
reference: Date = new Date(0)
1111
): Meeting {
12-
const time = getTimeslot(height, position, '', width, reference);
12+
const time = getTimeslot(height, position, meeting.time, width, reference);
1313
return new Meeting({ ...meeting, time });
1414
}
1515

components/recur-select/index.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { FormEvent, useCallback, useMemo } from 'react';
2+
import { Select, SelectProps } from '@rmwc/select';
3+
4+
import { TCallback } from 'lib/model';
5+
6+
import styles from './recur-select.module.scss';
7+
8+
const rrules = {
9+
Daily: 'RRULE:FREQ=DAILY',
10+
Weekly: 'RRULE:FREQ=WEEKLY',
11+
Biweekly: 'RRULE:FREQ=WEEKLY;INTERVAL=2',
12+
Monthly: 'RRULE:FREQ=MONTHLY',
13+
};
14+
15+
function inverse(record: Record<string, string>): Record<string, string> {
16+
const inverted: Record<string, string> = {};
17+
Object.entries(record).forEach(([key, val]) => {
18+
inverted[val] = key;
19+
});
20+
return inverted;
21+
}
22+
23+
export type RecurSelectProps = {
24+
value: string;
25+
onChange: TCallback<string>;
26+
} & Omit<SelectProps, 'value' | 'onChange'>;
27+
28+
// TODO: Allow this to be rendered to portal by capturing clicks on the enhanced
29+
// menu surface. See: https://github.com/jamesmfriedman/rmwc/pull/723
30+
export default function RecurSelect({
31+
value,
32+
onChange,
33+
...props
34+
}: RecurSelectProps): JSX.Element {
35+
const onSelectChange = useCallback(
36+
(evt: FormEvent<HTMLSelectElement>) => {
37+
onChange(rrules[evt.currentTarget.value] || '');
38+
},
39+
[onChange]
40+
);
41+
const selectValue = useMemo(() => inverse(rrules)[value] || '', [value]);
42+
43+
return (
44+
<Select
45+
options={['Daily', 'Weekly', 'Biweekly', 'Monthly']}
46+
className={styles.select}
47+
onChange={onSelectChange}
48+
value={selectValue}
49+
enhanced
50+
{...props}
51+
/>
52+
);
53+
}

0 commit comments

Comments
 (0)