Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';

export default function Footer() {
return (
<footer className='sticky bottom-0 bg-[#0C2C47] px-8 py-4 text-white'>
<footer className='sticky bottom-0 z-10 bg-[#0C2C47] px-8 py-4 text-white'>
<div className='mx-auto flex items-center justify-between'>
<div className='flex space-x-4'>
<Button variant='ghost' size='icon' asChild>
Expand Down
141 changes: 99 additions & 42 deletions components/booking/Bookings.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
'use client';

import 'moment-timezone';
import './react-big-calendar.css';

import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import { addHours } from 'date-fns';
import moment from 'moment';
import { useSearchParams } from 'next/navigation';
import { useActionState, useEffect, useState } from 'react';
import { useCallback, useMemo } from 'react';
import { useActionState, useState } from 'react';
import {
Calendar as ReactCalendar,
momentLocalizer,
SlotInfo,
} from 'react-big-calendar';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod/v4';

import BookingModal from '@/components/booking/BookingModal';
import { Calendar } from '@/components/ui/calendar';
import { Spinner } from '@/components/ui/spinner';
import VenuesTimetable from '@/components/venue/VenuesTimetable';
import {
createBooking,
deleteBooking,
Expand All @@ -22,7 +32,14 @@ import { getNext30Minutes } from '@/lib/utils/client/time';
import type { BookingView } from '@/lib/utils/server/booking';
import type { VenueView } from '@/lib/utils/server/venue';

import BookingModal from './BookingModal';
moment.tz.setDefault('Asia/Singapore');
moment.locale('en-sg', {
week: {
dow: 1,
doy: 1,
},
});
const localizer = momentLocalizer(moment);

interface BookingsProp {
bookings: BookingView[];
Expand All @@ -35,17 +52,24 @@ interface BookingsProp {

const today = getNext30Minutes();

export default function Bookings({ bookings, venues, userOrgs }: BookingsProp) {
export default function Bookings({
bookings: bookingsOld,
venues,
userOrgs,
}: BookingsProp) {
const isAuthenticated = useAuth();
const searchParams = useSearchParams();

let date = today;
try {
const searchParamsDate = searchParams.get('date');
if (searchParamsDate && !isNaN(Date.parse(searchParamsDate))) {
date = new Date(searchParamsDate);
}
} catch {}
const date = useMemo(() => {
let date = new Date();
try {
const searchParamsDate = searchParams.get('date');
if (searchParamsDate && !isNaN(Date.parse(searchParamsDate))) {
date = new Date(searchParamsDate);
}
} catch {}
return date;
}, [searchParams]);

const [selectedBooking, setSelectedBooking] = useState<BookingView | null>(
null,
Expand Down Expand Up @@ -190,19 +214,25 @@ export default function Bookings({ bookings, venues, userOrgs }: BookingsProp) {
});
};

// Track mouse position for positioning the hover card
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
document.documentElement.style.setProperty('--mouse-x', `${e.clientX}px`);
document.documentElement.style.setProperty('--mouse-y', `${e.clientY}px`);
};

document.addEventListener('mousemove', handleMouseMove);

return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
const bookings = bookingsOld.map((booking) => ({
...booking,
resourceId: booking.venue.id,
}));

const handleSelectSlot = useCallback(
({ start, end, resourceId }: SlotInfo) => {
setSelectedTimeRange({
// Use the room from dragStart (column where user started dragging)
venue: venues.find(({ id }) => id === resourceId)!,
startTime: start,
endTime: end,
});
form.setValue('venueId', resourceId as number);
form.setValue('startTime', start);
form.setValue('endTime', end);
},
[],
);

return (
<div className={`relative flex flex-col bg-[#0C2C47] lg:flex-row`}>
Expand All @@ -215,7 +245,7 @@ export default function Bookings({ bookings, venues, userOrgs }: BookingsProp) {
)}
{/* Calendar - Hidden on mobile */}
{/* TODO: How do mobile people select dates? */}
<div className={`hidden w-72 rounded-lg bg-white p-4 lg:block`}>
<div className='hidden w-72 rounded-lg bg-white p-4 lg:block'>
<Calendar
mode='single'
selected={date}
Expand All @@ -226,27 +256,54 @@ export default function Bookings({ bookings, venues, userOrgs }: BookingsProp) {
window.history.pushState(null, '', `?${params.toString()}`);
}
}}
className='w-full rounded-md'
className='sticky top-19 w-full rounded-md'
showOutsideDays={false}
ISOWeek
/>
</div>

<VenuesTimetable
bookings={bookings}
venues={venues}
date={date}
handleBookingClick={(booking) => {
setSelectedBooking(booking);
form.setValue('bookingName', booking.bookingName);
form.setValue('organisationId', booking.bookedForOrg.id);
form.setValue('venueId', booking.venue.id);
form.setValue('startTime', booking.start);
form.setValue('endTime', booking.end);
form.setValue('addToCalendar', booking.event !== null);
}}
setSelectedTimeRange={setSelectedTimeRange}
form={form}
/>
<div className={`mt-10 flex-1 overflow-auto px-2 lg:ml-4 lg:px-0`}>
<ReactCalendar
selectable
showMultiDayTimes
toolbar={false}
formats={{
eventTimeRangeStartFormat: ({ start, end }, culture, localizer) =>
localizer!.format(start, 'hh:mm a', culture) +
' - ' +
localizer!.format(end, 'hh:mm a', culture),
eventTimeRangeEndFormat: ({ start, end }, culture, localizer) =>
localizer!.format(start, 'hh:mm a', culture) +
' - ' +
localizer!.format(end, 'hh:mm a', culture),
}}
date={date}
defaultView='day'
views={['day']}
localizer={localizer}
events={bookings}
titleAccessor={(event) =>
`${event.bookingName}\n${event.bookedForOrg.name}`
}
tooltipAccessor={(event) =>
`${event.bookingName}\n${event.bookedForOrg.name}`
}
resources={venues}
resourceIdAccessor='id'
resourceTitleAccessor='name'
onSelectSlot={handleSelectSlot}
onSelectEvent={(booking) => {
setSelectedBooking(booking);
form.setValue('bookingName', booking.bookingName);
form.setValue('organisationId', booking.bookedForOrg.id);
form.setValue('venueId', booking.venue.id);
form.setValue('startTime', booking.start);
form.setValue('endTime', booking.end);
form.setValue('addToCalendar', booking.event !== null);
}}
step={30}
/>
</div>

{/* Create Booking Modal */}
<BookingModal
Expand Down
72 changes: 72 additions & 0 deletions components/booking/react-big-calendar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@import 'tailwindcss';
@import 'react-big-calendar/lib/css/react-big-calendar.css';

/* TODO: Known issue - https://github.com/jquense/react-big-calendar/issues/2739 */

.rbc-allday-cell {
display: none;
}

.rbc-time-header,
.rbc-time-content {
@apply gap-1;
}

.rbc-time-header-content {
@apply !m-0 h-14 w-50 justify-center rounded-tl-3xl rounded-tr-3xl bg-white text-[#0C2C47];
}

.rbc-time-header-gutter {
@apply !z-1 !border-r-0 !bg-[#0C2C47];
}

.rbc-header,
.rbc-day-slot {
@apply !min-w-[200];
}

.rbc-time-gutter {
@apply !z-1 !-translate-y-3 !border-r-0 !bg-[#0C2C47];
}

.rbc-time-content {
@apply !overflow-y-visible;
}

.rbc-time-header {
@apply !overflow-visible;
}

.rbc-time-view,
.rbc-header,
.rbc-row-resource,
.rbc-timeslot-group,
.rbc-time-content {
@apply !border-0;
}

.rbc-header {
white-space: normal !important;
}

.rbc-day-slot {
background-color: white;
@apply !cursor-cell;
}

.rbc-day-slot .rbc-timeslot-group {
border-color: lab(90.952% -0.0000596046 0);
@apply !border-b;
}

.rbc-label {
@apply !text-white;
}

.rbc-event {
@apply !bg-[#FFD6CC] !text-xs !text-gray-800;
}

.rbc-event-content {
white-space: pre-line;
}
3 changes: 2 additions & 1 deletion components/event/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,8 @@ export default function Events({ events, userOrgs }: EventsProps) {

<div className='text-xs'>
<div className='mb-3 grid grid-cols-7 gap-1'>
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((dayLetter, index) => (
{/* TODO: Add a box to show current date, similar to the calendar on bookings page */}
{['M', 'T', 'W', 'T', 'F', 'S', 'S'].map((dayLetter, index) => (
<div
key={`${dayLetter}-${index}`}
className='py-1 text-center text-[#0C2C47]'
Expand Down
Loading