+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {event.title || 'Untitled Event'}
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{' '}
-
- {event.participants?.map((user) => (
-
- ))}
-
-
+
+
+
+
+
+
-
-
-
- {session.data?.user?.id === event.organizer.id ? (
-
- ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
-
- {session.data?.user?.id === event.organizer.id ? (
-
- ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{' '}
+
+ {event.participants?.map((user) => (
+
+ ))}
+
+
+
+
+
+
+ {session.data?.user?.id === event.organizer.id ? (
+
+ ) : 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 (
-