- {Object.entries(classesByCategory ?? {}).map(
- ([category, classesForCategory]) => {
- const sectionId = getCategorySectionId(category);
+ {classesByCategory.map(([category, classesForCategory]) => {
+ const sectionId = getCategorySectionId(category);
- const classesWithoutSubCategory = classesForCategory.filter(
- (c) => !c.subcategory,
- );
+ const classesWithoutSubCategory = classesForCategory.filter(
+ (c) => !c.subcategory,
+ );
- // Group classes by subcategory
- const classesBySubcategory = classesForCategory.reduce(
- (rec, c) => {
- if (!c.subcategory) return rec; // Skip those without subcategory
- const subcategory = c.subcategory;
- rec[subcategory] = [...(rec[subcategory] ?? []), c];
- return rec;
- },
- {} as Record
,
- );
+ // Group classes by subcategory
+ const classesBySubcategory = classesForCategory.reduce(
+ (rec, c) => {
+ if (!c.subcategory) return rec; // Skip those without subcategory
+ const subcategory = c.subcategory;
+ rec[subcategory] = [...(rec[subcategory] ?? []), c];
+ return rec;
+ },
+ {} as Record,
+ );
- return (
-
- {classesWithoutSubCategory.length > 0 && (
-
- {classesWithoutSubCategory.map((c) => (
- openSidebarFor(c.id)}
- />
- ))}
-
- )}
- {Object.entries(classesBySubcategory ?? {}).map(
- ([subcategory, classesForSubcategory]) => (
-
-
{subcategory}
-
- {classesForSubcategory.map((c) => (
- openSidebarFor(c.id)}
- />
- ))}
-
+ return (
+
+ {classesWithoutSubCategory.length > 0 && (
+
+ {classesWithoutSubCategory.map((c) => (
+ openSidebarFor(c.id)}
+ />
+ ))}
+
+ )}
+ {Object.entries(classesBySubcategory ?? {}).map(
+ ([subcategory, classesForSubcategory]) => (
+
+
{subcategory}
+
+ {classesForSubcategory.map((c) => (
+ openSidebarFor(c.id)}
+ />
+ ))}
- ),
- )}
-
- );
- },
- )}
+
+ ),
+ )}
+
+ );
+ })}
>
);
diff --git a/src/components/classes/list/content/term-form.tsx b/src/components/classes/list/content/term-form.tsx
index 64f961e9..8739a0a8 100644
--- a/src/components/classes/list/content/term-form.tsx
+++ b/src/components/classes/list/content/term-form.tsx
@@ -245,7 +245,13 @@ function TermFormShell({
}
export const TermForm = NiceModal.create(
- ({ editingId }: { editingId: string | null }) => {
+ ({
+ editingId,
+ onCreated,
+ }: {
+ editingId: string | null;
+ onCreated?: (termId: string) => void;
+ }) => {
const modal = useModal();
const apiUtils = clientApi.useUtils();
const editing = !!editingId;
@@ -258,7 +264,8 @@ export const TermForm = NiceModal.create(
const { mutate: createTermMutation, isPending: isUpdatingTerm } =
clientApi.term.create.useMutation({
- onSuccess: async () => {
+ onSuccess: async (createdTermId) => {
+ onCreated?.(createdTermId);
await apiUtils.term.all.invalidate();
await modal.hide();
},
diff --git a/src/components/classes/primitives/star-class-button.tsx b/src/components/classes/primitives/star-class-button.tsx
index abac64f1..84c1e779 100644
--- a/src/components/classes/primitives/star-class-button.tsx
+++ b/src/components/classes/primitives/star-class-button.tsx
@@ -69,6 +69,7 @@ export function StarClassButton({
{...props}
>
diff --git a/src/components/form/FormCheckbox.tsx b/src/components/form/FormCheckbox.tsx
index 95725106..2672f79a 100644
--- a/src/components/form/FormCheckbox.tsx
+++ b/src/components/form/FormCheckbox.tsx
@@ -1,9 +1,8 @@
import type { ComponentProps } from "react";
import type {
Control,
- ControllerProps,
FieldPath,
- FieldValues,
+ FieldValues
} from "react-hook-form";
import { Checkbox } from "../ui/checkbox";
import { FormFieldController } from "./FormField";
@@ -54,7 +53,7 @@ function FormCheckboxField<
{({ value, onChange, ...field }) => (
{label}}
description={description}
required={required}
hideErrors={hideErrors}
diff --git a/src/components/form/FormField.tsx b/src/components/form/FormField.tsx
index f4ec839c..84a22ae8 100644
--- a/src/components/form/FormField.tsx
+++ b/src/components/form/FormField.tsx
@@ -46,9 +46,9 @@ export interface FormFieldProps<
> {
name: TName;
control: Control;
- children: (
+ children: React.ReactNode | ((
field: FormFieldProp,
- ) => React.ReactNode;
+ ) => React.ReactNode);
}
export function FormFieldController<
@@ -84,7 +84,7 @@ export function FormFieldController<
fieldState,
}}
>
- {children(fieldWithMeta)}
+ {typeof children === "function" ? children(fieldWithMeta) : children}
);
}}
diff --git a/src/components/form/FormLayout.tsx b/src/components/form/FormLayout.tsx
index 7deb1759..5cca80fa 100644
--- a/src/components/form/FormLayout.tsx
+++ b/src/components/form/FormLayout.tsx
@@ -1,4 +1,7 @@
+import { isReactNodeNullthy } from "@/lib/nullthy";
+import { wrapIfNotArray } from "@/utils/arrayUtils";
import type { VariantProps } from "class-variance-authority";
+import type { ComponentProps } from "react";
import {
Field,
FieldContent,
@@ -9,9 +12,6 @@ import {
} from "../ui/field";
import { LabelRequiredMarker } from "../ui/label";
import { useFormFieldContext } from "./FormField";
-import { isReactNodeNullthy } from "@/lib/nullthy";
-import type { ComponentProps } from "react";
-import { wrapIfNotArray } from "@/utils/arrayUtils";
export interface FormFieldLayoutProps {
label?: React.ReactNode;
@@ -115,7 +115,7 @@ function FormError({
hideErrors?: boolean;
errors?: FieldErrorsType;
}) {
- const { fieldState, name } = useFormFieldContext();
+ const { fieldState } = useFormFieldContext();
if (
hideErrors ||
(!fieldState.invalid && !wrapIfNotArray(errors).filter((e) => !!e).length)
@@ -126,10 +126,9 @@ function FormError({
}
export {
- FormDescription,
- FormError,
- FormContent,
- FormField,
+ FormContent, FormDescription,
+ FormError, FormField,
FormFieldLayout,
- FormLabel,
+ FormLabel
};
+
diff --git a/src/components/form/FormSelect.tsx b/src/components/form/FormSelect.tsx
index 787a4489..3f506b91 100644
--- a/src/components/form/FormSelect.tsx
+++ b/src/components/form/FormSelect.tsx
@@ -1,9 +1,8 @@
import type { ComponentProps } from "react";
import type {
Control,
- ControllerProps,
FieldPath,
- FieldValues,
+ FieldValues
} from "react-hook-form";
import {
Select,
@@ -12,7 +11,7 @@ import {
SelectValue,
} from "../ui/select";
import { FormFieldController } from "./FormField";
-import { FormFieldLayout } from "./FormLayout";
+import { FormFieldLayout, type FormFieldLayoutProps } from "./FormLayout";
export interface FormSelectProps
extends Omit, "value" | "onValueChange"> {
@@ -60,6 +59,7 @@ export interface FormSelectFieldProps<
control: Control;
label?: React.ReactNode;
description?: React.ReactNode;
+ orientation?: FormFieldLayoutProps["orientation"]
required?: boolean;
hideErrors?: boolean;
placeholder?: string;
@@ -78,6 +78,7 @@ function FormSelectField<
description,
required,
hideErrors,
+ orientation,
placeholder,
className,
children,
@@ -91,6 +92,7 @@ function FormSelectField<
description={description}
required={required}
hideErrors={hideErrors}
+ orientation={orientation}
>
+ {Array.from({ length: 7 }, (_, i) => {
+ const curDate = new Date(weekStart);
+ curDate.setDate(weekStart.getDate() + i);
+
+ const isToday = isSameDay(curDate, selectedDate);
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/fullcalendar/fullcalendar-component.tsx b/src/components/fullcalendar/fullcalendar-component.tsx
new file mode 100644
index 00000000..dd250270
--- /dev/null
+++ b/src/components/fullcalendar/fullcalendar-component.tsx
@@ -0,0 +1,304 @@
+"use client";
+
+import { backgroundColors } from "@/components/ui/avatar";
+import { useBreakpoint } from "@/hooks/use-breakpoint";
+import { formatTimeRange } from "@/lib/schedule-fmt";
+import { cn } from "@/lib/utils";
+import { createPrng } from "@/utils/prngUtils";
+import {
+ type CalendarOptions,
+ type DayHeaderContentArg,
+ type EventContentArg,
+ type SlotLabelContentArg,
+} from "@fullcalendar/core";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import FullCalendarComponent from "@fullcalendar/react";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import { format, addHours } from "date-fns";
+import { Clock } from "lucide-react";
+import { useEffect, useEffectEvent, useMemo, useRef, useState } from "react";
+import useMeasure from "react-use-measure";
+import {
+ CalendarView,
+ isOddDay,
+ isSameDay,
+} from "../../app/(authorized)/schedule/dateUtils";
+import { DAYVIEW_TRIGGER_WIDTH_PX, FIRST_DAY_OF_WEEK } from "./constants";
+import { DayViewHeader } from "./day-view-header";
+import { useFullCalendarContext } from "./fullcalendar-context";
+import { FullCalendarDropShadowStyleOverrides } from "./fullcalendar.css.tsx";
+import "./fullcalendar.css";
+
+export function FullCalendar({
+ onDateChange,
+ className,
+ ...opts
+}: CalendarOptions & {
+ className?: string;
+ onDateChange?: (date: Date) => void;
+}) {
+ const { calendarRef, calendarApi } = useFullCalendarContext();
+
+ // Keep FullCalendar width in sync with layout changes (e.g. opening/closing the aside).
+ // ResizeObserver picks up the PageLayout aside animation and we debounce via rAF
+ // so FullCalendar can recompute column widths without jitter.
+ // This part was generated by ChatGPT to help with some animation nuances.
+ const calendarContainerRef = useRef