@@ -170,6 +169,51 @@ export default function StudentGroups({ orgs }: StudentGroupsProps) {
Student Groups
+
+
+
+
+
+ {EVENT_CATEGORIES.map((category) => (
+
+
+ handleCategoryChange(category.name, checked as boolean)
+ }
+ className={category.bgColor}
+ />
+
+
+ ))}
+
+
+
+
+
+ setShowInactiveOrgs(checked as boolean)
+ }
+ />
+
+
+
{igCardsToDisplay.length} RESULTS
diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx
index 9cf46cb..9cb1310 100644
--- a/components/ui/calendar.tsx
+++ b/components/ui/calendar.tsx
@@ -228,7 +228,7 @@ function CalendarDayButton({
ref={ref}
variant='ghost'
size='icon'
- data-day={day.date.toLocaleDateString()}
+ data-day={day.date.toISOString().slice(0, 10)}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx
index dc3b4c1..f781880 100644
--- a/components/ui/checkbox.tsx
+++ b/components/ui/checkbox.tsx
@@ -19,7 +19,7 @@ const Checkbox = React.forwardRef<
{...props}
>
diff --git a/components/ui/mobile-calendar.tsx b/components/ui/mobile-calendar.tsx
new file mode 100644
index 0000000..c36cbef
--- /dev/null
+++ b/components/ui/mobile-calendar.tsx
@@ -0,0 +1,126 @@
+'use client';
+
+import {
+ addDays,
+ addWeeks,
+ eachDayOfInterval,
+ isSameDay,
+ startOfDay,
+ startOfWeek,
+} from 'date-fns';
+import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
+import { useMemo, useRef, useState } from 'react';
+
+import { cn } from '@/lib/utils';
+import { SGT } from '@/lib/utils/client/time';
+
+const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
+
+export function MobileWeekStrip({
+ selected,
+ onSelect,
+}: {
+ selected: Date;
+ onSelect: (d: Date) => void;
+}) {
+ const [offset, setOffset] = useState(0);
+ // Main dragging logic
+ const dragX = useRef
(null);
+ const today = useMemo(() => startOfDay(new Date()), []);
+
+ const getWeek = (off: number) => {
+ const base = addWeeks(startOfWeek(today, { weekStartsOn: 1 }), off);
+ return eachDayOfInterval({ start: base, end: addDays(base, 6) });
+ };
+
+ const settle = (endX: number) => {
+ if (dragX.current === null) return;
+ const delta = endX - dragX.current;
+ if (delta < -60) setOffset((o) => o + 1);
+ else if (delta > 60) setOffset((o) => o - 1);
+ dragX.current = null;
+ };
+
+ const mid = getWeek(offset)[3];
+ const monthLabel = mid.toLocaleString('default', {
+ month: 'long',
+ year: 'numeric',
+ timeZone: SGT,
+ });
+
+ return (
+ {
+ dragX.current = e.clientX;
+ }}
+ onMouseUp={(e) => settle(e.clientX)}
+ onTouchStart={(e) => {
+ dragX.current = e.touches[0].clientX;
+ }}
+ onTouchEnd={(e) => settle(e.changedTouches[0].clientX)}
+ >
+
+
+
+ {monthLabel}
+
+
+
+
+
+ {getWeek(offset).map((day, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/lib/utils/client/index.ts b/lib/utils/client/index.ts
index aac11cd..e1ed7b6 100644
--- a/lib/utils/client/index.ts
+++ b/lib/utils/client/index.ts
@@ -1,3 +1,5 @@
+import { toSGT } from '@/lib/utils/client/time';
+
export function timeToIndex(time: string): number {
// Parse time string like "8:00am", "2:30pm", etc.
const match = time.match(/^(\d{1,2}):(\d{2}) (am|pm)$/i);
@@ -18,9 +20,8 @@ export function timeToIndex(time: string): number {
}
export function dateToHalfHourIndex(date: Date): number {
- const hour = date.getHours();
- const minute = date.getMinutes();
- return hour * 2 + (minute >= 30 ? 1 : 0);
+ const sgt = toSGT(date);
+ return sgt.getHours() * 2 + (sgt.getMinutes() >= 30 ? 1 : 0);
}
export function getTimeSpanInHalfHours(startDate: Date, endDate: Date): number {
diff --git a/lib/utils/client/time.ts b/lib/utils/client/time.ts
index dededd5..b53f99c 100644
--- a/lib/utils/client/time.ts
+++ b/lib/utils/client/time.ts
@@ -1,9 +1,26 @@
+import { TZDate } from '@date-fns/tz';
import { roundToNearestMinutes } from 'date-fns';
-export const dateTimeFormatter = Intl.DateTimeFormat('en-SG');
+export const SGT = 'Asia/Singapore';
+
+/** Convert any Date to a timezone-aware SGT TZDate. */
+export const toSGT = (date: Date) => new TZDate(date, SGT);
+
+export const dateTimeFormatter = Intl.DateTimeFormat('en-SG', {
+ timeZone: SGT,
+});
+
+export const timeFormatter = Intl.DateTimeFormat('en-GB', {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ timeZone: SGT,
+});
+
+export const formatTime = (d: Date) => timeFormatter.format(d);
export const getNext30Minutes = () => {
- return roundToNearestMinutes(new Date(), {
+ return roundToNearestMinutes(new TZDate(new Date(), SGT), {
nearestTo: 30,
roundingMethod: 'ceil',
});
diff --git a/package.json b/package.json
index 39d0a2c..4ed5aa6 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
+ "@date-fns/tz": "^1.4.1",
"@hookform/resolvers": "^5.2.2",
"@prisma/adapter-pg": "^7.4.1",
"@prisma/client": "^7.4.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 318be77..e9d1d93 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@date-fns/tz':
+ specifier: ^1.4.1
+ version: 1.4.1
'@hookform/resolvers':
specifier: ^5.2.2
version: 5.2.2(react-hook-form@7.71.2(react@19.2.4))