Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
},
"dependencies": {
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.76.1",
"@tanstack/react-router": "^1.109.2",
"@vanilla-extract/css": "^1.17.0",
"@vanilla-extract/dynamic": "^2.1.2",
Expand Down
20 changes: 20 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion frontend/src/features/my-calendar/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { request } from '@/utils/fetch';

import type { DateRangeParams, PersonalEventRequest, PersonalEventResponse } from '../model';
import type {
DateRangeParams,
PersonalEventRequest,
PersonalEventResponse,
PersonalEventSyncResponse,
} from '../model';

export const personalEventApi = {
getPersonalEvent: async (
Expand All @@ -26,4 +31,8 @@ export const personalEventApi = {
params: { syncWithGoogleCalendar: syncWithGoogleCalendar.toString() },
});
},
syncPersonalEvent: async (): Promise<PersonalEventSyncResponse> => {
const response = await request.get('/api/v1/personal-event/sync');
return response;
},
};
90 changes: 89 additions & 1 deletion frontend/src/features/my-calendar/api/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { QueryClient } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';

import type { PersonalEventRequest } from '../model';
import { addNoti } from '@/store/global/notification';
import { EndolphinDate } from '@/utils/endolphin-date';
import { HTTPError } from '@/utils/error';

import type { GooglePersonalEventDTO, PersonalEventRequest, PersonalEventResponse } from '../model';
import { personalEventApi } from '.';
import { personalEventKeys } from './keys';

Expand Down Expand Up @@ -52,4 +58,86 @@ export const usePersonalEventDeleteMutation = () => {
});

return { mutate };
};

export const usePersonalEventSyncMutation = (sunday: EndolphinDate, saturday: EndolphinDate) => {
const queryClient = useQueryClient();

const { mutate, isPending } = useMutation({
mutationFn: personalEventApi.syncPersonalEvent,
onSuccess: ({ events, type }) => {
if (type === 'sync') {
events?.forEach((event) => {
const start = new EndolphinDate(event.startDateTime);
const end = new EndolphinDate(event.endDateTime);
if (event.status === 'cancelled') removeEvent({ queryClient, id: event.googleEventId });
if (event.status === 'confirmed' && end >= sunday && start <= saturday) {
updateEvent({
queryClient,
event,
startDate: sunday.formatDateToBarString(),
endDate: saturday.formatDateToBarString(),
});
}
});
}
mutate();
},
onError: (error) => {
if (error instanceof HTTPError) {
if (error.isTimeoutError()) {
mutate();
return;
}
addNoti({ type: 'error', title: error.message });
}
setTimeout(mutate, 1000);
},
});

useEffect(() => {
if (!isPending) mutate();
}, []);
};

const updateEvent = (
{ queryClient, event, startDate, endDate }:
{
queryClient: QueryClient;
event: GooglePersonalEventDTO;
startDate: string;
endDate: string;
},
) => {
queryClient.setQueryData(personalEventKeys.detail({ startDate, endDate }),
(oldData: PersonalEventResponse[]) => {
const isNewEvent
= !oldData || oldData.every((oldEvent) => oldEvent.googleEventId !== event.googleEventId);
if (isNewEvent) return [...oldData, event];
return oldData.map((oldEvent) => {
if (oldEvent.googleEventId === event.googleEventId) {
return {
...oldEvent,
startDateTime: event.startDateTime,
endDateTime: event.endDateTime,
title: event.title,
};
}
return oldEvent;
});
});
};

const removeEvent = (
{ queryClient, id }:
{ queryClient: QueryClient; id: string },
) => {
const allEvents = queryClient.getQueriesData<PersonalEventResponse[]>({
queryKey: personalEventKeys.all,
});
allEvents.forEach(([key, oldData]) => {
if (!oldData) return;
const newData = oldData.filter(event => event.googleEventId !== id);
if (newData.length !== oldData.length) queryClient.setQueryData(key, newData);
});
};
16 changes: 16 additions & 0 deletions frontend/src/features/my-calendar/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,25 @@ const PersonalEventRequest = PersonalEventDTO.omit(
{ id: true, googleEventId: true, calendarId: true },
);

const GooglePersonalEventDTO = PersonalEventDTO.pick({
googleEventId: true,
title: true,
startDateTime: true,
endDateTime: true,
}).extend({
status: z.enum(['tentative', 'confirmed', 'cancelled']),
});

const PersonalEventSyncResponse = z.object({
events: z.array(GooglePersonalEventDTO).nullable(),
type: z.enum(['timeout', 'replaced', 'sync']),
});

export type PersonalEventDTO = z.infer<typeof PersonalEventDTO>;
export type GooglePersonalEventDTO = z.infer<typeof GooglePersonalEventDTO>;
export type PersonalEventResponse = z.infer<typeof PersonalEventResponse>;
export type PersonalEventRequest = z.infer<typeof PersonalEventRequest>;
export type PersonalEventSyncResponse = z.infer<typeof PersonalEventSyncResponse>;

export interface DateRangeParams {
startDate: string;
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/features/my-calendar/ui/MyCalendar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { useSharedCalendarContext } from '@/components/Calendar/context/SharedCa
import { formatDateToWeekRange, isAllday } from '@/utils/date';
import { formatDateToBarString } from '@/utils/date/format';
import { calcSizeByDate } from '@/utils/date/position';
import { EndolphinDate } from '@/utils/endolphin-date';

import { usePersonalEventSyncMutation } from '../../api/mutations';
import { usePersonalEventsQuery } from '../../api/queries';
import type { PersonalEventResponse } from '../../model';
import { CalendarCard } from '../CalendarCard';
Expand Down Expand Up @@ -51,6 +53,8 @@ export const MyCalendar = () => {
endDate: formatDateToBarString(endDate),
});

usePersonalEventSyncMutation(new EndolphinDate(startDate), new EndolphinDate(endDate));

return (
<Calendar {...calendar} className={calendarStyle}>
<Calendar.Core />
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { QueryClient } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import {
createRootRouteWithContext,
HeadContent,
Expand Down Expand Up @@ -32,6 +33,7 @@ export const Route = createRootRouteWithContext<QueryClientContext>()({
<Outlet />
<Footer />
<TanStackRouterDevtools />
<ReactQueryDevtools initialIsOpen={false} />
</>
),
notFoundComponent: () => (
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/utils/date/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ export const formatDateToWeekRange = (date: Date): {
const selected = new Date(date);
const firstDateOfWeek = new Date(selected);
firstDateOfWeek.setDate(firstDateOfWeek.getDate() - selected.getDay());
firstDateOfWeek.setHours(0, 0, 0, 0);

const lastDateOfWeek = new Date(firstDateOfWeek);
lastDateOfWeek.setDate(firstDateOfWeek.getDate() + 6);
lastDateOfWeek.setHours(23, 59, 59, 999);

return { startDate: firstDateOfWeek, endDate: lastDateOfWeek };
};
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/utils/endolphin-date/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default class EndolphinDate {

#formateToDate (input: InputDate): Date | null {
if (!input) return null;
if (input instanceof Date) return this.#date;
if (input instanceof Date) return input;
if (typeof input === 'number') {
return new Date(input);
}
Expand Down Expand Up @@ -59,4 +59,17 @@ export default class EndolphinDate {
getDate () {
return this.#date;
}

valueOf () {
return this.#date?.getTime() ?? 0;
}

toString () {
return format.formatDateToBarString(this.#date);
}

[Symbol.toPrimitive] (hint: string) {
if (hint === 'number') return this.valueOf();
return this.toString();
}
}
1 change: 1 addition & 0 deletions frontend/src/utils/error/HTTPError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class HTTPError extends Error {
}

isUnAuthorizedError = () => this.#status === 401;
isTimeoutError = () => this.#status === 408;
isForbiddenError = () => this.#status === 403;
isTooManyRequestsError = () => this.#status === 429;

Expand Down