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
11 changes: 9 additions & 2 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,15 @@ curl "https://api.brocktimetable.com/api/getNameList?timetableType=UG&session=FW

#### Example Response

```
[ABED 4F84 D2", "ABED 4F85 D3", "ABTE 8P85 D3", "ABTE 8P90 D3", ...]
```json
[
{
"label": "COSC 1P02 D2",
"courseCode": "COSC1P02",
"duration": "2",
"courseName": "Introduction to Computer Science"
}
]
```

### 2. Get Course Data
Expand Down
31 changes: 19 additions & 12 deletions src/components/generator/Calendar/CalendarComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,18 +179,25 @@ export default function CalendarComponent({
currentColors,
);

const courseDetails = newEvents
.filter((event) => event.description)
.map((event) => {
let titleArray = event.title.trim().split(" ");
return {
name: titleArray[0],
instructor: event.description,
section: titleArray.pop(),
startDate: event.startRecur,
endDate: event.endRecur,
};
});
const courseDetails = Array.from(
new Map(
newEvents
.filter((event) => !event.extendedProps.isBlocked)
.map((event) => {
let titleArray = event.title.trim().split(" ");
const detail = {
name: titleArray[0],
courseName: event.extendedProps.courseName || "",
instructor: event.description,
section: titleArray.pop(),
startDate: event.startRecur,
endDate: event.endRecur,
};

return [detail.name, detail];
}),
).values(),
);

setCourseDetails(courseDetails);
setEvents(newEvents);
Expand Down
31 changes: 19 additions & 12 deletions src/components/generator/Calendar/hooks/useCalendarEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,25 @@ export const useCalendarEvents = ({
currentColors,
);

const courseDetails = newEvents
.filter((event) => event.description)
.map((event) => {
let titleArray = event.title.trim().split(" ");
return {
name: titleArray[0],
instructor: event.description,
section: titleArray.pop(),
startDate: event.startRecur,
endDate: event.endRecur,
};
});
const courseDetails = Array.from(
new Map(
newEvents
.filter((event) => !event.extendedProps.isBlocked)
.map((event) => {
let titleArray = event.title.trim().split(" ");
const detail = {
name: titleArray[0],
courseName: event.extendedProps.courseName || "",
instructor: event.description,
section: titleArray.pop(),
startDate: event.startRecur,
endDate: event.endRecur,
};

return [detail.name, detail];
}),
).values(),
);

setCourseDetails(courseDetails);
setEvents(newEvents);
Expand Down
52 changes: 33 additions & 19 deletions src/components/generator/Calendar/utils/calendarUtils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ export const renderEventContent = (eventInfo, isMobile = false) => {
);
} else {
const isPinned = isEventPinned(eventInfo.event);
const courseNameText = eventInfo.event.extendedProps.courseName?.trim();
const instructorText = eventInfo.event.extendedProps.description?.trim();
const shouldShowCourseName = Boolean(courseNameText);
const shouldShowInstructor =
!!instructorText && instructorText.toLowerCase() !== "no instructor";

Expand Down Expand Up @@ -89,27 +91,39 @@ export const renderEventContent = (eventInfo, isMobile = false) => {
</div>
)}
<div style={{ position: "relative", zIndex: 1 }}>
<b>{eventInfo.timeText}</b>
<br />
<span>{eventInfo.event.title}</span>
<b style={{ display: "block" }}>{eventInfo.timeText}</b>
<span style={{ display: "block" }}>{eventInfo.event.title}</span>
{(isMobile || eventDuration >= minDurationToShowTime) &&
shouldShowCourseName && (
<span
style={{
display: "block",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: "100%",
lineHeight: "1.2",
}}
title={courseNameText}
>
{courseNameText}
</span>
)}
{(isMobile || eventDuration >= minDurationToShowTime) &&
shouldShowInstructor && (
<>
<br />
<span
style={{
display: "block",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: "100%",
lineHeight: "1.2",
}}
title={instructorText}
>
{instructorText}
</span>
</>
<span
style={{
display: "block",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: "100%",
lineHeight: "1.2",
}}
title={instructorText}
>
{instructorText}
</span>
)}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,16 @@ export default function CourseListComponent({
<Card className="transition-none hover:bg-muted/40">
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer flex flex-row items-center justify-between space-y-0 py-3">
<CardTitle className="text-base font-normal uppercase">
{course}
</CardTitle>
<div className="min-w-0 pr-3">
<CardTitle className="text-base font-normal uppercase">
{course}
</CardTitle>
{courseDetail?.courseName && (
<p className="truncate text-sm text-muted-foreground">
{courseDetail.courseName}
</p>
)}
</div>
<div className="flex items-center gap-2">
<div
className="relative flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border border-border hover:opacity-80"
Expand Down Expand Up @@ -126,6 +133,12 @@ export default function CourseListComponent({
<CollapsibleContent className="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
<CardContent className="pt-0">
<div className="grid gap-3 text-sm text-muted-foreground">
<div className="flex items-start justify-between gap-4">
<span>Course Name</span>
<span className="text-right text-foreground">
{courseDetail?.courseName || "N/A"}
</span>
</div>
<div className="flex items-start justify-between">
<span>Course Instructor</span>
<span className="text-foreground">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,33 @@ export default function CourseSearchComponent({
const listRef = React.useRef(null);
const PAGE_SIZE = 200;

const normalizedOptions = React.useMemo(() => {
return courseOptions.map((option) => {
if (typeof option === "string") {
return {
label: option,
courseCode: option.replace(/\s+D\d+$/i, "").replace(/\s+/g, ""),
duration: option.match(/D(\d+)$/i)?.[1] || "",
courseName: "",
};
}

const label =
option.label ||
option.value ||
`${option.courseCode?.slice(0, 4) || ""} ${
option.courseCode?.slice(4) || ""
}${option.duration ? ` D${option.duration}` : ""}`.trim();

return {
label,
courseCode: option.courseCode || "",
duration: option.duration || "",
courseName: option.courseName || "",
};
});
}, [courseOptions]);

React.useEffect(() => {
setDisplayLimit(PAGE_SIZE);
}, [value]);
Expand All @@ -51,30 +78,49 @@ export default function CourseSearchComponent({
const selectedItem = listEl?.querySelector('[data-selected="true"]');
if (selectedItem) return;
event.preventDefault();
if (filteredOptions.length > 0) {
handleSelectOption(filteredOptions[0]);
return;
}
onEnterPress(value);
setOpen(false);
};

const filteredOptions = React.useMemo(() => {
const query = (value || "").trim().toUpperCase();
const results = [];
for (let i = 0; i < courseOptions.length; i += 1) {
const option = courseOptions[i];
if (!query || option.toUpperCase().startsWith(query)) {
for (let i = 0; i < normalizedOptions.length; i += 1) {
const option = normalizedOptions[i];
const label = option.label.toUpperCase();
const courseName = option.courseName.toUpperCase();
const matchesQuery =
!query ||
label.startsWith(query) ||
label.includes(query) ||
courseName.includes(query);

if (matchesQuery) {
results.push(option);
if (results.length >= displayLimit) {
break;
}
}
}
return results;
}, [courseOptions, value, displayLimit]);
}, [normalizedOptions, value, displayLimit]);

const handleSelectOption = (option) => {
setValue(option.label);
onCourseCodeChange(null, option.label);
setOpen(false);
onEnterPress(option.label);
};

const groupedOptions = React.useMemo(() => {
const groups = new Map();
for (let i = 0; i < filteredOptions.length; i += 1) {
const option = filteredOptions[i];
const key = option.slice(0, 4).toUpperCase();
const key = option.label.slice(0, 4).toUpperCase();
if (!groups.has(key)) {
groups.set(key, []);
}
Expand All @@ -94,6 +140,7 @@ export default function CourseSearchComponent({
variant="outline"
role="combobox"
aria-expanded={open}
aria-controls="course-search-list"
className="w-full justify-between transition-none"
>
{value ? value : "Add a course"}
Expand All @@ -112,28 +159,36 @@ export default function CourseSearchComponent({
}}
onKeyDown={handleInputKeyDown}
/>
<CommandList onScroll={handleListScroll} ref={listRef}>
<CommandList
id="course-search-list"
onScroll={handleListScroll}
ref={listRef}
>
<CommandEmpty>No course found.</CommandEmpty>
{groupedOptions.map((group) => (
<CommandGroup key={group.group} heading={group.group}>
{group.items.map((option) => (
<CommandItem
key={option}
value={option}
onSelect={(currentValue) => {
setValue(currentValue);
onCourseCodeChange(null, currentValue);
setOpen(false);
onEnterPress(currentValue);
key={option.label}
value={`${option.label} ${option.courseName}`.trim()}
onSelect={() => {
handleSelectOption(option);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
option === value ? "opacity-100" : "opacity-0",
option.label === value ? "opacity-100" : "opacity-0",
)}
/>
{option}
<div className="min-w-0">
<div className="truncate">{option.label}</div>
{option.courseName && (
<div className="truncate text-xs text-muted-foreground">
{option.courseName}
</div>
)}
</div>
</CommandItem>
))}
</CommandGroup>
Expand Down
4 changes: 3 additions & 1 deletion src/lib/generator/courseData.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ const initializeCourseData = (courseCode) => {
};

export const storeCourseData = (course) => {
const { courseCode, sections, labs, tutorials, seminars } = course;
const { courseCode, courseName, sections, labs, tutorials, seminars } =
course;

initializeCourseData(courseCode);

courseData[courseCode].courseCode = courseCode;
courseData[courseCode].courseName = courseName || "";
courseData[courseCode].sections.push(...sections);
replaceSectionId(courseData[courseCode].sections);
courseData[courseCode].labs.push(...labs);
Expand Down
1 change: 1 addition & 0 deletions src/lib/generator/createCalendarEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const createCalendarEvents = (
description: component.instructor,
color: customColor,
extendedProps: {
courseName: course.courseName || "",
isPinned: component.pinned,
isMain: component.isMain ?? false,
},
Expand Down
10 changes: 9 additions & 1 deletion src/lib/generator/timetableGeneration/utils/combinationUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,18 @@ export const generateSingleCourseCombinations = (course, timeSlots) => {
]);

combinations.forEach(([lab, tutorial, seminar]) => {
singleCourseCombinations.push({
const courseCombination = {
courseCode: course.courseCode,
mainComponents: mainComponentGroup,
secondaryComponents: { lab, tutorial, seminar },
};

if (course.courseName) {
courseCombination.courseName = course.courseName;
}

singleCourseCombinations.push({
...courseCombination,
});
});
});
Expand Down
Loading