Skip to content

Commit 6533989

Browse files
committed
feat: switch between alternative and main courses
1 parent 723f37e commit 6533989

File tree

3 files changed

+105
-4
lines changed

3 files changed

+105
-4
lines changed

apps/web/src/modules/course-selection/CourseSelector.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ const CourseSelector = ({
5555
api.userCourseOfferings.removeUserCourseOffering,
5656
);
5757

58+
const swapWithAlternative = useMutation(
59+
api.userCourseOfferings.swapWithAlternative,
60+
);
61+
5862
const [hoveredSection, setHoveredSection] = useState<CourseOffering | null>(
5963
null,
6064
);
@@ -213,6 +217,18 @@ const CourseSelector = ({
213217
}
214218
};
215219

220+
const handleSwap = async (alternativeId: Id<"userCourseOfferings">) => {
221+
try {
222+
await swapWithAlternative({ alternativeId });
223+
} catch (error) {
224+
const errorMessage =
225+
error instanceof ConvexError
226+
? (error.data as string)
227+
: "Unexpected error occurred";
228+
toast.error(errorMessage);
229+
}
230+
};
231+
216232
const handleConflictAddAsMain = async () => {
217233
if (!conflictState?.course) return;
218234

@@ -286,6 +302,7 @@ const CourseSelector = ({
286302
course={selectedCourse}
287303
onClose={() => onCourseSelect?.(null)}
288304
onDelete={handleDelete}
305+
onSwap={handleSwap}
289306
/>
290307
</div>
291308
);

apps/web/src/modules/course-selection/components/course-detail-panel.tsx

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ interface CourseDetailPanelProps {
1919
title: string,
2020
alternativeOf?: Id<"userCourseOfferings">,
2121
) => void;
22+
onSwap?: (alternativeId: Id<"userCourseOfferings">) => void;
2223
}
2324

2425
export function CourseDetailPanel({
2526
course,
2627
onClose,
2728
onDelete,
29+
onSwap,
2830
}: CourseDetailPanelProps) {
2931
const alternatives = useQuery(
3032
api.userCourseOfferings.getAlternativeCourses,
@@ -36,6 +38,24 @@ export function CourseDetailPanel({
3638
: "skip",
3739
);
3840

41+
// Get the main course info if this course is an alternative
42+
const allUserCourses = useQuery(
43+
api.userCourseOfferings.getUserCourseOfferings,
44+
);
45+
const currentCourse = course?.userCourseOfferingId
46+
? allUserCourses?.find((c) => c._id === course.userCourseOfferingId)
47+
: null;
48+
49+
const alternativeOfId: Id<"userCourseOfferings"> | null = currentCourse
50+
? (currentCourse.alternativeOf as Id<"userCourseOfferings"> | null)
51+
: course?.alternativeOf
52+
? (course.alternativeOf as Id<"userCourseOfferings">)
53+
: null;
54+
55+
const mainCourse = alternativeOfId
56+
? allUserCourses?.find((c) => c._id === alternativeOfId)
57+
: null;
58+
3959
if (!course) return null;
4060

4161
const handleDelete = () => {
@@ -44,7 +64,7 @@ export function CourseDetailPanel({
4464
course.userCourseOfferingId as Id<"userCourseOfferings">,
4565
course.classNumber,
4666
course.title,
47-
course.alternativeOf as Id<"userCourseOfferings"> | undefined,
67+
alternativeOfId ?? undefined,
4868
);
4969
onClose();
5070
}
@@ -214,17 +234,17 @@ export function CourseDetailPanel({
214234
<Separator />
215235
<div className="space-y-3">
216236
<h4 className="text-sm font-semibold text-foreground">
217-
Alternative Courses
237+
Alternative Courses Added by You
218238
</h4>
219239
<div className="space-y-2">
220240
{alternatives.map((alt) => (
221241
<div
222242
key={alt._id}
223-
className="rounded-lg border p-3 space-y-2 hover:bg-accent/50 transition-colors"
243+
className="rounded-lg border p-3 space-y-2 hover:bg-accent/50 transition-colors group"
224244
>
225245
<div className="flex items-start justify-between gap-2">
226246
<div className="flex-1 min-w-0">
227-
<p className="text-sm font-medium break-words">
247+
<p className="text-sm font-medium break-words mb-1">
228248
{alt.courseOffering.courseCode} -{" "}
229249
{alt.courseOffering.title}
230250
</p>
@@ -249,12 +269,45 @@ export function CourseDetailPanel({
249269
{alt.courseOffering.startTime} -{" "}
250270
{alt.courseOffering.endTime}
251271
</div>
272+
<Button
273+
variant="outline"
274+
size="sm"
275+
className="w-full text-xs"
276+
onClick={() => onSwap?.(alt._id)}
277+
>
278+
Switch to this course
279+
</Button>
252280
</div>
253281
))}
254282
</div>
255283
</div>
256284
</>
257285
)}
286+
287+
{/* Alternative Course Info */}
288+
{mainCourse && (
289+
<div className="rounded-lg border p-4 space-y-3">
290+
<p className="text-sm text-muted-foreground">
291+
This course is an alternative of{" "}
292+
<span className="text-foreground">
293+
{mainCourse.courseOffering.title}
294+
</span>
295+
</p>
296+
<Button
297+
variant="outline"
298+
size="sm"
299+
className="w-full text-xs"
300+
onClick={() =>
301+
onSwap?.(
302+
course.userCourseOfferingId as Id<"userCourseOfferings">,
303+
)
304+
}
305+
>
306+
Replace with {mainCourse.courseOffering.courseCode} -
307+
{" Section "} {mainCourse.courseOffering.section}
308+
</Button>
309+
</div>
310+
)}
258311
</div>
259312
</ScrollArea>
260313

packages/server/convex/userCourseOfferings.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,37 @@ export const removeUserCourseOffering = protectedMutation({
211211
},
212212
});
213213

214+
export const swapWithAlternative = protectedMutation({
215+
args: {
216+
alternativeId: v.id("userCourseOfferings"),
217+
},
218+
handler: async (ctx, args) => {
219+
const alternative = await ctx.db.get(args.alternativeId);
220+
221+
if (!alternative || alternative.userId !== ctx.user.subject) {
222+
throw new ConvexError("Alternative course not found or unauthorized");
223+
}
224+
225+
if (!alternative.alternativeOf) {
226+
throw new ConvexError(
227+
"This course is not an alternative of another course",
228+
);
229+
}
230+
231+
const mainCourse = await ctx.db.get(alternative.alternativeOf);
232+
233+
if (!mainCourse || mainCourse.userId !== ctx.user.subject) {
234+
throw new ConvexError("Main course not found or unauthorized");
235+
}
236+
237+
// swap
238+
await ctx.db.patch(args.alternativeId, { alternativeOf: undefined });
239+
await ctx.db.patch(alternative.alternativeOf, {
240+
alternativeOf: args.alternativeId,
241+
});
242+
},
243+
});
244+
214245
export const getAlternativeCourses = protectedQuery({
215246
args: { userCourseOfferingId: v.id("userCourseOfferings") },
216247
handler: async (ctx, args) => {

0 commit comments

Comments
 (0)