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
88 changes: 53 additions & 35 deletions src/components/classes/edit/content/class-general-section.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"use client";

import { useEffect } from "react";
import { useWatch } from "react-hook-form";

import { CLASS_CATEGORIES } from "@/components/classes/constants";
import { FormInputField } from "@/components/form/FormInput";
import { FormSelectField } from "@/components/form/FormSelect";
Expand All @@ -22,9 +25,21 @@ import { FormError } from "@/components/form/FormLayout";

export function ClassGeneralSection() {
const {
form: { control },
form: { control, setValue, getValues },
} = useClassForm();

const category = useWatch({ control, name: "category" });
const isExercise = category?.includes("Exercise") ?? false;

useEffect(() => {
if (isExercise && !getValues("levelRange")) {
setValue("levelRange", [1, 4], {
shouldValidate: true,
shouldDirty: true,
});
}
}, [isExercise, setValue, getValues]);

return (
<Card className="w-full">
<CardHeader>
Expand Down Expand Up @@ -65,42 +80,45 @@ export function ClassGeneralSection() {
/>
</FieldGroup>

<FormFieldController control={control} name="levelRange">
{({ onChange, value, ...field }) => (
<>
<FieldLabel className="w-full flex justify-between items-end">
<span>
Levels <LabelRequiredMarker />
</span>
<span className="text-sm">
Level {value[0]} {value[0] !== value[1] && `- ${value[1]}`}
</span>
</FieldLabel>

<div>
<Slider
min={1}
max={4}
step={1}
value={value}
onValueChange={onChange}
className="my-2"
{...field}
/>

{/* Labels for each level */}
<div className="flex justify-between text-xs text-muted-foreground">
<span>Level 1</span>
<span>Level 2</span>
<span>Level 3</span>
<span>Level 4</span>
{isExercise && (
<FormFieldController control={control} name="levelRange">
{({ onChange, value, ...field }) => (
<>
<FieldLabel className="w-full flex justify-between items-end">
<span>
Levels <LabelRequiredMarker />
</span>
<span className="text-sm">
Level {value?.[0] ?? 1}{" "}
{value?.[0] !== value?.[1] && `- ${value?.[1] ?? 4}`}
</span>
</FieldLabel>

<div>
<Slider
min={1}
max={4}
step={1}
value={value ?? [1, 4]}
onValueChange={onChange}
className="my-2"
{...field}
/>

{/* Labels for each level */}
<div className="flex justify-between text-xs text-muted-foreground">
<span>Level 1</span>
<span>Level 2</span>
<span>Level 3</span>
<span>Level 4</span>
</div>
</div>
</div>

<FormError />
</>
)}
</FormFieldController>
<FormError />
</>
)}
</FormFieldController>
)}

<FormTextareaField
control={control}
Expand Down
10 changes: 6 additions & 4 deletions src/components/classes/edit/hooks/use-class-form-submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,15 @@ export function useClassFormSubmit({
deletedSchedules: deletedIds,
};

const isExercise = values.category.includes("Exercise");
const effectiveLevelRange = isExercise ? levelRange : null;
if (
!isEditing ||
initial.levelRange[0] !== levelRange[0] ||
initial.levelRange[1] !== levelRange[1]
initial.levelRange?.[0] !== effectiveLevelRange?.[0] ||
initial.levelRange?.[1] !== effectiveLevelRange?.[1]
) {
updatedValues.lowerLevel = levelRange[0]!;
updatedValues.upperLevel = levelRange[1]!;
updatedValues.lowerLevel = effectiveLevelRange?.[0] ?? null;
updatedValues.upperLevel = effectiveLevelRange?.[1] ?? null;
}

return updatedValues;
Expand Down
4 changes: 2 additions & 2 deletions src/components/classes/edit/hooks/use-class-upsert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export function useClassUpsert({
const createdId = await createClass({
termId,
name: payload.name!,
lowerLevel: payload.lowerLevel!,
upperLevel: payload.upperLevel!,
lowerLevel: payload.lowerLevel ?? null,
upperLevel: payload.upperLevel ?? null,
category: payload.category!,
subcategory: payload.subcategory ?? undefined,
image: payload.image ?? undefined,
Expand Down
17 changes: 12 additions & 5 deletions src/components/classes/edit/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@ export const ClassEditSchema = z
meetingURL: asNullishField(z.url("Please enter a valid meeting url.")),
category: z.string().nonempty("Please fill out this field."),
subcategory: asNullishField(z.string()),
levelRange: z.array(z.int().min(1).max(4)).length(2),
levelRange: z.array(z.int().min(1).max(4)).length(2).nullish(),
schedules: z.array(ScheduleEditSchema),
image: z.string().nullable(),
})
.refine((val) => val.levelRange[0]! <= val.levelRange[1]!, {
error: "The upper level must be greater than the lower level",
path: ["levelRange"],
});
.refine(
(val) => {
if (!val.levelRange) return true;
return val.levelRange[0]! <= val.levelRange[1]!;
},
{
error: "The upper level must be greater than the lower level",
path: ["levelRange"],
},
)
;
export type ClassEditSchemaType = z.infer<typeof ClassEditSchema>;
export type ClassEditSchemaInput = z.input<typeof ClassEditSchema>;
export type ClassEditSchemaOutput = z.output<typeof ClassEditSchema>;
2 changes: 1 addition & 1 deletion src/components/classes/edit/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function classToFormValues(
category: c?.category ?? "",
subcategory: c?.subcategory ?? "",
image: (config ? buildImageUrl(config, c?.image) : undefined) ?? null,
levelRange: [c?.lowerLevel ?? 1, c?.upperLevel ?? 4],
levelRange: c?.lowerLevel && c?.upperLevel ? [c.lowerLevel, c.upperLevel] : null,
schedules:
c?.schedules.map((s) => ({
id: s.id,
Expand Down
20 changes: 11 additions & 9 deletions src/components/classes/list/components/class-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,17 @@ export function ClassCard({
/>

<div className="flex flex-col items-start">
<TypographySmall className="text-primary-muted mb-1">
{classData.lowerLevel === classData.upperLevel ? (
<>Level {classData.lowerLevel}</>
) : (
<>
Level {classData.lowerLevel}-{classData.upperLevel}
</>
)}
</TypographySmall>
{classData.lowerLevel && classData.upperLevel ? (
<TypographySmall className="text-primary-muted mb-1">
{classData.lowerLevel === classData.upperLevel ? (
<>Level {classData.lowerLevel}</>
) : (
<>
Level {classData.lowerLevel}-{classData.upperLevel}
</>
)}
</TypographySmall>
) : null}

<TypographyRegBold className="mb-0.5">
{classData.name}
Expand Down
18 changes: 12 additions & 6 deletions src/models/api/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ export const CreateClass = z.object({
name: z.string().nonempty(),
description: z.string().optional(),
meetingURL : z.url().optional(),
lowerLevel: z.int().min(1).max(4),
upperLevel: z.int().min(1).max(4),
lowerLevel: z.int().min(1).max(4).nullish(),
upperLevel: z.int().min(1).max(4).nullish(),
category: z.string(),
subcategory: z.string().optional(),
schedules: z.array(CreateSchedule).default([]),
});
}).refine(
(val) => (val.lowerLevel == null) === (val.upperLevel == null),
{ message: "Both levels must be provided or both must be empty", path: ["lowerLevel"] },
);
export type CreateClassInput = z.input<typeof CreateClass>;
export type CreateClassOutput= z.output<typeof CreateClass>;

Expand All @@ -29,12 +32,15 @@ export const UpdateClass = z.object({
meetingURL: z.url().nullish(),
category: z.string().optional(),
subcategory: z.string().nullish(),
lowerLevel: z.int().optional(),
upperLevel: z.int().optional(),
lowerLevel: z.int().nullish(),
upperLevel: z.int().nullish(),
addedSchedules: z.array(CreateSchedule).default([]),
updatedSchedules: z.array(UpdateSchedule).default([]),
deletedSchedules: z.array(z.uuid()).default([]),
});
}).refine(
(val) => (val.lowerLevel == null) === (val.upperLevel == null),
{ message: "Both levels must be provided or both must be empty", path: ["lowerLevel"] },
);
export type UpdateClassInput = z.input<typeof UpdateClass>;
export type UpdateClassOutput = z.output<typeof UpdateClass>;

Expand Down
4 changes: 2 additions & 2 deletions src/models/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export type Class = {
meetingURL?: string;
category: string;
subcategory?: string;
lowerLevel: number;
upperLevel: number;
lowerLevel: number | null;
upperLevel: number | null;
schedules: Schedule[];
createdAt: Date;
updatedAt: Date;
Expand Down
8 changes: 8 additions & 0 deletions src/server/db/migrations/0008_uneven_eddie_brock.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ALTER TABLE "course" DROP CONSTRAINT "chk_lower_level_bounds";--> statement-breakpoint
ALTER TABLE "course" DROP CONSTRAINT "chk_upper_level_bounds";--> statement-breakpoint
ALTER TABLE "course" ALTER COLUMN "lower_level" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "course" ALTER COLUMN "upper_level" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "course" ADD CONSTRAINT "chk_levels_both_or_neither" CHECK (("course"."lower_level" IS NULL) = ("course"."upper_level" IS NULL));--> statement-breakpoint
ALTER TABLE "course" ADD CONSTRAINT "chk_lower_lte_upper" CHECK ("course"."lower_level" IS NULL OR "course"."lower_level" <= "course"."upper_level");--> statement-breakpoint
ALTER TABLE "course" ADD CONSTRAINT "chk_lower_level_bounds" CHECK ("course"."lower_level" IS NULL OR ("course"."lower_level" >= 1 AND "course"."lower_level" <= 4));--> statement-breakpoint
ALTER TABLE "course" ADD CONSTRAINT "chk_upper_level_bounds" CHECK ("course"."upper_level" IS NULL OR ("course"."upper_level" >= 1 AND "course"."upper_level" <= 4));
Loading