From 2c526e0db6a9ba92965e01c2bf42fc5f0feffb90 Mon Sep 17 00:00:00 2001 From: TheMythologist Date: Fri, 9 Jan 2026 15:48:45 +0800 Subject: [PATCH 1/7] ui: Use known calendar npm package instead --- components/booking/Bookings.tsx | 134 +++++++++----- components/booking/react-big-calendar.css | 63 +++++++ components/event/Events.tsx | 2 + package.json | 4 + pnpm-lock.yaml | 210 ++++++++++++++++++++++ 5 files changed, 373 insertions(+), 40 deletions(-) create mode 100644 components/booking/react-big-calendar.css diff --git a/components/booking/Bookings.tsx b/components/booking/Bookings.tsx index 4965a10..d1c78d2 100644 --- a/components/booking/Bookings.tsx +++ b/components/booking/Bookings.tsx @@ -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, @@ -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[]; @@ -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( null, @@ -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 (
@@ -228,25 +258,49 @@ export default function Bookings({ bookings, venues, userOrgs }: BookingsProp) { }} className='w-full rounded-md' showOutsideDays={false} + ISOWeek />
- { - 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} - /> +
+ + 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}` + } + 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} + /> +
{/* Create Booking Modal */}
+ {/* TODO: Change this to start on Monday instead */} + {/* TODO: Add a box to show current date, similar to the calendar on bookings page */} {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((dayLetter, index) => (
=6.9.0'} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.4': resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} @@ -559,6 +575,9 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@posthog/core@1.2.4': resolution: {integrity: sha512-o2TkycuV98PtAkcqE8B1DJv5LBvHEDTWirK5TlkQMeF2MJg0BYliY95CeRZFILNgZJCbI3k/fhahSMRQlpXOMg==} @@ -987,6 +1006,11 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@restart/hooks@0.4.16': + resolution: {integrity: sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==} + peerDependencies: + react: '>=16.8.0' + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1113,6 +1137,9 @@ packages: '@tybys/wasm-util@0.10.0': resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/date-arithmetic@4.1.4': + resolution: {integrity: sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1134,6 +1161,12 @@ packages: '@types/node@20.19.11': resolution: {integrity: sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-big-calendar@1.16.3': + resolution: {integrity: sha512-CR+5BKMhlr/wPgsp+sXOeNKNkoU1h/+6H1XoWuL7xnurvzGRQv/EnM8jPS9yxxBvXI8pjQBaJcI7RTSGiewG/Q==} + '@types/react-dom@19.1.9': resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} peerDependencies: @@ -1142,6 +1175,9 @@ packages: '@types/react@19.1.12': resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==} + '@types/warning@3.0.3': + resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} + '@typescript-eslint/eslint-plugin@8.42.0': resolution: {integrity: sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1498,6 +1534,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1553,6 +1593,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + date-arithmetic@4.1.0: + resolution: {integrity: sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==} + date-fns-jalali@4.1.0-0: resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} @@ -1562,6 +1605,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -1597,6 +1643,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -1615,6 +1665,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -1917,6 +1970,9 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + globalize@0.1.1: + resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1985,6 +2041,9 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2231,6 +2290,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.22: + resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -2255,6 +2317,9 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2264,6 +2329,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + magic-string@0.30.18: resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==} @@ -2277,6 +2346,9 @@ packages: mdn-data@2.23.0: resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==} + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2308,6 +2380,15 @@ packages: engines: {node: '>=10'} hasBin: true + moment-timezone@0.5.48: + resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==} + + moment-timezone@0.6.0: + resolution: {integrity: sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2580,6 +2661,12 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + react-big-calendar@1.19.4: + resolution: {integrity: sha512-FrvbDx2LF6JAWFD96LU1jjloppC5OgIvMYUYIPzAw5Aq+ArYFPxAjLqXc4DyxfsQDN0TJTMuS/BIbcSB7Pg0YA==} + peerDependencies: + react: ^16.14.0 || ^17 || ^18 || ^19 + react-dom: ^16.14.0 || ^17 || ^18 || ^19 + react-day-picker@9.9.0: resolution: {integrity: sha512-NtkJbuX6cl/VaGNb3sVVhmMA6LSMnL5G3xNL+61IyoZj0mUZFWTg4hmj7PHjIQ8MXN9dHWhUHFoJWG6y60DKSg==} engines: {node: '>=18'} @@ -2605,6 +2692,15 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-overlays@5.2.1: + resolution: {integrity: sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==} + peerDependencies: + react: '>=16.3.0' + react-dom: '>=16.3.0' + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -2921,6 +3017,11 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + uncontrollable@7.2.1: + resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} + peerDependencies: + react: '>=15.0.0' + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -2953,6 +3054,9 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} @@ -3004,6 +3108,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/runtime@7.28.4': {} + '@babel/types@7.28.4': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -3299,6 +3405,8 @@ snapshots: '@pkgr/core@0.2.9': {} + '@popperjs/core@2.11.8': {} + '@posthog/core@1.2.4': {} '@prisma/client@6.15.0(prisma@6.15.0(typescript@5.9.2))(typescript@5.9.2)': @@ -3722,6 +3830,11 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@restart/hooks@0.4.16(react@18.3.1)': + dependencies: + dequal: 2.0.3 + react: 18.3.1 + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.12.0': {} @@ -3825,6 +3938,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/date-arithmetic@4.1.4': {} + '@types/estree@1.0.8': {} '@types/js-cookie@3.0.6': {} @@ -3844,6 +3959,14 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/prop-types@15.7.15': {} + + '@types/react-big-calendar@1.16.3': + dependencies: + '@types/date-arithmetic': 4.1.4 + '@types/prop-types': 15.7.15 + '@types/react': 19.1.12 + '@types/react-dom@19.1.9(@types/react@19.1.12)': dependencies: '@types/react': 19.1.12 @@ -3852,6 +3975,8 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/warning@3.0.3': {} + '@typescript-eslint/eslint-plugin@8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -4202,6 +4327,8 @@ snapshots: client-only@0.0.1: {} + clsx@1.2.1: {} + clsx@2.1.1: {} cmdk@1.1.1(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -4260,12 +4387,16 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + date-arithmetic@4.1.0: {} + date-fns-jalali@4.1.0-0: {} date-fns@3.6.0: {} date-fns@4.1.0: {} + dayjs@1.11.19: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -4292,6 +4423,8 @@ snapshots: defu@6.1.4: {} + dequal@2.0.3: {} + destr@2.0.5: {} detect-libc@2.1.2: {} @@ -4304,6 +4437,11 @@ snapshots: dependencies: esutils: 2.0.3 + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.4 + csstype: 3.1.3 + dotenv@16.6.1: {} dotenv@17.2.2: {} @@ -4775,6 +4913,8 @@ snapshots: dependencies: is-glob: 4.0.3 + globalize@0.1.1: {} + globals@14.0.0: {} globalthis@1.0.4: @@ -4829,6 +4969,10 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -5072,6 +5216,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.22: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} @@ -5088,6 +5234,8 @@ snapshots: lodash.once@4.1.1: {} + lodash@4.17.21: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -5096,6 +5244,8 @@ snapshots: dependencies: react: 18.3.1 + luxon@3.7.2: {} + magic-string@0.30.18: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5106,6 +5256,8 @@ snapshots: mdn-data@2.23.0: {} + memoize-one@6.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -5131,6 +5283,16 @@ snapshots: mkdirp@3.0.1: {} + moment-timezone@0.5.48: + dependencies: + moment: 2.30.1 + + moment-timezone@0.6.0: + dependencies: + moment: 2.30.1 + + moment@2.30.1: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -5340,6 +5502,27 @@ snapshots: defu: 6.1.4 destr: 2.0.5 + react-big-calendar@1.19.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + clsx: 1.2.1 + date-arithmetic: 4.1.0 + dayjs: 1.11.19 + dom-helpers: 5.2.1 + globalize: 0.1.1 + invariant: 2.2.4 + lodash: 4.17.21 + lodash-es: 4.17.22 + luxon: 3.7.2 + memoize-one: 6.0.0 + moment: 2.30.1 + moment-timezone: 0.5.48 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-overlays: 5.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + uncontrollable: 7.2.1(react@18.3.1) + react-day-picker@9.9.0(react@18.3.1): dependencies: '@date-fns/tz': 1.4.1 @@ -5363,6 +5546,21 @@ snapshots: react-is@16.13.1: {} + react-lifecycles-compat@3.0.4: {} + + react-overlays@5.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@popperjs/core': 2.11.8 + '@restart/hooks': 0.4.16(react@18.3.1) + '@types/warning': 3.0.3 + dom-helpers: 5.2.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + uncontrollable: 7.2.1(react@18.3.1) + warning: 4.0.3 + react-remove-scroll-bar@2.3.8(@types/react@19.1.12)(react@18.3.1): dependencies: react: 18.3.1 @@ -5775,6 +5973,14 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + uncontrollable@7.2.1(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@types/react': 19.1.12 + invariant: 2.2.4 + react: 18.3.1 + react-lifecycles-compat: 3.0.4 + undici-types@6.21.0: {} unrs-resolver@1.11.1: @@ -5822,6 +6028,10 @@ snapshots: v8-compile-cache-lib@3.0.1: {} + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + web-vitals@4.2.4: {} which-boxed-primitive@1.1.1: From 781a861596ebd6a3d1c4c631087034171ab6918a Mon Sep 17 00:00:00 2001 From: TheMythologist Date: Fri, 9 Jan 2026 15:52:19 +0800 Subject: [PATCH 2/7] fix: Proper day labels for events page --- components/event/Events.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/event/Events.tsx b/components/event/Events.tsx index 5940df7..730e855 100644 --- a/components/event/Events.tsx +++ b/components/event/Events.tsx @@ -296,9 +296,8 @@ export default function Events({ events, userOrgs }: EventsProps) {
- {/* TODO: Change this to start on Monday instead */} {/* TODO: Add a box to show current date, similar to the calendar on bookings page */} - {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((dayLetter, index) => ( + {['M', 'T', 'W', 'T', 'F', 'S', 'S'].map((dayLetter, index) => (
Date: Fri, 9 Jan 2026 15:58:39 +0800 Subject: [PATCH 3/7] chore: run prettier --- components/booking/react-big-calendar.css | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/components/booking/react-big-calendar.css b/components/booking/react-big-calendar.css index f2229bf..5eb8b20 100644 --- a/components/booking/react-big-calendar.css +++ b/components/booking/react-big-calendar.css @@ -5,7 +5,8 @@ display: none; } -.rbc-time-header, .rbc-time-content { +.rbc-time-header, +.rbc-time-content { @apply gap-1; } @@ -14,18 +15,23 @@ } .rbc-time-header-gutter { - @apply !bg-transparent !border-r-0; + @apply !border-r-0 !bg-transparent; } -.rbc-header, .rbc-day-slot { +.rbc-header, +.rbc-day-slot { @apply !min-w-[200]; } .rbc-time-gutter { - @apply !bg-transparent !border-r-0; + @apply !border-r-0 !bg-transparent; } -.rbc-time-view, .rbc-header, .rbc-row-resource, .rbc-timeslot-group, .rbc-time-content { +.rbc-time-view, +.rbc-header, +.rbc-row-resource, +.rbc-timeslot-group, +.rbc-time-content { @apply !border-0; } @@ -58,6 +64,6 @@ } .rbc-day-slot .rbc-timeslot-group { - border-color: lab(90.952% -.0000596046 0); + border-color: lab(90.952% -0.0000596046 0); @apply !border-b; } From fc374a09d5f64541e9377fbd6e2ae473f8890db1 Mon Sep 17 00:00:00 2001 From: TheMythologist Date: Fri, 9 Jan 2026 23:04:12 +0800 Subject: [PATCH 4/7] chore: Remove unused components --- components/venue/VenueTimetable.tsx | 145 -------------------------- components/venue/VenuesTimetable.tsx | 150 --------------------------- 2 files changed, 295 deletions(-) delete mode 100644 components/venue/VenueTimetable.tsx delete mode 100644 components/venue/VenuesTimetable.tsx diff --git a/components/venue/VenueTimetable.tsx b/components/venue/VenueTimetable.tsx deleted file mode 100644 index 097a257..0000000 --- a/components/venue/VenueTimetable.tsx +++ /dev/null @@ -1,145 +0,0 @@ -'use client'; - -import { format } from 'date-fns'; -import type { Dispatch, SetStateAction } from 'react'; - -import { TIMETABLE_TIMESLOTS } from '@/lib/formOptions'; -import { - dateToHalfHourIndex, - getTimeSpanInHalfHours, - timeToIndex, -} from '@/lib/utils/client'; -import type { BookingView } from '@/lib/utils/server/booking'; -import type { VenueView } from '@/lib/utils/server/venue'; - -import type { DragPosition } from './VenuesTimetable'; - -interface VenueTimetableProps { - venue: VenueView; - bookings: BookingView[]; - dragStart: DragPosition | null; - setDragStart: Dispatch>; - dragEnd: DragPosition | null; - setDragEnd: Dispatch>; - handleBookingClick: (booking: BookingView) => void; - setHoveredBooking: Dispatch>; -} - -export default function VenueTimetable({ - venue, - bookings, - dragStart, - setDragStart, - dragEnd, - setDragEnd, - handleBookingClick, - setHoveredBooking, -}: VenueTimetableProps) { - const getCellBooking = (time: string) => { - const timeIndex = timeToIndex(time); - - // Find a booking that overlaps with this 30-minute slot - return bookings.find((booking) => { - const startIndex = dateToHalfHourIndex(booking.start); - let endIndex = dateToHalfHourIndex(booking.end); - // Account for bookings ending at midnight - if (endIndex === 0) endIndex = 48; - return timeIndex >= startIndex && timeIndex < endIndex; - }); - }; - - const handleMouseDown = (time: string) => { - if (!getCellBooking(time)) { - setDragStart({ time, venue }); - setDragEnd({ time, venue }); - } - }; - - const handleMouseMove = (time: string) => { - if (dragStart) { - setDragEnd({ time, venue }); - } - }; - - return ( -
- {/* Room header */} -
- {venue.name} -
- - {/* Time cells */} -
- {TIMETABLE_TIMESLOTS.map((time) => { - const booking = getCellBooking(time); - - // Check if this cell is in drag selection - const isSelected = - dragStart && - dragEnd && - dragStart.venue.id === venue.id && - timeToIndex(time) >= - Math.min( - timeToIndex(dragStart.time), - timeToIndex(dragEnd.time), - ) && - timeToIndex(time) <= - Math.max( - timeToIndex(dragStart.time), - timeToIndex(dragEnd.time), - ) && - !booking; - - if (!booking) - return ( -
handleMouseDown(time.toUpperCase())} - onMouseMove={() => handleMouseMove(time.toUpperCase())} - /> - ); - - const bookingStartTime = format(booking.start, 'p') - .toLowerCase() - .replace(/\s/g, ''); - const normalizedTime = time.toLowerCase().replace(/\s/g, ''); - const isFirstCell = bookingStartTime === normalizedTime; - - const getSpan = () => { - if (!booking || !isFirstCell) return 1; - return getTimeSpanInHalfHours(booking.start, booking.end); - }; - - const span = getSpan(); - - if (isFirstCell) - return ( -
handleBookingClick(booking)} - onMouseEnter={() => setHoveredBooking(booking)} - onMouseLeave={() => setHoveredBooking(null)} - > -
-
{booking.bookingName}
-
{booking.bookedForOrg.name}
-
- {`${format(booking.start, 'p')} - ${format(booking.end, 'h:mma')}`} -
-
-
- ); - - return null; - })} -
-
- ); -} diff --git a/components/venue/VenuesTimetable.tsx b/components/venue/VenuesTimetable.tsx deleted file mode 100644 index 78e71d4..0000000 --- a/components/venue/VenuesTimetable.tsx +++ /dev/null @@ -1,150 +0,0 @@ -'use client'; - -import { isSameDay } from 'date-fns'; -import { type Dispatch, type SetStateAction, useState } from 'react'; -import { UseFormReturn } from 'react-hook-form'; -import { z } from 'zod/v4'; - -import BookingCard from '@/components/booking/BookingCard'; -import CardPortal from '@/components/CardPortal'; -import { TIMETABLE_TIMESLOTS } from '@/lib/formOptions'; -import { NewBookingClientSchema } from '@/lib/schema/booking'; -import { timeToIndex } from '@/lib/utils/client'; -import { BookingView } from '@/lib/utils/server/booking'; -import { VenueView } from '@/lib/utils/server/venue'; - -import VenueTimetable from './VenueTimetable'; - -export interface DragPosition { - time: string; - venue: VenueView; -} - -interface VenuesTimetableProps { - bookings: BookingView[]; - venues: VenueView[]; - date: Date; - handleBookingClick: (booking: BookingView) => void; - setSelectedTimeRange: Dispatch< - SetStateAction<{ - venue: VenueView; - startTime: Date; - endTime: Date; - } | null> - >; - form: UseFormReturn>; -} - -export default function VenuesTimetable({ - bookings, - venues, - date, - handleBookingClick, - setSelectedTimeRange, - form, -}: VenuesTimetableProps) { - const [dragStart, setDragStart] = useState(null); - const [dragEnd, setDragEnd] = useState(null); - const [hoveredBooking, setHoveredBooking] = useState( - null, - ); - - const handleMouseUp = () => { - if (dragStart && dragEnd) { - // Calculate start and end times from drag - const startIndex = Math.min( - timeToIndex(dragStart.time), - timeToIndex(dragEnd.time), - ); - const endIndex = - Math.max(timeToIndex(dragStart.time), timeToIndex(dragEnd.time)) + 1; - - const startTime = new Date(date); - startTime.setHours(Math.floor(startIndex / 2), 0); - if (startIndex % 2 === 1) startTime.setMinutes(30); - - const endTime = new Date(date); - endTime.setHours(Math.floor(endIndex / 2), 0); - if (endIndex % 2 === 1) endTime.setMinutes(30); - - setSelectedTimeRange({ - // Use the room from dragStart (column where user started dragging) - venue: dragStart.venue, - startTime, - endTime, - }); - form.setValue('venueId', dragStart.venue.id); - form.setValue('startTime', startTime); - form.setValue('endTime', endTime); - - // Reset drag state - setDragStart(null); - setDragEnd(null); - } - }; - - return ( -
- {/* Timetable - Full width on mobile */} -
- {/* Time labels column */} -
-
- {TIMETABLE_TIMESLOTS.map((time, index) => - index % 2 === 0 ? ( -
- {time} - {/* White connecting line */} -
-
- ) : null, - )} -
- 12:00 am - {/* White connecting line */} -
-
-
- - {/* Scrollable container for room timetables */} -
-
- {venues.map((venue) => ( - - booking.venue.id === venue.id && - isSameDay(booking.start, date), - )} - dragStart={dragStart} - dragEnd={dragEnd} - setHoveredBooking={setHoveredBooking} - setDragStart={setDragStart} - setDragEnd={setDragEnd} - handleBookingClick={handleBookingClick} - /> - ))} -
-
-
- - {/* Hover card portal */} - {/* TODO: Use react-tooltip instead */} - {/* TODO: Disable tooltip on mobile */} - {hoveredBooking && ( - -
- -
-
- )} -
- ); -} From c95c9fe900184572e92ef2b299870c3dbbfabe6b Mon Sep 17 00:00:00 2001 From: TheMythologist Date: Sat, 10 Jan 2026 12:13:30 +0800 Subject: [PATCH 5/7] ui: Fix react big calendar time gutter z-index --- components/booking/react-big-calendar.css | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/components/booking/react-big-calendar.css b/components/booking/react-big-calendar.css index 5eb8b20..1c4436d 100644 --- a/components/booking/react-big-calendar.css +++ b/components/booking/react-big-calendar.css @@ -24,7 +24,7 @@ } .rbc-time-gutter { - @apply !border-r-0 !bg-transparent; + @apply !border-r-0 !bg-transparent !-translate-y-2 !z-0; } .rbc-time-view, @@ -41,14 +41,16 @@ .rbc-day-slot { background-color: white; + @apply !cursor-cell; } -.rbc-label { - @apply !text-white; +.rbc-day-slot .rbc-timeslot-group { + border-color: lab(90.952% -0.0000596046 0); + @apply !border-b; } -.rbc-time-gutter { - @apply !-translate-y-2; +.rbc-label { + @apply !text-white; } .rbc-event { @@ -58,12 +60,3 @@ .rbc-event-content { white-space: pre-line; } - -.rbc-day-slot { - @apply !cursor-cell; -} - -.rbc-day-slot .rbc-timeslot-group { - border-color: lab(90.952% -0.0000596046 0); - @apply !border-b; -} From 4b60a502ece8f5b4ec9def63048bc3c1fc82605c Mon Sep 17 00:00:00 2001 From: TheMythologist Date: Sat, 10 Jan 2026 23:51:04 +0800 Subject: [PATCH 6/7] ui: Miscellaneous css fixes --- components/booking/react-big-calendar.css | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/components/booking/react-big-calendar.css b/components/booking/react-big-calendar.css index 1c4436d..b95974d 100644 --- a/components/booking/react-big-calendar.css +++ b/components/booking/react-big-calendar.css @@ -11,7 +11,7 @@ } .rbc-time-header-content { - @apply h-14 justify-center rounded-tl-3xl rounded-tr-3xl bg-white text-[#0C2C47]; + @apply !m-0 h-14 w-50 justify-center rounded-tl-3xl rounded-tr-3xl bg-white text-[#0C2C47]; } .rbc-time-header-gutter { @@ -24,7 +24,16 @@ } .rbc-time-gutter { - @apply !border-r-0 !bg-transparent !-translate-y-2 !z-0; + @apply !z-0 !-translate-y-3 !border-r-0 !bg-transparent; +} + +.rbc-time-content { + /* TODO: This doesn't work - https://stackoverflow.com/a/6433475/19723226 */ + @apply !overflow-y-visible; +} + +.rbc-time-header { + @apply !overflow-visible; } .rbc-time-view, From 45f47b3103d70ff610db07033cb2276e039f4ccc Mon Sep 17 00:00:00 2001 From: TheMythologist Date: Sun, 11 Jan 2026 14:04:24 +0800 Subject: [PATCH 7/7] ui: More miscellaneous css fixes --- components/Footer.tsx | 2 +- components/booking/Bookings.tsx | 7 +++++-- components/booking/react-big-calendar.css | 7 ++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/components/Footer.tsx b/components/Footer.tsx index 9631c3c..510f88c 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button'; export default function Footer() { return ( -