Compare commits
3 commits
ce27923118
...
a0965e1b8d
Author | SHA1 | Date | |
---|---|---|---|
a0965e1b8d | |||
3d07ca077f | |||
016b4371c2 |
19 changed files with 1055 additions and 42 deletions
10
src/app/(main)/blocker/[slotId]/page.tsx
Normal file
10
src/app/(main)/blocker/[slotId]/page.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import BlockedSlotForm from '@/components/forms/blocked-slot-form';
|
||||||
|
|
||||||
|
export default async function NewBlockedSlotPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ slotId?: string }>;
|
||||||
|
}) {
|
||||||
|
const resolvedParams = await params;
|
||||||
|
return <BlockedSlotForm existingBlockedSlotId={resolvedParams.slotId} />;
|
||||||
|
}
|
5
src/app/(main)/blocker/new/page.tsx
Normal file
5
src/app/(main)/blocker/new/page.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import BlockedSlotForm from '@/components/forms/blocked-slot-form';
|
||||||
|
|
||||||
|
export default function NewBlockedSlotPage() {
|
||||||
|
return <BlockedSlotForm />;
|
||||||
|
}
|
56
src/app/(main)/blocker/page.tsx
Normal file
56
src/app/(main)/blocker/page.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { RedirectButton } from '@/components/buttons/redirect-button';
|
||||||
|
import BlockedSlotListEntry from '@/components/custom-ui/blocked-slot-list-entry';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { useGetApiBlockedSlots } from '@/generated/api/blocked-slots/blocked-slots';
|
||||||
|
|
||||||
|
export default function BlockedSlots() {
|
||||||
|
const { data: blockedSlotsData, isLoading, error } = useGetApiBlockedSlots();
|
||||||
|
|
||||||
|
if (isLoading) return <div className='text-center mt-10'>Loading...</div>;
|
||||||
|
if (error)
|
||||||
|
return (
|
||||||
|
<div className='text-center mt-10 text-red-500'>
|
||||||
|
Error loading blocked slots
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const blockedSlots = blockedSlotsData?.data?.blocked_slots || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative h-full flex flex-col items-center'>
|
||||||
|
{/* Heading */}
|
||||||
|
<h1 className='text-3xl font-bold mt-8 mb-4 text-center z-10'>
|
||||||
|
My Blockers
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Scrollable blocked slot list */}
|
||||||
|
<div className='w-full flex justify-center overflow-hidden'>
|
||||||
|
<div className='grid gap-8 w-[max(90%, 500px)] p-6 overflow-y-auto'>
|
||||||
|
{blockedSlots.length > 0 ? (
|
||||||
|
blockedSlots.map((slot) => (
|
||||||
|
<BlockedSlotListEntry
|
||||||
|
key={slot.id}
|
||||||
|
{...slot}
|
||||||
|
updated_at={new Date(slot.updated_at)}
|
||||||
|
created_at={new Date(slot.created_at)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-1 flex-col items-center justify-center min-h-[300px]'>
|
||||||
|
<Label size='large' className='justify-center text-center'>
|
||||||
|
You don't have any blockers right now
|
||||||
|
</Label>
|
||||||
|
<RedirectButton
|
||||||
|
redirectUrl='/blocker/new'
|
||||||
|
buttonText='Create New Blocker'
|
||||||
|
className='mt-4'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -155,7 +155,11 @@ export default function ShowEvent() {
|
||||||
</Label>{' '}
|
</Label>{' '}
|
||||||
<div className='grid grid-cols-1 mt-3 sm:max-h-60 sm:grid-cols-2 sm:overflow-y-auto sm:mb-0'>
|
<div className='grid grid-cols-1 mt-3 sm:max-h-60 sm:grid-cols-2 sm:overflow-y-auto sm:mb-0'>
|
||||||
{event.participants?.map((user) => (
|
{event.participants?.map((user) => (
|
||||||
<ParticipantListEntry key={user.user.id} {...user} />
|
<ParticipantListEntry
|
||||||
|
key={user.user.id}
|
||||||
|
{...user}
|
||||||
|
eventID={event.id}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
165
src/app/api/blocked_slots/[slotID]/route.ts
Normal file
165
src/app/api/blocked_slots/[slotID]/route.ts
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import { auth } from '@/auth';
|
||||||
|
import { prisma } from '@/prisma';
|
||||||
|
import {
|
||||||
|
returnZodTypeCheckedResponse,
|
||||||
|
userAuthenticated,
|
||||||
|
} from '@/lib/apiHelpers';
|
||||||
|
import {
|
||||||
|
updateBlockedSlotSchema,
|
||||||
|
BlockedSlotResponseSchema,
|
||||||
|
} from '@/app/api/blocked_slots/validation';
|
||||||
|
import {
|
||||||
|
ErrorResponseSchema,
|
||||||
|
SuccessResponseSchema,
|
||||||
|
ZodErrorResponseSchema,
|
||||||
|
} from '@/app/api/validation';
|
||||||
|
|
||||||
|
export const GET = auth(async function GET(req, { params }) {
|
||||||
|
const authCheck = userAuthenticated(req);
|
||||||
|
if (!authCheck.continue)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
authCheck.response,
|
||||||
|
authCheck.metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
const slotID = (await params).slotID;
|
||||||
|
|
||||||
|
const blockedSlot = await prisma.blockedSlot.findUnique({
|
||||||
|
where: {
|
||||||
|
id: slotID,
|
||||||
|
user_id: authCheck.user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
start_time: true,
|
||||||
|
end_time: true,
|
||||||
|
reason: true,
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
is_recurring: true,
|
||||||
|
recurrence_end_date: true,
|
||||||
|
rrule: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!blockedSlot) {
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Blocked slot not found or not owned by user',
|
||||||
|
},
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
BlockedSlotResponseSchema,
|
||||||
|
{
|
||||||
|
blocked_slot: blockedSlot,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PATCH = auth(async function PATCH(req, { params }) {
|
||||||
|
const authCheck = userAuthenticated(req);
|
||||||
|
if (!authCheck.continue)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
authCheck.response,
|
||||||
|
authCheck.metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
const slotID = (await params).slotID;
|
||||||
|
|
||||||
|
const dataRaw = await req.json();
|
||||||
|
const data = await updateBlockedSlotSchema.safeParseAsync(dataRaw);
|
||||||
|
if (!data.success)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ZodErrorResponseSchema,
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid request data',
|
||||||
|
errors: data.error.issues,
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const blockedSlot = await prisma.blockedSlot.update({
|
||||||
|
where: {
|
||||||
|
id: slotID,
|
||||||
|
user_id: authCheck.user.id,
|
||||||
|
},
|
||||||
|
data: data.data,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
start_time: true,
|
||||||
|
end_time: true,
|
||||||
|
reason: true,
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
is_recurring: true,
|
||||||
|
recurrence_end_date: true,
|
||||||
|
rrule: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!blockedSlot) {
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Blocked slot not found or not owned by user',
|
||||||
|
},
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
BlockedSlotResponseSchema,
|
||||||
|
{ success: true, blocked_slot: blockedSlot },
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DELETE = auth(async function DELETE(req, { params }) {
|
||||||
|
const authCheck = userAuthenticated(req);
|
||||||
|
if (!authCheck.continue)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
authCheck.response,
|
||||||
|
authCheck.metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
const slotID = (await params).slotID;
|
||||||
|
|
||||||
|
const deletedSlot = await prisma.blockedSlot.delete({
|
||||||
|
where: {
|
||||||
|
id: slotID,
|
||||||
|
user_id: authCheck.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deletedSlot) {
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Blocked slot not found or not owned by user',
|
||||||
|
},
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
SuccessResponseSchema,
|
||||||
|
{ success: true },
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
90
src/app/api/blocked_slots/[slotID]/swagger.ts
Normal file
90
src/app/api/blocked_slots/[slotID]/swagger.ts
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import {
|
||||||
|
updateBlockedSlotSchema,
|
||||||
|
BlockedSlotResponseSchema,
|
||||||
|
} from '@/app/api/blocked_slots/validation';
|
||||||
|
import {
|
||||||
|
notAuthenticatedResponse,
|
||||||
|
serverReturnedDataValidationErrorResponse,
|
||||||
|
userNotFoundResponse,
|
||||||
|
invalidRequestDataResponse,
|
||||||
|
} from '@/lib/defaultApiResponses';
|
||||||
|
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
|
||||||
|
import { SlotIdParamSchema } from '@/app/api/validation';
|
||||||
|
import zod from 'zod/v4';
|
||||||
|
|
||||||
|
export default function registerSwaggerPaths(registry: OpenAPIRegistry) {
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'get',
|
||||||
|
path: '/api/blocked_slots/{slotID}',
|
||||||
|
request: {
|
||||||
|
params: zod.object({
|
||||||
|
slotID: SlotIdParamSchema,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Blocked slot retrieved successfully',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: BlockedSlotResponseSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...userNotFoundResponse,
|
||||||
|
...notAuthenticatedResponse,
|
||||||
|
...serverReturnedDataValidationErrorResponse,
|
||||||
|
},
|
||||||
|
tags: ['Blocked Slots'],
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'delete',
|
||||||
|
path: '/api/blocked_slots/{slotID}',
|
||||||
|
request: {
|
||||||
|
params: zod.object({
|
||||||
|
slotID: SlotIdParamSchema,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Blocked slot deleted successfully',
|
||||||
|
},
|
||||||
|
...userNotFoundResponse,
|
||||||
|
...notAuthenticatedResponse,
|
||||||
|
...serverReturnedDataValidationErrorResponse,
|
||||||
|
},
|
||||||
|
tags: ['Blocked Slots'],
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'patch',
|
||||||
|
path: '/api/blocked_slots/{slotID}',
|
||||||
|
request: {
|
||||||
|
params: zod.object({
|
||||||
|
slotID: SlotIdParamSchema,
|
||||||
|
}),
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: updateBlockedSlotSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Blocked slot updated successfully',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: BlockedSlotResponseSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...userNotFoundResponse,
|
||||||
|
...notAuthenticatedResponse,
|
||||||
|
...serverReturnedDataValidationErrorResponse,
|
||||||
|
...invalidRequestDataResponse,
|
||||||
|
},
|
||||||
|
tags: ['Blocked Slots'],
|
||||||
|
});
|
||||||
|
}
|
127
src/app/api/blocked_slots/route.ts
Normal file
127
src/app/api/blocked_slots/route.ts
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import { auth } from '@/auth';
|
||||||
|
import { prisma } from '@/prisma';
|
||||||
|
import {
|
||||||
|
returnZodTypeCheckedResponse,
|
||||||
|
userAuthenticated,
|
||||||
|
} from '@/lib/apiHelpers';
|
||||||
|
import {
|
||||||
|
blockedSlotsQuerySchema,
|
||||||
|
BlockedSlotsResponseSchema,
|
||||||
|
BlockedSlotsSchema,
|
||||||
|
createBlockedSlotSchema,
|
||||||
|
} from './validation';
|
||||||
|
import {
|
||||||
|
ErrorResponseSchema,
|
||||||
|
ZodErrorResponseSchema,
|
||||||
|
} from '@/app/api/validation';
|
||||||
|
|
||||||
|
export const GET = auth(async function GET(req) {
|
||||||
|
const authCheck = userAuthenticated(req);
|
||||||
|
if (!authCheck.continue)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
authCheck.response,
|
||||||
|
authCheck.metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataRaw: Record<string, string | string[]> = {};
|
||||||
|
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 blockedSlotsQuerySchema.safeParseAsync(dataRaw);
|
||||||
|
if (!data.success)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ZodErrorResponseSchema,
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid request data',
|
||||||
|
errors: data.error.issues,
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
const { start, end } = data.data;
|
||||||
|
|
||||||
|
const requestUserId = authCheck.user.id;
|
||||||
|
|
||||||
|
const blockedSlots = await prisma.blockedSlot.findMany({
|
||||||
|
where: {
|
||||||
|
user_id: requestUserId,
|
||||||
|
start_time: { gte: start },
|
||||||
|
end_time: { lte: end },
|
||||||
|
},
|
||||||
|
orderBy: { start_time: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
start_time: true,
|
||||||
|
end_time: true,
|
||||||
|
reason: true,
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
BlockedSlotsResponseSchema,
|
||||||
|
{ success: true, blocked_slots: blockedSlots },
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const POST = auth(async function POST(req) {
|
||||||
|
const authCheck = userAuthenticated(req);
|
||||||
|
if (!authCheck.continue)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
authCheck.response,
|
||||||
|
authCheck.metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataRaw = await req.json();
|
||||||
|
const data = await createBlockedSlotSchema.safeParseAsync(dataRaw);
|
||||||
|
if (!data.success)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ZodErrorResponseSchema,
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid request data',
|
||||||
|
errors: data.error.issues,
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestUserId = authCheck.user.id;
|
||||||
|
|
||||||
|
if (!requestUserId) {
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'User not authenticated',
|
||||||
|
},
|
||||||
|
{ status: 401 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockedSlot = await prisma.blockedSlot.create({
|
||||||
|
data: {
|
||||||
|
...data.data,
|
||||||
|
user_id: requestUserId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return returnZodTypeCheckedResponse(BlockedSlotsSchema, blockedSlot, {
|
||||||
|
status: 201,
|
||||||
|
});
|
||||||
|
});
|
66
src/app/api/blocked_slots/swagger.ts
Normal file
66
src/app/api/blocked_slots/swagger.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import {
|
||||||
|
BlockedSlotResponseSchema,
|
||||||
|
BlockedSlotsResponseSchema,
|
||||||
|
blockedSlotsQuerySchema,
|
||||||
|
createBlockedSlotSchema,
|
||||||
|
} from './validation';
|
||||||
|
import {
|
||||||
|
invalidRequestDataResponse,
|
||||||
|
notAuthenticatedResponse,
|
||||||
|
serverReturnedDataValidationErrorResponse,
|
||||||
|
userNotFoundResponse,
|
||||||
|
} from '@/lib/defaultApiResponses';
|
||||||
|
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
|
||||||
|
|
||||||
|
export default function registerSwaggerPaths(registry: OpenAPIRegistry) {
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'get',
|
||||||
|
path: '/api/blocked_slots',
|
||||||
|
request: {
|
||||||
|
query: blockedSlotsQuerySchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Blocked slots retrieved successfully.',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: BlockedSlotsResponseSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...notAuthenticatedResponse,
|
||||||
|
...userNotFoundResponse,
|
||||||
|
...serverReturnedDataValidationErrorResponse,
|
||||||
|
},
|
||||||
|
tags: ['Blocked Slots'],
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'post',
|
||||||
|
path: '/api/blocked_slots',
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: createBlockedSlotSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
201: {
|
||||||
|
description: 'Blocked slot created successfully.',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: BlockedSlotResponseSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...notAuthenticatedResponse,
|
||||||
|
...userNotFoundResponse,
|
||||||
|
...serverReturnedDataValidationErrorResponse,
|
||||||
|
...invalidRequestDataResponse,
|
||||||
|
},
|
||||||
|
tags: ['Blocked Slots'],
|
||||||
|
});
|
||||||
|
}
|
52
src/app/api/blocked_slots/validation.ts
Normal file
52
src/app/api/blocked_slots/validation.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
|
||||||
|
import zod from 'zod/v4';
|
||||||
|
import {
|
||||||
|
eventEndTimeSchema,
|
||||||
|
eventStartTimeSchema,
|
||||||
|
} from '@/app/api/event/validation';
|
||||||
|
|
||||||
|
extendZodWithOpenApi(zod);
|
||||||
|
|
||||||
|
export const blockedSlotsQuerySchema = zod.object({
|
||||||
|
start: eventStartTimeSchema.optional(),
|
||||||
|
end: eventEndTimeSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const blockedSlotRecurrenceEndDateSchema = zod.iso
|
||||||
|
.datetime()
|
||||||
|
.or(zod.date().transform((date) => date.toISOString()));
|
||||||
|
|
||||||
|
export const BlockedSlotsSchema = zod
|
||||||
|
.object({
|
||||||
|
start_time: eventStartTimeSchema,
|
||||||
|
end_time: eventEndTimeSchema,
|
||||||
|
id: zod.string(),
|
||||||
|
reason: zod.string().nullish(),
|
||||||
|
created_at: zod.date(),
|
||||||
|
updated_at: zod.date(),
|
||||||
|
})
|
||||||
|
.openapi('BlockedSlotsSchema', {
|
||||||
|
description: 'Blocked time slot in the user calendar',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BlockedSlotsResponseSchema = zod.object({
|
||||||
|
success: zod.boolean().default(true),
|
||||||
|
blocked_slots: zod.array(BlockedSlotsSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BlockedSlotResponseSchema = zod.object({
|
||||||
|
success: zod.boolean().default(true),
|
||||||
|
blocked_slot: BlockedSlotsSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createBlockedSlotSchema = BlockedSlotsSchema.omit({
|
||||||
|
id: true,
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateBlockedSlotSchema = zod.object({
|
||||||
|
start_time: eventStartTimeSchema.optional(),
|
||||||
|
end_time: eventEndTimeSchema.optional(),
|
||||||
|
reason: zod.string().optional(),
|
||||||
|
});
|
|
@ -48,6 +48,7 @@ export const VisibleSlotSchema = EventSchema.omit({
|
||||||
type: zod.literal('event'),
|
type: zod.literal('event'),
|
||||||
users: zod.string().array(),
|
users: zod.string().array(),
|
||||||
user_id: zod.string().optional(),
|
user_id: zod.string().optional(),
|
||||||
|
organizer_id: zod.string().optional(),
|
||||||
})
|
})
|
||||||
.openapi('VisibleSlotSchema', {
|
.openapi('VisibleSlotSchema', {
|
||||||
description: 'Visible time slot in the user calendar',
|
description: 'Visible time slot in the user calendar',
|
||||||
|
|
|
@ -85,3 +85,14 @@ export const EventIdParamSchema = registry.registerParameter(
|
||||||
example: '67890',
|
example: '67890',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SlotIdParamSchema = registry.registerParameter(
|
||||||
|
'SlotIdParam',
|
||||||
|
zod.string().openapi({
|
||||||
|
param: {
|
||||||
|
name: 'slotID',
|
||||||
|
in: 'path',
|
||||||
|
},
|
||||||
|
example: 'abcde12345',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button';
|
||||||
import { fromZodIssue } from 'zod-validation-error/v4';
|
import { fromZodIssue } from 'zod-validation-error/v4';
|
||||||
import type { $ZodIssue } from 'zod/v4/core';
|
import type { $ZodIssue } from 'zod/v4/core';
|
||||||
import { useGetApiCalendar } from '@/generated/api/calendar/calendar';
|
import { useGetApiCalendar } from '@/generated/api/calendar/calendar';
|
||||||
|
import { usePatchApiBlockedSlotsSlotID } from '@/generated/api/blocked-slots/blocked-slots';
|
||||||
|
|
||||||
moment.updateLocale('en', {
|
moment.updateLocale('en', {
|
||||||
week: {
|
week: {
|
||||||
|
@ -47,6 +48,7 @@ const DaDRBCalendar = withDragAndDrop<
|
||||||
end: Date;
|
end: Date;
|
||||||
type: UserCalendarSchemaItem['type'];
|
type: UserCalendarSchemaItem['type'];
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
organizer?: string;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -190,6 +192,13 @@ function CalendarWithUserEvents({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const { mutate: patchBlockedSlot } = usePatchApiBlockedSlotsSlotID({
|
||||||
|
mutation: {
|
||||||
|
throwOnError(error) {
|
||||||
|
throw error.response?.data || 'Failed to update blocked slot';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DaDRBCalendar
|
<DaDRBCalendar
|
||||||
|
@ -218,11 +227,19 @@ function CalendarWithUserEvents({
|
||||||
end: new Date(event.end_time),
|
end: new Date(event.end_time),
|
||||||
type: event.type,
|
type: event.type,
|
||||||
userId: event.users[0],
|
userId: event.users[0],
|
||||||
|
organizer: event.type === 'event' ? event.organizer_id : undefined,
|
||||||
})) ?? []),
|
})) ?? []),
|
||||||
...(additionalEvents ?? []),
|
...(additionalEvents ?? []),
|
||||||
]}
|
]}
|
||||||
onSelectEvent={(event) => {
|
onSelectEvent={(event) => {
|
||||||
router.push(`/events/${event.id}`);
|
if (event.type === 'blocked_private') return;
|
||||||
|
if (event.type === 'blocked_owned') {
|
||||||
|
router.push(`/blocker/${event.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.type === 'event') {
|
||||||
|
router.push(`/events/${event.id}`);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onSelectSlot={(slotInfo) => {
|
onSelectSlot={(slotInfo) => {
|
||||||
router.push(
|
router.push(
|
||||||
|
@ -236,53 +253,105 @@ function CalendarWithUserEvents({
|
||||||
selectable={sesstion.data?.user?.id === userId}
|
selectable={sesstion.data?.user?.id === userId}
|
||||||
onEventDrop={(event) => {
|
onEventDrop={(event) => {
|
||||||
const { start, end, event: droppedEvent } = event;
|
const { start, end, event: droppedEvent } = event;
|
||||||
if (droppedEvent.type === 'blocked_private') return;
|
if (
|
||||||
|
droppedEvent.type === 'blocked_private' ||
|
||||||
|
(droppedEvent.organizer &&
|
||||||
|
droppedEvent.organizer !== sesstion.data?.user?.id)
|
||||||
|
)
|
||||||
|
return;
|
||||||
const startISO = new Date(start).toISOString();
|
const startISO = new Date(start).toISOString();
|
||||||
const endISO = new Date(end).toISOString();
|
const endISO = new Date(end).toISOString();
|
||||||
patchEvent(
|
if (droppedEvent.type === 'blocked_owned') {
|
||||||
{
|
patchBlockedSlot(
|
||||||
eventID: droppedEvent.id,
|
{
|
||||||
data: {
|
slotID: droppedEvent.id,
|
||||||
start_time: startISO,
|
data: {
|
||||||
end_time: endISO,
|
start_time: startISO,
|
||||||
|
end_time: endISO,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
refetch();
|
||||||
refetch();
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error updating blocked slot:', error);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
);
|
||||||
console.error('Error updating event:', error);
|
return;
|
||||||
|
} else if (droppedEvent.type === 'event') {
|
||||||
|
patchEvent(
|
||||||
|
{
|
||||||
|
eventID: droppedEvent.id,
|
||||||
|
data: {
|
||||||
|
start_time: startISO,
|
||||||
|
end_time: endISO,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
);
|
onSuccess: () => {
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error updating event:', error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onEventResize={(event) => {
|
onEventResize={(event) => {
|
||||||
const { start, end, event: resizedEvent } = event;
|
const { start, end, event: resizedEvent } = event;
|
||||||
if (resizedEvent.type === 'blocked_private') return;
|
if (
|
||||||
|
resizedEvent.type === 'blocked_private' ||
|
||||||
|
(resizedEvent.organizer &&
|
||||||
|
resizedEvent.organizer !== sesstion.data?.user?.id)
|
||||||
|
)
|
||||||
|
return;
|
||||||
const startISO = new Date(start).toISOString();
|
const startISO = new Date(start).toISOString();
|
||||||
const endISO = new Date(end).toISOString();
|
const endISO = new Date(end).toISOString();
|
||||||
if (startISO === endISO) {
|
if (startISO === endISO) {
|
||||||
console.warn('Start and end times are the same, skipping resize.');
|
console.warn('Start and end times are the same, skipping resize.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
patchEvent(
|
if (resizedEvent.type === 'blocked_owned') {
|
||||||
{
|
patchBlockedSlot(
|
||||||
eventID: resizedEvent.id,
|
{
|
||||||
data: {
|
slotID: resizedEvent.id,
|
||||||
start_time: startISO,
|
data: {
|
||||||
end_time: endISO,
|
start_time: startISO,
|
||||||
|
end_time: endISO,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
refetch();
|
||||||
refetch();
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error resizing blocked slot:', error);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
);
|
||||||
console.error('Error resizing event:', error);
|
return;
|
||||||
|
} else if (resizedEvent.type === 'event') {
|
||||||
|
patchEvent(
|
||||||
|
{
|
||||||
|
eventID: resizedEvent.id,
|
||||||
|
data: {
|
||||||
|
start_time: startISO,
|
||||||
|
end_time: endISO,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
);
|
onSuccess: () => {
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error resizing event:', error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/custom-ui/sidebar';
|
} from '@/components/custom-ui/sidebar';
|
||||||
|
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { CalendarMinus, CalendarMinus2, ChevronDown } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
|
@ -28,8 +28,8 @@ import Link from 'next/link';
|
||||||
import {
|
import {
|
||||||
Star,
|
Star,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
User,
|
//User,
|
||||||
Users,
|
//Users,
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
CalendarPlus,
|
CalendarPlus,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
@ -40,7 +40,7 @@ const items = [
|
||||||
url: '/home',
|
url: '/home',
|
||||||
icon: CalendarDays,
|
icon: CalendarDays,
|
||||||
},
|
},
|
||||||
{
|
/*{
|
||||||
title: 'Friends',
|
title: 'Friends',
|
||||||
url: '#',
|
url: '#',
|
||||||
icon: User,
|
icon: User,
|
||||||
|
@ -49,12 +49,17 @@ const items = [
|
||||||
title: 'Groups',
|
title: 'Groups',
|
||||||
url: '#',
|
url: '#',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
},
|
},*/
|
||||||
{
|
{
|
||||||
title: 'Events',
|
title: 'Events',
|
||||||
url: '/events',
|
url: '/events',
|
||||||
icon: CalendarClock,
|
icon: CalendarClock,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Blockers',
|
||||||
|
url: '/blocker',
|
||||||
|
icon: CalendarMinus,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
|
@ -123,6 +128,17 @@ export function AppSidebar() {
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
<SidebarMenuItem className='pl-[8px]'>
|
||||||
|
<Link
|
||||||
|
href='/blocker/new'
|
||||||
|
className='flex items-center gap-2 text-xl font-label'
|
||||||
|
>
|
||||||
|
<CalendarMinus2 className='size-8' />
|
||||||
|
<span className='group-data-[collapsible=icon]:hidden text-nowrap whitespace-nowrap'>
|
||||||
|
New Blocker
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuItem>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
56
src/components/custom-ui/blocked-slot-list-entry.tsx
Normal file
56
src/components/custom-ui/blocked-slot-list-entry.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
'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 { BlockedSlotsSchema } from '@/app/api/blocked_slots/validation';
|
||||||
|
|
||||||
|
type BlockedSlotListEntryProps = zod.output<typeof BlockedSlotsSchema>;
|
||||||
|
|
||||||
|
export default function BlockedSlotListEntry(slot: BlockedSlotListEntryProps) {
|
||||||
|
const formatDate = (isoString?: string) => {
|
||||||
|
if (!isoString) return '-';
|
||||||
|
return new Date(isoString).toLocaleDateString();
|
||||||
|
};
|
||||||
|
const formatTime = (isoString?: string) => {
|
||||||
|
if (!isoString) return '-';
|
||||||
|
return new Date(isoString).toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Link href={`/blocker/${slot.id}`} className='block'>
|
||||||
|
<Card className='w-full'>
|
||||||
|
<div className='grid grid-cols-1 gap-2 mx-auto md:mx-4 md:grid-cols-[80px_1fr_250px]'>
|
||||||
|
<div className='w-full items-center justify-center grid'>
|
||||||
|
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||||
|
</div>
|
||||||
|
<div className='w-full items-center justify-center grid my-3 md:my-0'>
|
||||||
|
<h2 className='text-center'>{slot.reason}</h2>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-4'>
|
||||||
|
<div className='grid grid-cols-[80px_auto] gap-2'>
|
||||||
|
<Label className='text-[var(--color-neutral-300)] justify-end'>
|
||||||
|
start
|
||||||
|
</Label>
|
||||||
|
<Label>
|
||||||
|
{formatDate(slot.start_time)} {formatTime(slot.start_time)}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-[80px_auto] gap-2'>
|
||||||
|
<Label className='text-[var(--color-neutral-300)] justify-end'>
|
||||||
|
end
|
||||||
|
</Label>
|
||||||
|
<Label>
|
||||||
|
{formatDate(slot.end_time)} {formatTime(slot.end_time)}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
|
@ -12,7 +12,6 @@ export default function LabeledInput({
|
||||||
error,
|
error,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
type: 'text' | 'email' | 'password';
|
|
||||||
label: string;
|
label: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
|
|
@ -5,16 +5,28 @@ import { user_default_light } from '@/assets/usericon/default/defaultusericon-ex
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import zod from 'zod/v4';
|
import zod from 'zod/v4';
|
||||||
import { ParticipantSchema } from '@/app/api/event/[eventID]/participant/validation';
|
import { ParticipantSchema } from '@/app/api/event/[eventID]/participant/validation';
|
||||||
|
import { usePatchApiEventEventIDParticipantUser } from '@/generated/api/event-participant/event-participant';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '../ui/select';
|
||||||
|
|
||||||
type ParticipantListEntryProps = zod.output<typeof ParticipantSchema>;
|
type ParticipantListEntryProps = zod.output<typeof ParticipantSchema>;
|
||||||
|
|
||||||
export default function ParticipantListEntry({
|
export default function ParticipantListEntry({
|
||||||
user,
|
user,
|
||||||
status,
|
status,
|
||||||
}: ParticipantListEntryProps) {
|
eventID,
|
||||||
|
}: ParticipantListEntryProps & { eventID?: string }) {
|
||||||
|
const session = useSession();
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
const defaultImage =
|
const defaultImage =
|
||||||
resolvedTheme === 'dark' ? user_default_dark : user_default_light;
|
resolvedTheme === 'dark' ? user_default_dark : user_default_light;
|
||||||
|
const updateAttendance = usePatchApiEventEventIDParticipantUser();
|
||||||
|
|
||||||
const finalImageSrc = user.image ?? defaultImage;
|
const finalImageSrc = user.image ?? defaultImage;
|
||||||
|
|
||||||
|
@ -22,7 +34,38 @@ export default function ParticipantListEntry({
|
||||||
<div className='flex items-center gap-2 py-1 ml-5'>
|
<div className='flex items-center gap-2 py-1 ml-5'>
|
||||||
<Image src={finalImageSrc} alt='Avatar' width={30} height={30} />
|
<Image src={finalImageSrc} alt='Avatar' width={30} height={30} />
|
||||||
<span>{user.name}</span>
|
<span>{user.name}</span>
|
||||||
<span className='text-sm text-gray-500'>{status}</span>
|
{user.id === session.data?.user?.id && eventID ? (
|
||||||
|
<Select
|
||||||
|
defaultValue={status}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
updateAttendance.mutate({
|
||||||
|
eventID: eventID,
|
||||||
|
user: session.data?.user?.id || '',
|
||||||
|
data: {
|
||||||
|
status: value as
|
||||||
|
| 'ACCEPTED'
|
||||||
|
| 'TENTATIVE'
|
||||||
|
| 'DECLINED'
|
||||||
|
| 'PENDING',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id='language'>
|
||||||
|
<SelectValue placeholder='Select status' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value='ACCEPTED'>Attending</SelectItem>
|
||||||
|
<SelectItem value='TENTATIVE'>Maybe Attending</SelectItem>
|
||||||
|
<SelectItem value='DECLINED'>Not Attending</SelectItem>
|
||||||
|
<SelectItem value='PENDING' disabled>
|
||||||
|
Pending Response
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<span className='text-sm text-gray-500'>{status}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
244
src/components/forms/blocked-slot-form.tsx
Normal file
244
src/components/forms/blocked-slot-form.tsx
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import useZodForm from '@/lib/hooks/useZodForm';
|
||||||
|
import {
|
||||||
|
updateBlockedSlotSchema,
|
||||||
|
createBlockedSlotSchema,
|
||||||
|
} from '@/app/api/blocked_slots/validation';
|
||||||
|
import {
|
||||||
|
useGetApiBlockedSlotsSlotID,
|
||||||
|
usePatchApiBlockedSlotsSlotID,
|
||||||
|
useDeleteApiBlockedSlotsSlotID,
|
||||||
|
usePostApiBlockedSlots,
|
||||||
|
} from '@/generated/api/blocked-slots/blocked-slots';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import React from 'react';
|
||||||
|
import LabeledInput from '../custom-ui/labeled-input';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Card, CardContent, CardHeader } from '../ui/card';
|
||||||
|
import Logo from '../misc/logo';
|
||||||
|
import { eventStartTimeSchema } from '@/app/api/event/validation';
|
||||||
|
import zod from 'zod/v4';
|
||||||
|
|
||||||
|
const dateForDateTimeInputValue = (date: Date) =>
|
||||||
|
new Date(date.getTime() + new Date().getTimezoneOffset() * -60 * 1000)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 19);
|
||||||
|
|
||||||
|
export default function BlockedSlotForm({
|
||||||
|
existingBlockedSlotId,
|
||||||
|
}: {
|
||||||
|
existingBlockedSlotId?: string;
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { data: existingBlockedSlot, isLoading: isLoadingExisting } =
|
||||||
|
useGetApiBlockedSlotsSlotID(existingBlockedSlotId || '');
|
||||||
|
|
||||||
|
const {
|
||||||
|
register: registerCreate,
|
||||||
|
handleSubmit: handleCreateSubmit,
|
||||||
|
formState: formStateCreate,
|
||||||
|
reset: resetCreate,
|
||||||
|
} = useZodForm(
|
||||||
|
createBlockedSlotSchema.extend({
|
||||||
|
start_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })),
|
||||||
|
end_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register: registerUpdate,
|
||||||
|
handleSubmit: handleUpdateSubmit,
|
||||||
|
formState: formStateUpdate,
|
||||||
|
reset: resetUpdate,
|
||||||
|
setValue: setValueUpdate,
|
||||||
|
} = useZodForm(
|
||||||
|
updateBlockedSlotSchema.extend({
|
||||||
|
start_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })),
|
||||||
|
end_time: eventStartTimeSchema.or(zod.iso.datetime({ local: true })),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: updateBlockedSlot } = usePatchApiBlockedSlotsSlotID({
|
||||||
|
mutation: {
|
||||||
|
onSuccess: () => {
|
||||||
|
resetUpdate();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: deleteBlockedSlot } = useDeleteApiBlockedSlotsSlotID({
|
||||||
|
mutation: {
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push('/blocker');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: createBlockedSlot } = usePostApiBlockedSlots({
|
||||||
|
mutation: {
|
||||||
|
onSuccess: () => {
|
||||||
|
resetCreate();
|
||||||
|
router.push('/blocker');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (existingBlockedSlot?.data) {
|
||||||
|
setValueUpdate(
|
||||||
|
'start_time',
|
||||||
|
dateForDateTimeInputValue(
|
||||||
|
new Date(existingBlockedSlot?.data.blocked_slot.start_time),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setValueUpdate(
|
||||||
|
'end_time',
|
||||||
|
dateForDateTimeInputValue(
|
||||||
|
new Date(existingBlockedSlot?.data.blocked_slot.end_time),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setValueUpdate(
|
||||||
|
'reason',
|
||||||
|
existingBlockedSlot?.data.blocked_slot.reason || '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
existingBlockedSlot?.data,
|
||||||
|
resetUpdate,
|
||||||
|
setValueUpdate,
|
||||||
|
isLoadingExisting,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onUpdateSubmit = handleUpdateSubmit(async (data) => {
|
||||||
|
await updateBlockedSlot(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
start_time: new Date(data.start_time).toISOString(),
|
||||||
|
end_time: new Date(data.end_time).toISOString(),
|
||||||
|
},
|
||||||
|
slotID: existingBlockedSlotId || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
router.back();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDeleteSubmit = async () => {
|
||||||
|
if (existingBlockedSlotId) {
|
||||||
|
await deleteBlockedSlot({ slotID: existingBlockedSlotId });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateSubmit = handleCreateSubmit(async (data) => {
|
||||||
|
await createBlockedSlot({
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
start_time: new Date(data.start_time).toISOString(),
|
||||||
|
end_time: new Date(data.end_time).toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center justify-center h-full'>
|
||||||
|
<Card className='w-[max(80%, 500px)] max-w-screen p-0 gap-0 max-xl:w-[95%] mx-auto'>
|
||||||
|
<CardHeader className='p-0 m-0 gap-0 px-6'>
|
||||||
|
<div className='h-full mt-0 ml-2 mb-16 flex items-center justify-between max-sm:grid max-sm:grid-row-start:auto max-sm:mb-6 max-sm:mt-10 max-sm:ml-0'>
|
||||||
|
<div className='w-[100px] max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||||
|
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||||
|
</div>
|
||||||
|
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||||
|
<h1 className='text-center'>
|
||||||
|
{existingBlockedSlotId ? 'Update Blocker' : 'Create Blocker'}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className='w-0 sm:w-[100px]'></div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={existingBlockedSlotId ? onUpdateSubmit : onCreateSubmit}
|
||||||
|
>
|
||||||
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'>
|
||||||
|
<LabeledInput
|
||||||
|
label='Start Time'
|
||||||
|
type='datetime-local'
|
||||||
|
id='start_time'
|
||||||
|
{...(existingBlockedSlotId
|
||||||
|
? registerUpdate('start_time')
|
||||||
|
: registerCreate('start_time'))}
|
||||||
|
error={
|
||||||
|
formStateCreate.errors.start_time?.message ||
|
||||||
|
formStateUpdate.errors.start_time?.message
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label='End Time'
|
||||||
|
type='datetime-local'
|
||||||
|
id='end_time'
|
||||||
|
{...(existingBlockedSlotId
|
||||||
|
? registerUpdate('end_time')
|
||||||
|
: registerCreate('end_time'))}
|
||||||
|
error={
|
||||||
|
formStateCreate.errors.end_time?.message ||
|
||||||
|
formStateUpdate.errors.end_time?.message
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label='Reason'
|
||||||
|
type='text'
|
||||||
|
id='reason'
|
||||||
|
{...(existingBlockedSlotId
|
||||||
|
? registerUpdate('reason')
|
||||||
|
: registerCreate('reason'))}
|
||||||
|
error={
|
||||||
|
formStateCreate.errors.reason?.message ||
|
||||||
|
formStateUpdate.errors.reason?.message
|
||||||
|
}
|
||||||
|
placeholder='Optional reason for blocking this slot'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-end gap-2 p-4'>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
variant='primary'
|
||||||
|
disabled={
|
||||||
|
formStateCreate.isSubmitting || formStateUpdate.isSubmitting
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{existingBlockedSlotId ? 'Update Blocker' : 'Create Blocker'}
|
||||||
|
</Button>
|
||||||
|
{existingBlockedSlotId && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='destructive'
|
||||||
|
onClick={onDeleteSubmit}
|
||||||
|
>
|
||||||
|
Delete Blocker
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{formStateCreate.errors.root && (
|
||||||
|
<p className='text-red-500 text-sm mt-1'>
|
||||||
|
{formStateCreate.errors.root.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{formStateUpdate.errors.root && (
|
||||||
|
<p className='text-red-500 text-sm mt-1'>
|
||||||
|
{formStateUpdate.errors.root.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -175,7 +175,6 @@ const EventForm: React.FC<EventFormProps> = (props) => {
|
||||||
console.log('Creating event');
|
console.log('Creating event');
|
||||||
const mutationResult = await createEvent({ data });
|
const mutationResult = await createEvent({ data });
|
||||||
eventID = mutationResult.data.event.id;
|
eventID = mutationResult.data.event.id;
|
||||||
createEvent({ data });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.custom((t) => (
|
toast.custom((t) => (
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
export const queryClient = new QueryClient();
|
||||||
|
|
||||||
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue