From 6231d6cd45037f7611374f9c197d06391741488f Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Sat, 28 Jun 2025 17:11:56 +0200 Subject: [PATCH 1/3] feat(event): display user calendars on event creation and edit pages --- package.json | 1 + src/app/(main)/events/[eventID]/page.tsx | 308 +++++++++-------- src/app/(main)/events/edit/[eventID]/page.tsx | 18 +- .../api/{user/[user] => }/calendar/route.ts | 65 +++- .../api/{user/[user] => }/calendar/swagger.ts | 9 +- .../{user/[user] => }/calendar/validation.ts | 9 +- src/app/api/search/user/route.ts | 2 +- src/components/calendar.tsx | 199 ++++++++++- src/components/custom-toolbar.css | 5 + src/components/custom-ui/app-sidebar.tsx | 2 +- src/components/forms/event-form.tsx | 317 ++++++++++-------- yarn.lock | 17 + 12 files changed, 615 insertions(+), 337 deletions(-) rename src/app/api/{user/[user] => }/calendar/route.ts (73%) rename src/app/api/{user/[user] => }/calendar/swagger.ts (77%) rename src/app/api/{user/[user] => }/calendar/validation.ts (91%) diff --git a/package.json b/package.json index 7ec859d..e5e8c9e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@hookform/resolvers": "^5.0.1", + "@marko19907/string-to-color": "^1.0.0", "@prisma/client": "^6.9.0", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-collapsible": "^1.1.11", diff --git a/src/app/(main)/events/[eventID]/page.tsx b/src/app/(main)/events/[eventID]/page.tsx index bfc390d..81b98cf 100644 --- a/src/app/(main)/events/[eventID]/page.tsx +++ b/src/app/(main)/events/[eventID]/page.tsx @@ -70,169 +70,167 @@ export default function ShowEvent() { }; return ( -
- - + + - -
-
-
-
- -
-
-

- {event.title || 'Untitled Event'} -

-
-
+ +
+
+
+
+
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
+
+

+ {event.title || 'Untitled Event'} +

-
-
-
-
- - -
-
-
- - -
-
-
- {' '} -
- {event.participants?.map((user) => ( - - ))} -
-
+
+
+
+
+ +
- -
-
- {session.data?.user?.id === event.organizer.id ? ( - - - - - - - Delete Event - - Are you sure you want to delete the event “ - {event.title}”? This action cannot be undone. - - - - - - - - - ) : null} +
+ + +
+
+ + +
+
+
+ +
-
- {session.data?.user?.id === event.organizer.id ? ( - - ) : null} +
+ +
+
+
+
+
+ + +
+
+
+ + +
+
+
+ {' '} +
+ {event.participants?.map((user) => ( + + ))} +
+
+
+ +
+
+ {session.data?.user?.id === event.organizer.id ? ( + + + + + + + Delete Event + + Are you sure you want to delete the event “ + {event.title}”? This action cannot be undone. + + + + + + + + + ) : null} +
+
+ {session.data?.user?.id === event.organizer.id ? ( + + ) : null} +
+
- - -
+
+ + ); } diff --git a/src/app/(main)/events/edit/[eventID]/page.tsx b/src/app/(main)/events/edit/[eventID]/page.tsx index 42c6e8b..b099f10 100644 --- a/src/app/(main)/events/edit/[eventID]/page.tsx +++ b/src/app/(main)/events/edit/[eventID]/page.tsx @@ -9,16 +9,14 @@ export default async function Page({ }) { const eventID = (await params).eventID; return ( -
- - + + - - - - - - -
+ + + + + + ); } diff --git a/src/app/api/user/[user]/calendar/route.ts b/src/app/api/calendar/route.ts similarity index 73% rename from src/app/api/user/[user]/calendar/route.ts rename to src/app/api/calendar/route.ts index 62142e9..fa6dd47 100644 --- a/src/app/api/user/[user]/calendar/route.ts +++ b/src/app/api/calendar/route.ts @@ -15,7 +15,7 @@ import { } from '@/app/api/validation'; import { z } from 'zod/v4'; -export const GET = auth(async function GET(req, { params }) { +export const GET = auth(async function GET(req) { const authCheck = userAuthenticated(req); if (!authCheck.continue) return returnZodTypeCheckedResponse( @@ -24,7 +24,22 @@ export const GET = auth(async function GET(req, { params }) { authCheck.metadata, ); - const dataRaw = Object.fromEntries(new URL(req.url).searchParams); + const dataRaw: Record = {}; + for (const [key, value] of req.nextUrl.searchParams.entries()) { + if (key.endsWith('[]')) { + const cleanKey = key.slice(0, -2); + if (!dataRaw[cleanKey]) { + dataRaw[cleanKey] = []; + } + if (Array.isArray(dataRaw[cleanKey])) { + (dataRaw[cleanKey] as string[]).push(value); + } else { + dataRaw[cleanKey] = [dataRaw[cleanKey] as string, value]; + } + } else { + dataRaw[key] = value; + } + } const data = await userCalendarQuerySchema.safeParseAsync(dataRaw); if (!data.success) return returnZodTypeCheckedResponse( @@ -36,15 +51,15 @@ export const GET = auth(async function GET(req, { params }) { }, { status: 400 }, ); - const { end, start } = data.data; + const { end, start, userIds } = data.data; const requestUserId = authCheck.user.id; - const requestedUserId = (await params).user; - - const requestedUser = await prisma.user.findFirst({ + const requestedUser = await prisma.user.findMany({ where: { - id: requestedUserId, + id: { + in: userIds, + }, }, select: { meetingParts: { @@ -64,6 +79,7 @@ export const GET = auth(async function GET(req, { params }) { }, }, select: { + user_id: true, meeting: { select: { id: true, @@ -136,6 +152,7 @@ export const GET = auth(async function GET(req, { params }) { start_time: 'asc', }, select: { + user_id: true, id: true, reason: true, start_time: true, @@ -153,46 +170,64 @@ export const GET = auth(async function GET(req, { params }) { if (!requestedUser) return returnZodTypeCheckedResponse( ErrorResponseSchema, - { success: false, message: 'User not found' }, + { success: false, message: 'User/s not found' }, { status: 404 }, ); const calendar: z.input = []; - for (const event of requestedUser.meetingParts) { + for (const event of requestedUser.map((r) => r.meetingParts).flat()) { if ( event.meeting.participants.some((p) => p.user.id === requestUserId) || event.meeting.organizer_id === requestUserId ) { - calendar.push({ ...event.meeting, type: 'event' }); + calendar.push({ + ...event.meeting, + type: 'event', + users: event.meeting.participants + .map((p) => p.user.id) + .filter((id) => userIds.includes(id)), + }); } else { calendar.push({ id: event.meeting.id, start_time: event.meeting.start_time, end_time: event.meeting.end_time, type: 'blocked_private', + users: event.meeting.participants + .map((p) => p.user.id) + .filter((id) => userIds.includes(id)), }); } } - for (const event of requestedUser.meetingsOrg) { + for (const event of requestedUser.map((r) => r.meetingsOrg).flat()) { if ( event.participants.some((p) => p.user.id === requestUserId) || event.organizer_id === requestUserId ) { - calendar.push({ ...event, type: 'event' }); + calendar.push({ + ...event, + type: 'event', + users: event.participants + .map((p) => p.user.id) + .filter((id) => userIds.includes(id)), + }); } else { calendar.push({ id: event.id, start_time: event.start_time, end_time: event.end_time, type: 'blocked_private', + users: event.participants + .map((p) => p.user.id) + .filter((id) => userIds.includes(id)), }); } } - for (const slot of requestedUser.blockedSlots) { - if (requestUserId === requestedUserId) { + for (const slot of requestedUser.map((r) => r.blockedSlots).flat()) { + if (requestUserId === userIds[0] && userIds.length === 1) { calendar.push({ start_time: slot.start_time, end_time: slot.end_time, @@ -204,6 +239,7 @@ export const GET = auth(async function GET(req, { params }) { created_at: slot.created_at, updated_at: slot.updated_at, type: 'blocked_owned', + users: [requestUserId], }); } else { calendar.push({ @@ -211,6 +247,7 @@ export const GET = auth(async function GET(req, { params }) { end_time: slot.end_time, id: slot.id, type: 'blocked_private', + users: [slot.user_id], }); } } diff --git a/src/app/api/user/[user]/calendar/swagger.ts b/src/app/api/calendar/swagger.ts similarity index 77% rename from src/app/api/user/[user]/calendar/swagger.ts rename to src/app/api/calendar/swagger.ts index fb48629..b4f5898 100644 --- a/src/app/api/user/[user]/calendar/swagger.ts +++ b/src/app/api/calendar/swagger.ts @@ -7,17 +7,12 @@ import { userNotFoundResponse, } from '@/lib/defaultApiResponses'; import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import zod from 'zod/v4'; -import { UserIdParamSchema } from '@/app/api/validation'; export default function registerSwaggerPaths(registry: OpenAPIRegistry) { registry.registerPath({ method: 'get', - path: '/api/user/{user}/calendar', + path: '/api/calendar', request: { - params: zod.object({ - user: UserIdParamSchema, - }), query: userCalendarQuerySchema, }, responses: { @@ -32,6 +27,6 @@ export default function registerSwaggerPaths(registry: OpenAPIRegistry) { ...notAuthenticatedResponse, ...userNotFoundResponse, }, - tags: ['User'], + tags: ['Calendar'], }); } diff --git a/src/app/api/user/[user]/calendar/validation.ts b/src/app/api/calendar/validation.ts similarity index 91% rename from src/app/api/user/[user]/calendar/validation.ts rename to src/app/api/calendar/validation.ts index 1572793..5bf45a6 100644 --- a/src/app/api/user/[user]/calendar/validation.ts +++ b/src/app/api/calendar/validation.ts @@ -14,6 +14,8 @@ export const BlockedSlotSchema = zod end_time: eventEndTimeSchema, type: zod.literal('blocked_private'), id: zod.string(), + users: zod.string().array(), + user_id: zod.string().optional(), }) .openapi('BlockedSlotSchema', { description: 'Blocked time slot in the user calendar', @@ -31,17 +33,21 @@ export const OwnedBlockedSlotSchema = zod created_at: zod.date().nullish(), updated_at: zod.date().nullish(), type: zod.literal('blocked_owned'), + users: zod.string().array(), + user_id: zod.string().optional(), }) .openapi('OwnedBlockedSlotSchema', { description: 'Blocked slot owned by the user', }); export const VisibleSlotSchema = EventSchema.omit({ - organizer: true, participants: true, + organizer: true, }) .extend({ type: zod.literal('event'), + users: zod.string().array(), + user_id: zod.string().optional(), }) .openapi('VisibleSlotSchema', { description: 'Visible time slot in the user calendar', @@ -86,6 +92,7 @@ export const userCalendarQuerySchema = zod ); return endOfWeek; }), + userIds: zod.string().array(), }) .openapi('UserCalendarQuerySchema', { description: 'Query parameters for filtering the user calendar', diff --git a/src/app/api/search/user/route.ts b/src/app/api/search/user/route.ts index a8b6414..0bcb6cf 100644 --- a/src/app/api/search/user/route.ts +++ b/src/app/api/search/user/route.ts @@ -19,7 +19,7 @@ export const GET = auth(async function GET(req) { authCheck.metadata, ); - const dataRaw = Object.fromEntries(new URL(req.url).searchParams); + const dataRaw = Object.fromEntries(req.nextUrl.searchParams); const data = await searchUserSchema.safeParseAsync(dataRaw); if (!data.success) return returnZodTypeCheckedResponse( diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx index a8d6005..ba9e730 100644 --- a/src/components/calendar.tsx +++ b/src/components/calendar.tsx @@ -7,7 +7,6 @@ import '@/components/react-big-calendar.css'; import 'react-big-calendar/lib/addons/dragAndDrop/styles.css'; import CustomToolbar from '@/components/custom-toolbar'; import React from 'react'; -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'; @@ -17,6 +16,11 @@ 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'; +import { useGetApiCalendar } from '@/generated/api/calendar/calendar'; +//import { +// generateColor, +// generateSecondaryColor, +//} from '@marko19907/string-to-color'; moment.updateLocale('en', { week: { @@ -25,12 +29,31 @@ moment.updateLocale('en', { }, }); +function eventPropGetter() { + // event: { + // id: string; + // start: Date; + // end: Date; + // type: UserCalendarSchemaItem['type']; + // userId?: string; + // } + return { + // style: { + // backgroundColor: generateColor(event.userId || 'defaultColor', { + // saturation: 0.7, + // lightness: 0.5, + // }), + // }, + }; +} + const DaDRBCalendar = withDragAndDrop< { id: string; start: Date; end: Date; type: UserCalendarSchemaItem['type']; + userId?: string; }, { id: string; @@ -44,9 +67,20 @@ const localizer = momentLocalizer(moment); export default function Calendar({ userId, height, + additionalEvents = [], + className, }: { - userId?: string; + userId?: string | string[]; height: string; + additionalEvents?: { + id: string; + title: string; + start: Date; + end: Date; + type: UserCalendarSchemaItem['type']; + userId?: string; + }[]; + className?: string; }) { return ( @@ -67,10 +101,26 @@ export default function Calendar({
)} > - {userId ? ( - + {typeof userId === 'string' ? ( + + ) : Array.isArray(userId) && userId.length > 0 ? ( + ) : ( - + )} )} @@ -81,9 +131,20 @@ export default function Calendar({ function CalendarWithUserEvents({ userId, height, + additionalEvents, + className, }: { userId: string; height: string; + additionalEvents?: { + id: string; + title: string; + start: Date; + end: Date; + type: UserCalendarSchemaItem['type']; + userId?: string; + }[]; + className?: string; }) { const sesstion = useSession(); const [currentView, setCurrentView] = React.useState< @@ -92,9 +153,9 @@ function CalendarWithUserEvents({ const [currentDate, setCurrentDate] = React.useState(new Date()); const router = useRouter(); - const { data, refetch, error, isError } = useGetApiUserUserCalendar( - userId, + const { data, refetch, error, isError } = useGetApiCalendar( { + userIds: [userId, userId + '_blocked'], start: moment(currentDate) .startOf( currentView === 'agenda' @@ -137,6 +198,8 @@ function CalendarWithUserEvents({ return ( { setCurrentDate(date); }} - events={ - data?.data.calendar.map((event) => ({ + events={[ + ...(data?.data.calendar.map((event) => ({ id: event.id, title: event.type === 'event' ? event.title : 'Blocker', start: new Date(event.start_time), end: new Date(event.end_time), type: event.type, - })) ?? [] - } + userId: event.users[0], + })) ?? []), + ...(additionalEvents ?? []), + ]} onSelectEvent={(event) => { router.push(`/events/${event.id}`); }} @@ -228,7 +293,114 @@ function CalendarWithUserEvents({ ); } -function CalendarWithoutUserEvents({ height }: { height: string }) { +function CalendarWithMultiUserEvents({ + userIds, + height, + additionalEvents, + className, +}: { + userIds: string[]; + height: string; + additionalEvents?: { + id: string; + title: string; + start: Date; + end: Date; + type: UserCalendarSchemaItem['type']; + userId?: string; + }[]; + className?: string; +}) { + const [currentView, setCurrentView] = React.useState< + 'month' | 'week' | 'day' | 'agenda' | 'work_week' + >('week'); + const [currentDate, setCurrentDate] = React.useState(new Date()); + + const { data, error, isError } = useGetApiCalendar( + { + userIds: userIds, + 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, + }, + }, + ); + + if (isError) { + throw error.response?.data || 'Failed to fetch calendar data'; + } + + return ( + { + setCurrentDate(date); + }} + events={[ + ...(data?.data.calendar.map((event) => ({ + id: event.id, + title: event.type === 'event' ? event.title : 'Blocker', + start: new Date(event.start_time), + end: new Date(event.end_time), + type: event.type, + userId: event.users[0], + })) ?? []), + ...(additionalEvents ?? []), + ]} + /> + ); +} + +function CalendarWithoutUserEvents({ + height, + additionalEvents, + className, +}: { + height: string; + additionalEvents?: { + id: string; + title: string; + start: Date; + end: Date; + type: UserCalendarSchemaItem['type']; + userId?: string; + }[]; + className?: string; +}) { const [currentView, setCurrentView] = React.useState< 'month' | 'week' | 'day' | 'agenda' | 'work_week' >('week'); @@ -236,6 +408,8 @@ function CalendarWithoutUserEvents({ height }: { height: string }) { return ( { setCurrentDate(date); }} + events={additionalEvents} /> ); } diff --git a/src/components/custom-toolbar.css b/src/components/custom-toolbar.css index 37a2a4d..1230384 100644 --- a/src/components/custom-toolbar.css +++ b/src/components/custom-toolbar.css @@ -32,6 +32,11 @@ align-items: center; } +.custom-toolbar .navigation-controls .handleWeek { + display: grid; + grid-template-columns: 1fr 1fr; +} + .custom-toolbar .navigation-controls button { padding: 8px 12px; color: #ffffff; diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx index f823970..4188141 100644 --- a/src/components/custom-ui/app-sidebar.tsx +++ b/src/components/custom-ui/app-sidebar.tsx @@ -75,7 +75,7 @@ export function AppSidebar() { className='group-data-[collapsible=]:hidden group-data-[mobile=true]/mobile:hidden' > - + diff --git a/src/components/forms/event-form.tsx b/src/components/forms/event-form.tsx index a445080..00322cc 100644 --- a/src/components/forms/event-form.tsx +++ b/src/components/forms/event-form.tsx @@ -21,6 +21,16 @@ import { useSearchParams } from 'next/navigation'; import zod from 'zod/v4'; import { PublicUserSchema } from '@/app/api/user/validation'; +import Calendar from '@/components/calendar'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../ui/dialog'; type User = zod.output; @@ -68,6 +78,8 @@ const EventForm: React.FC = (props) => { const [location, setLocation] = React.useState(''); const [description, setDescription] = React.useState(''); + const [calendarOpen, setCalendarOpen] = React.useState(false); + // Update state when event data loads React.useEffect(() => { if (props.type === 'edit' && event) { @@ -194,149 +206,182 @@ const EventForm: React.FC = (props) => { return
Error loading event.
; return ( -
-
-
-
- -
-
- setTitle(e.target.value)} - /> -
-
-
-
-
- -
-
- -
-
- setLocation(e.target.value)} - /> -
-
-
- - + <> + + +
+
+
+ +
+
+ setTitle(e.target.value)} + /> +
+
-
- -

- {updatedAtDisplay} -

-
-
-
-
-
-
-
- - +
+
+ +
+
+ +
+
+ setLocation(e.target.value)} + /> +
+
+
+ + +
+
+ +

+ {updatedAtDisplay} +

+
-
- setDescription(e.target.value)} - > -
-
-
- - { - setSelectedParticipants((current) => - current.find((u) => u.id === user.id) - ? current - : [...current, user], - ); - }} - removeUserAction={(user) => { - setSelectedParticipants((current) => - current.filter((u) => u.id !== user.id), - ); - }} - /> -
- {selectedParticipants.map((user) => ( - +
+
+
+ + +
+
+
+ setDescription(e.target.value)} + > +
+
+
+ + { + setSelectedParticipants((current) => + current.find((u) => u.id === user.id) + ? current + : [...current, user], + ); + }} + removeUserAction={(user) => { + setSelectedParticipants((current) => + current.filter((u) => u.id !== user.id), + ); + }} /> - ))} + + + +
+ {selectedParticipants.map((user) => ( + + ))} +
+
-
-
-
-
- +
+
+ +
+
+ +
+
+ {isSuccess &&

Event created!

} + {error &&

Error: {error.message}

}
-
- -
-
- {isSuccess &&

Event created!

} - {error &&

Error: {error.message}

} -
- + + + + Calendar + + Calendar for selected participants + + + + u.id)} + additionalEvents={[ + { + id: 'temp-event', + title: title || 'New Event', + start: startDate ? new Date(startDate) : new Date(), + end: endDate ? new Date(endDate) : new Date(), + type: 'event', + userId: 'create-event', + }, + ]} + height='600px' + /> + + + + ); }; diff --git a/yarn.lock b/yarn.lock index 816d896..b241026 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1013,6 +1013,15 @@ __metadata: languageName: node linkType: hard +"@marko19907/string-to-color@npm:^1.0.0": + version: 1.0.0 + resolution: "@marko19907/string-to-color@npm:1.0.0" + dependencies: + esm-seedrandom: "npm:^3.0.5" + checksum: 10c0/fd297937fb5a8a0c3372bc614f9a4e21842e3392ef54e0d323f0fe730ec5b5a441f921e63b3b151bd39ca64a120ddfe905d1a67ca62ed637ade69c96c55fe641 + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^0.2.11": version: 0.2.11 resolution: "@napi-rs/wasm-runtime@npm:0.2.11" @@ -5717,6 +5726,13 @@ __metadata: languageName: node linkType: hard +"esm-seedrandom@npm:^3.0.5": + version: 3.0.5 + resolution: "esm-seedrandom@npm:3.0.5" + checksum: 10c0/6fe5a33f31bce0e733814df884cdfc27812e4b04ce973f7cf008b7b8500ecab6706f54591841bd12dd3bdd9bbb153abf04fb8efd4934a5b0ab273bff114cdb3b + languageName: node + linkType: hard + "espree@npm:^10.0.1, espree@npm:^10.4.0": version: 10.4.0 resolution: "espree@npm:10.4.0" @@ -7683,6 +7699,7 @@ __metadata: "@fortawesome/free-solid-svg-icons": "npm:^6.7.2" "@fortawesome/react-fontawesome": "npm:^0.2.2" "@hookform/resolvers": "npm:^5.0.1" + "@marko19907/string-to-color": "npm:^1.0.0" "@prisma/client": "npm:^6.9.0" "@radix-ui/react-avatar": "npm:^1.1.10" "@radix-ui/react-collapsible": "npm:^1.1.11" From 1cb2019298795d513b520b14e558c4ad8907959c Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Sun, 29 Jun 2025 18:32:32 +0200 Subject: [PATCH 2/3] feat(event): setting user status --- src/app/(main)/home/page.tsx | 2 +- src/components/calendar.tsx | 35 ++++++------ src/components/custom-ui/app-sidebar.tsx | 6 +-- src/components/custom-ui/event-list-entry.tsx | 54 +++++++++++++++++++ .../custom-ui/participant-list-entry.tsx | 2 + src/components/forms/event-form.tsx | 1 + 6 files changed, 77 insertions(+), 23 deletions(-) diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx index 38237f6..69e5be6 100644 --- a/src/app/(main)/home/page.tsx +++ b/src/app/(main)/home/page.tsx @@ -22,7 +22,7 @@ export default function Home() {
diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx index ba9e730..9d8e255 100644 --- a/src/components/calendar.tsx +++ b/src/components/calendar.tsx @@ -17,10 +17,6 @@ import { Button } from '@/components/ui/button'; import { fromZodIssue } from 'zod-validation-error/v4'; import type { $ZodIssue } from 'zod/v4/core'; import { useGetApiCalendar } from '@/generated/api/calendar/calendar'; -//import { -// generateColor, -// generateSecondaryColor, -//} from '@marko19907/string-to-color'; moment.updateLocale('en', { week: { @@ -29,21 +25,18 @@ moment.updateLocale('en', { }, }); -function eventPropGetter() { - // event: { - // id: string; - // start: Date; - // end: Date; - // type: UserCalendarSchemaItem['type']; - // userId?: string; - // } +function eventPropGetter(event: { + id: string; + start: Date; + end: Date; + type: UserCalendarSchemaItem['type']; + userId?: string; + colorOverride?: string; +}) { return { - // style: { - // backgroundColor: generateColor(event.userId || 'defaultColor', { - // saturation: 0.7, - // lightness: 0.5, - // }), - // }, + style: event.colorOverride + ? { backgroundColor: event.colorOverride } + : undefined, }; } @@ -79,6 +72,7 @@ export default function Calendar({ end: Date; type: UserCalendarSchemaItem['type']; userId?: string; + colorOverride?: string; }[]; className?: string; }) { @@ -143,6 +137,7 @@ function CalendarWithUserEvents({ end: Date; type: UserCalendarSchemaItem['type']; userId?: string; + colorOverride?: string; }[]; className?: string; }) { @@ -238,7 +233,7 @@ function CalendarWithUserEvents({ resourceTitleAccessor={(event) => event.title} startAccessor={(event) => event.start} endAccessor={(event) => event.end} - selectable={sesstion.data?.user?.id === userId} + selectable={sesstion.data?.user?.id === userId && !additionalEvents} onEventDrop={(event) => { const { start, end, event: droppedEvent } = event; if (droppedEvent.type === 'blocked_private') return; @@ -308,6 +303,7 @@ function CalendarWithMultiUserEvents({ end: Date; type: UserCalendarSchemaItem['type']; userId?: string; + colorOverride?: string; }[]; className?: string; }) { @@ -398,6 +394,7 @@ function CalendarWithoutUserEvents({ end: Date; type: UserCalendarSchemaItem['type']; userId?: string; + colorOverride?: string; }[]; className?: string; }) { diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx index 4188141..50e88c2 100644 --- a/src/components/custom-ui/app-sidebar.tsx +++ b/src/components/custom-ui/app-sidebar.tsx @@ -37,7 +37,7 @@ import { const items = [ { title: 'Calendar', - url: '#', + url: '/home', icon: CalendarDays, }, { @@ -52,7 +52,7 @@ const items = [ }, { title: 'Events', - url: '#', + url: '/events', icon: CalendarClock, }, ]; @@ -114,7 +114,7 @@ export function AppSidebar() { diff --git a/src/components/custom-ui/event-list-entry.tsx b/src/components/custom-ui/event-list-entry.tsx index 197649b..edc4a2f 100644 --- a/src/components/custom-ui/event-list-entry.tsx +++ b/src/components/custom-ui/event-list-entry.tsx @@ -1,9 +1,20 @@ +'use client'; + import { Card } from '@/components/ui/card'; import Logo from '@/components/misc/logo'; import { Label } from '@/components/ui/label'; import Link from 'next/link'; import zod from 'zod/v4'; import { EventSchema } from '@/app/api/event/validation'; +import { useSession } from 'next-auth/react'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../ui/select'; +import { usePatchApiEventEventIDParticipantUser } from '@/generated/api/event-participant/event-participant'; type EventListEntryProps = zod.output; @@ -13,7 +24,11 @@ export default function EventListEntry({ start_time, end_time, location, + participants, }: EventListEntryProps) { + const session = useSession(); + const updateAttendance = usePatchApiEventEventIDParticipantUser(); + const formatDate = (isoString?: string) => { if (!isoString) return '-'; return new Date(isoString).toLocaleDateString(); @@ -60,6 +75,45 @@ export default function EventListEntry({
)} + {participants && + participants.some( + (p) => p.user.id === session.data?.user?.id, + ) && ( +
+ +
+ )}
diff --git a/src/components/custom-ui/participant-list-entry.tsx b/src/components/custom-ui/participant-list-entry.tsx index 27827cf..2ec9c02 100644 --- a/src/components/custom-ui/participant-list-entry.tsx +++ b/src/components/custom-ui/participant-list-entry.tsx @@ -10,6 +10,7 @@ type ParticipantListEntryProps = zod.output; export default function ParticipantListEntry({ user, + status, }: ParticipantListEntryProps) { const { resolvedTheme } = useTheme(); const defaultImage = @@ -21,6 +22,7 @@ export default function ParticipantListEntry({
Avatar {user.name} + {status}
); } diff --git a/src/components/forms/event-form.tsx b/src/components/forms/event-form.tsx index 00322cc..2a2912c 100644 --- a/src/components/forms/event-form.tsx +++ b/src/components/forms/event-form.tsx @@ -374,6 +374,7 @@ const EventForm: React.FC = (props) => { end: endDate ? new Date(endDate) : new Date(), type: 'event', userId: 'create-event', + colorOverride: '#ff9800', }, ]} height='600px' From 9f0a5813bed06d471322ecf1fe42db7ddd179f9d Mon Sep 17 00:00:00 2001 From: Dominik Stahl Date: Sun, 29 Jun 2025 19:10:05 +0200 Subject: [PATCH 3/3] feat(calendar): hide declined events --- src/app/api/calendar/route.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/api/calendar/route.ts b/src/app/api/calendar/route.ts index fa6dd47..440bbd7 100644 --- a/src/app/api/calendar/route.ts +++ b/src/app/api/calendar/route.ts @@ -72,6 +72,9 @@ export const GET = auth(async function GET(req) { gte: start, }, }, + status: { + not: 'DECLINED', + }, }, orderBy: { meeting: {