diff --git a/package.json b/package.json index 5587b6c..fb4a2c1 100644 --- a/package.json +++ b/package.json @@ -61,11 +61,13 @@ "react-datepicker": "^8.4.0", "react-day-picker": "^9.7.0", "react-dom": "^19.0.0", + "react-error-boundary": "^6.0.0", "react-hook-form": "^7.56.4", "sonner": "^2.0.5", "swagger-ui-react": "^5.24.1", "tailwind-merge": "^3.2.0", - "zod": "^3.25.60" + "zod": "^3.25.60", + "zod-validation-error": "^3.5.2" }, "devDependencies": { "@eslint/eslintrc": "3.3.1", diff --git a/src/app/api/user/[user]/calendar/validation.ts b/src/app/api/user/[user]/calendar/validation.ts index 996307f..1572793 100644 --- a/src/app/api/user/[user]/calendar/validation.ts +++ b/src/app/api/user/[user]/calendar/validation.ts @@ -19,18 +19,22 @@ export const BlockedSlotSchema = zod description: 'Blocked time slot in the user calendar', }); -export const OwnedBlockedSlotSchema = BlockedSlotSchema.extend({ - id: zod.string(), - reason: zod.string().nullish(), - is_recurring: zod.boolean().default(false), - recurrence_end_date: zod.date().nullish(), - rrule: zod.string().nullish(), - created_at: zod.date().nullish(), - updated_at: zod.date().nullish(), - type: zod.literal('blocked_owned'), -}).openapi('OwnedBlockedSlotSchema', { - description: 'Blocked slot owned by the user', -}); +export const OwnedBlockedSlotSchema = zod + .object({ + start_time: eventStartTimeSchema, + end_time: eventEndTimeSchema, + id: zod.string(), + reason: zod.string().nullish(), + is_recurring: zod.boolean().default(false), + recurrence_end_date: zod.date().nullish(), + rrule: zod.string().nullish(), + created_at: zod.date().nullish(), + updated_at: zod.date().nullish(), + type: zod.literal('blocked_owned'), + }) + .openapi('OwnedBlockedSlotSchema', { + description: 'Blocked slot owned by the user', + }); export const VisibleSlotSchema = EventSchema.omit({ organizer: true, diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx index bfd8651..71f326b 100644 --- a/src/components/calendar.tsx +++ b/src/components/calendar.tsx @@ -11,6 +11,12 @@ import { useGetApiUserUserCalendar } from '@/generated/api/user/user'; import { useRouter } from 'next/navigation'; import { usePatchApiEventEventID } from '@/generated/api/event/event'; import { useSession } from 'next-auth/react'; +import { UserCalendarSchemaItem } from '@/generated/api/meetup.schemas'; +import { QueryErrorResetBoundary } from '@tanstack/react-query'; +import { ErrorBoundary } from 'react-error-boundary'; +import { Button } from '@/components/ui/button'; +import { fromZodIssue } from 'zod-validation-error/v4'; +import type { $ZodIssue } from 'zod/v4/core'; moment.updateLocale('en', { week: { @@ -24,16 +30,49 @@ const DaDRBCalendar = withDragAndDrop< id: string; start: Date; end: Date; + type: UserCalendarSchemaItem['type']; }, { id: string; title: string; + type: UserCalendarSchemaItem['type']; } >(RBCalendar); const localizer = momentLocalizer(moment); -export default function Calendar({ userId }: { userId: string }) { +export default function Calendar({ userId }: { userId?: string }) { + return ( + + {({ reset }) => ( + ( +
+ There was an error! +

+ {typeof error === 'string' + ? error + : error.errors + .map((e: $ZodIssue) => fromZodIssue(e).toString()) + .join(', ')} +

+ +
+ )} + > + {userId ? ( + + ) : ( + + )} +
+ )} +
+ ); +} + +function CalendarWithUserEvents({ userId }: { userId: string }) { const sesstion = useSession(); const [currentView, setCurrentView] = React.useState< 'month' | 'week' | 'day' | 'agenda' | 'work_week' @@ -41,33 +80,52 @@ export default function Calendar({ userId }: { userId: string }) { const [currentDate, setCurrentDate] = React.useState(new Date()); const router = useRouter(); - const { data, refetch } = useGetApiUserUserCalendar(userId, { - start: moment(currentDate) - .startOf( - currentView === 'agenda' - ? 'month' - : currentView === 'work_week' - ? 'week' - : currentView, - ) - .toISOString(), - end: moment(currentDate) - .endOf( - currentView === 'agenda' - ? 'month' - : currentView === 'work_week' - ? 'week' - : currentView, - ) - .toISOString(), - }); + const { data, refetch, error, isError } = useGetApiUserUserCalendar( + userId, + { + start: moment(currentDate) + .startOf( + currentView === 'agenda' + ? 'month' + : currentView === 'work_week' + ? 'week' + : currentView, + ) + .toISOString(), + end: moment(currentDate) + .endOf( + currentView === 'agenda' + ? 'month' + : currentView === 'work_week' + ? 'week' + : currentView, + ) + .toISOString(), + }, + { + query: { + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: true, + }, + }, + ); - const { mutate: patchEvent } = usePatchApiEventEventID(); + if (isError) { + throw error.response?.data || 'Failed to fetch calendar data'; + } + + const { mutate: patchEvent } = usePatchApiEventEventID({ + mutation: { + throwOnError(error) { + throw error.response?.data || 'Failed to update event'; + }, + }, + }); return ( { @@ -102,6 +161,7 @@ export default function Calendar({ userId }: { userId: string }) { selectable={sesstion.data?.user?.id === userId} onEventDrop={(event) => { const { start, end, event: droppedEvent } = event; + if (droppedEvent.type === 'blocked_private') return; const startISO = new Date(start).toISOString(); const endISO = new Date(end).toISOString(); patchEvent( @@ -124,8 +184,13 @@ export default function Calendar({ userId }: { userId: string }) { }} onEventResize={(event) => { const { start, end, event: resizedEvent } = event; + if (resizedEvent.type === 'blocked_private') return; const startISO = new Date(start).toISOString(); const endISO = new Date(end).toISOString(); + if (startISO === endISO) { + console.warn('Start and end times are the same, skipping resize.'); + return; + } patchEvent( { eventID: resizedEvent.id, @@ -147,3 +212,27 @@ export default function Calendar({ userId }: { userId: string }) { /> ); } + +function CalendarWithoutUserEvents() { + const [currentView, setCurrentView] = React.useState< + 'month' | 'week' | 'day' | 'agenda' | 'work_week' + >('week'); + const [currentDate, setCurrentDate] = React.useState(new Date()); + + return ( + { + setCurrentDate(date); + }} + /> + ); +} diff --git a/src/components/custom-toolbar.css b/src/components/custom-toolbar.css index c55d47a..8f8d1f6 100644 --- a/src/components/custom-toolbar.css +++ b/src/components/custom-toolbar.css @@ -109,5 +109,5 @@ } .datepicker-box { - z-index: 9999; + z-index: 5; } diff --git a/yarn.lock b/yarn.lock index 234ff57..74ff1df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -156,7 +156,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.7": +"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.7": version: 7.27.6 resolution: "@babel/runtime@npm:7.27.6" checksum: 10c0/89726be83f356f511dcdb74d3ea4d873a5f0cf0017d4530cb53aa27380c01ca102d573eff8b8b77815e624b1f8c24e7f0311834ad4fb632c90a770fda00bd4c8 @@ -7730,6 +7730,7 @@ __metadata: react-datepicker: "npm:^8.4.0" react-day-picker: "npm:^9.7.0" react-dom: "npm:^19.0.0" + react-error-boundary: "npm:^6.0.0" react-hook-form: "npm:^7.56.4" sonner: "npm:^2.0.5" swagger-ui-react: "npm:^5.24.1" @@ -7740,6 +7741,7 @@ __metadata: tw-animate-css: "npm:1.3.4" typescript: "npm:^5.8.3" zod: "npm:^3.25.60" + zod-validation-error: "npm:^3.5.2" languageName: unknown linkType: soft @@ -9004,6 +9006,17 @@ __metadata: languageName: node linkType: hard +"react-error-boundary@npm:^6.0.0": + version: 6.0.0 + resolution: "react-error-boundary@npm:6.0.0" + dependencies: + "@babel/runtime": "npm:^7.12.5" + peerDependencies: + react: ">=16.13.1" + checksum: 10c0/1914d600dee95a14f14af4afe9867b0d35c26c4f7826d23208800ba2a99728659029aad60a6ef95e13430b4d79c2c4c9b3585f50bf508450478760d2e4e732d8 + languageName: node + linkType: hard + "react-hook-form@npm:^7.56.4": version: 7.58.1 resolution: "react-hook-form@npm:7.58.1" @@ -11274,6 +11287,15 @@ __metadata: languageName: node linkType: hard +"zod-validation-error@npm:^3.5.2": + version: 3.5.2 + resolution: "zod-validation-error@npm:3.5.2" + peerDependencies: + zod: ^3.25.0 + checksum: 10c0/da50926ec91c7ad2880bacc5010a53c42de58f73f7c4629baad8132695c4daf74dd68620787198da81aca85134471182ed4c566b5fc9bc5349aefd8540946d57 + languageName: node + linkType: hard + "zod@npm:^3.25.60": version: 3.25.67 resolution: "zod@npm:3.25.67"