feat(calendar): enhance calendar error handlick and loading
This commit is contained in:
parent
fd7be58541
commit
758afb36b9
5 changed files with 155 additions and 38 deletions
|
@ -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",
|
||||
|
|
|
@ -19,7 +19,10 @@ export const BlockedSlotSchema = zod
|
|||
description: 'Blocked time slot in the user calendar',
|
||||
});
|
||||
|
||||
export const OwnedBlockedSlotSchema = BlockedSlotSchema.extend({
|
||||
export const OwnedBlockedSlotSchema = zod
|
||||
.object({
|
||||
start_time: eventStartTimeSchema,
|
||||
end_time: eventEndTimeSchema,
|
||||
id: zod.string(),
|
||||
reason: zod.string().nullish(),
|
||||
is_recurring: zod.boolean().default(false),
|
||||
|
@ -28,7 +31,8 @@ export const OwnedBlockedSlotSchema = BlockedSlotSchema.extend({
|
|||
created_at: zod.date().nullish(),
|
||||
updated_at: zod.date().nullish(),
|
||||
type: zod.literal('blocked_owned'),
|
||||
}).openapi('OwnedBlockedSlotSchema', {
|
||||
})
|
||||
.openapi('OwnedBlockedSlotSchema', {
|
||||
description: 'Blocked slot owned by the user',
|
||||
});
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<QueryErrorResetBoundary>
|
||||
{({ reset }) => (
|
||||
<ErrorBoundary
|
||||
onReset={reset}
|
||||
fallbackRender={({ resetErrorBoundary, error }) => (
|
||||
<div className='flex flex-col items-center justify-center h-full'>
|
||||
There was an error!
|
||||
<p className='text-red-500'>
|
||||
{typeof error === 'string'
|
||||
? error
|
||||
: error.errors
|
||||
.map((e: $ZodIssue) => fromZodIssue(e).toString())
|
||||
.join(', ')}
|
||||
</p>
|
||||
<Button onClick={() => resetErrorBoundary()}>Try again</Button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{userId ? (
|
||||
<CalendarWithUserEvents userId={userId} />
|
||||
) : (
|
||||
<CalendarWithoutUserEvents />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</QueryErrorResetBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarWithUserEvents({ userId }: { userId: string }) {
|
||||
const sesstion = useSession();
|
||||
const [currentView, setCurrentView] = React.useState<
|
||||
'month' | 'week' | 'day' | 'agenda' | 'work_week'
|
||||
|
@ -41,7 +80,9 @@ export default function Calendar({ userId }: { userId: string }) {
|
|||
const [currentDate, setCurrentDate] = React.useState<Date>(new Date());
|
||||
const router = useRouter();
|
||||
|
||||
const { data, refetch } = useGetApiUserUserCalendar(userId, {
|
||||
const { data, refetch, error, isError } = useGetApiUserUserCalendar(
|
||||
userId,
|
||||
{
|
||||
start: moment(currentDate)
|
||||
.startOf(
|
||||
currentView === 'agenda'
|
||||
|
@ -60,14 +101,31 @@ export default function Calendar({ userId }: { userId: string }) {
|
|||
: 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 (
|
||||
<DaDRBCalendar
|
||||
localizer={localizer}
|
||||
style={{ height: 500 }}
|
||||
culture='de-DE'
|
||||
defaultView='week'
|
||||
components={{
|
||||
|
@ -85,6 +143,7 @@ export default function Calendar({ userId }: { userId: string }) {
|
|||
title: event.type === 'event' ? event.title : 'Blocker',
|
||||
start: new Date(event.start_time),
|
||||
end: new Date(event.end_time),
|
||||
type: event.type,
|
||||
})) ?? []
|
||||
}
|
||||
onSelectEvent={(event) => {
|
||||
|
@ -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<Date>(new Date());
|
||||
|
||||
return (
|
||||
<DaDRBCalendar
|
||||
localizer={localizer}
|
||||
culture='de-DE'
|
||||
defaultView='week'
|
||||
components={{
|
||||
toolbar: CustomToolbar,
|
||||
}}
|
||||
onView={setCurrentView}
|
||||
view={currentView}
|
||||
date={currentDate}
|
||||
onNavigate={(date) => {
|
||||
setCurrentDate(date);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -109,5 +109,5 @@
|
|||
}
|
||||
|
||||
.datepicker-box {
|
||||
z-index: 9999;
|
||||
z-index: 5;
|
||||
}
|
||||
|
|
24
yarn.lock
24
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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue