Skip to content
Open
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
187 changes: 85 additions & 102 deletions src/components/scheduler/CalendarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect, useRef } from "react";
import { type SectionWithCourse } from "@/lib/scheduler/filters";
import { type CourseColor, getSectionColor } from "@/lib/scheduler/courseColors";
import { SectionDetailPopover } from "./SectionDetailPopover";

interface CalendarViewProps {
schedule: SectionWithCourse[];
Expand Down Expand Up @@ -31,28 +32,19 @@ function timeToMinutes(time: number): number {

export function CalendarView({ schedule, scheduleNumber, colorMap }: CalendarViewProps) {
const calendarRef = useRef<HTMLDivElement>(null);

// Define time range (6 AM to midnight)
const startHour = 6;
const endHour = 24;
const totalHours = endHour - startHour;

// Calculate minimum height based on hour height
const minCalendarHeight = totalHours * HOUR_HEIGHT;

// Separate async/remote courses from scheduled courses
const asyncCourses = schedule.filter(section =>
!section.meetingTimes ||
section.meetingTimes.length === 0 ||
section.meetingTimes.every(mt => !mt.days || mt.days.length === 0)
);

const scheduledCourses = schedule.filter(section =>
section.meetingTimes &&
section.meetingTimes.length > 0 &&
section.meetingTimes.some(mt => mt.days && mt.days.length > 0)
);

const asyncCourses = schedule.filter(section => section.meetingTimes.length === 0);
const scheduledCourses = schedule.filter(section => section.meetingTimes.length > 0);

// Auto-scroll to 7 AM on mount or when schedule changes
useEffect(() => {
if (calendarRef.current) {
Expand All @@ -61,7 +53,7 @@ export function CalendarView({ schedule, scheduleNumber, colorMap }: CalendarVie
calendarRef.current.scrollTop = scrollPosition;
}
}, [scheduleNumber, asyncCourses.length]);

// Define days
const days = [
{ short: "SUN", full: "SUNDAY", index: 0 },
Expand Down Expand Up @@ -95,7 +87,7 @@ export function CalendarView({ schedule, scheduleNumber, colorMap }: CalendarVie
};

return (
<div
<div
ref={calendarRef}
className="h-full w-full rounded-lg border border-gray-300 bg-white overflow-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"
>
Expand All @@ -111,7 +103,7 @@ export function CalendarView({ schedule, scheduleNumber, colorMap }: CalendarVie
</div>
)}
</div>

{/* Day Headers */}
{days.map((day) => (
<div key={day.index} className="h-12 flex items-center justify-center pb-1 bg-white">
Expand All @@ -130,111 +122,102 @@ export function CalendarView({ schedule, scheduleNumber, colorMap }: CalendarVie
{asyncCourses.map((section, index) => {
const sectionColor = getSectionColor(section, colorMap);
return (
<div
key={index}
className="rounded-md p-2 px-3 flex items-center gap-3 border"
style={{
backgroundColor: sectionColor?.fill,
borderColor: sectionColor?.stroke,
}}
>
<div className="text-base font-bold truncate text-neu8">
{section.courseSubject} {section.courseNumber}
</div>
<div className="text-base truncate text-neu6">
CRN {section.crn}
</div>
<div className="text-base truncate text-neu6 italic">
Asynchronous
<SectionDetailPopover key={index} section={section}>
<div
className="rounded-md p-2 px-3 flex items-center gap-3 border cursor-pointer hover:opacity-90 transition-opacity"
style={{
backgroundColor: sectionColor?.fill,
borderColor: sectionColor?.stroke,
}}
>
<div className="text-base font-bold truncate text-neu8">
{section.courseSubject} {section.courseNumber}
</div>
<div className="text-base truncate text-neu6">CRN {section.crn}</div>
<div className="text-base truncate text-neu6 italic">Asynchronous</div>
</div>
</div>
</SectionDetailPopover>
);
})}
</div>
</div>
)}

<div className="grid grid-cols-[65px_repeat(7,1fr)]" style={{ minHeight: `${minCalendarHeight}px` }}>
{/* Time Column */}
<div className="bg-white">
<div className="relative h-[calc(100%-3rem)] mt-2">
{timeSlots.map((time, index) => {
return (
<div
key={time}
className="absolute w-full flex items-start justify-end pr-2 text-sm text-neu6"
style={{ top: `${(index / totalHours) * 100}%`, height: `${(1 / totalHours) * 100}%` }}
>
<span className="-translate-y-1/2">{time}</span>
</div>
);
})}
</div>
</div>

{/* Day Columns */}
{days.map((day, idx) => (
<div key={day.index} className="relative bg-white">
{/* Time Grid Lines */}
<div className="grid grid-cols-[65px_repeat(7,1fr)]" style={{ minHeight: `${minCalendarHeight}px` }}>
{/* Time Column */}
<div className="bg-white">
<div className="relative h-[calc(100%-3rem)] mt-2">
{/* Top border for 12 AM */}
<div className="absolute w-full border-t border-gray-200" style={{ top: '0%' }} />

{timeSlots.map((_, index) => {
const isLastSlot = index === timeSlots.length - 1;
{timeSlots.map((time, index) => {
return (
<div
key={index}
className={`absolute w-full ${isLastSlot ? 'border-b border-transparent' : 'border-b border-gray-200'}`}
key={time}
className="absolute w-full flex items-start justify-end pr-2 text-sm text-neu6"
style={{ top: `${(index / totalHours) * 100}%`, height: `${(1 / totalHours) * 100}%` }}
/>
>
<span className="-translate-y-1/2">{time}</span>
</div>
);
})}
</div>
</div>

{/* Class Blocks */}
{scheduledCourses.map((section, sectionIndex) => {
const sectionColor = getSectionColor(section, colorMap);
return section.meetingTimes.map((meeting, meetingIndex) => {
if (!meeting.days.includes(day.index)) return null;

const position = getClassPosition(meeting.startTime, meeting.endTime);
{/* Day Columns */}
{days.map((day, _idx) => (
<div key={day.index} className="relative bg-white">
{/* Time Grid Lines */}
<div className="relative h-[calc(100%-3rem)] mt-2">
{/* Top border for 12 AM */}
<div className="absolute w-full border-t border-gray-200" style={{ top: '0%' }} />

{timeSlots.map((_, index) => {
const isLastSlot = index === timeSlots.length - 1;
return (
<div
key={`${sectionIndex}-${meetingIndex}`}
className="absolute w-[calc(100%-8px)] mx-1 rounded-md p-2 overflow-hidden border"
style={{
...position,
backgroundColor: sectionColor?.fill,
borderColor: sectionColor?.stroke,
}}
>
<div className="text-base font-bold truncate text-neu8">
{section.courseSubject} {section.courseNumber}
</div>
<div className="text-base truncate text-neu6">
{section.courseName}
</div>
{section.faculty && (
<div className="text-base truncate text-neu6">
{section.faculty}
</div>
)}
<div className="text-base truncate text-neu6">
CRN {section.crn}
</div>
<div className="text-base mt-1 text-neu6">
{formatTime(meeting.startTime)} - {formatTime(meeting.endTime)}
</div>
</div>
key={index}
className={`absolute w-full ${isLastSlot ? 'border-b border-transparent' : 'border-b border-gray-200'}`}
style={{ top: `${(index / totalHours) * 100}%`, height: `${(1 / totalHours) * 100}%` }}
/>
);
});
})}
})}

{/* Class Blocks */}
{scheduledCourses.map((section, sectionIndex) => {
const sectionColor = getSectionColor(section, colorMap);
return section.meetingTimes.map((meeting, meetingIndex) => {
if (!meeting.days.includes(day.index)) return null;

const position = getClassPosition(meeting.startTime, meeting.endTime);

return (
<SectionDetailPopover key={`${sectionIndex}-${meetingIndex}`} section={section}>
<div
className="absolute w-[calc(100%-8px)] mx-1 rounded-md p-2 overflow-hidden border cursor-pointer hover:opacity-90 transition-opacity"
style={{
...position,
backgroundColor: sectionColor?.fill,
borderColor: sectionColor?.stroke,
}}
>
<div className="text-base font-bold truncate text-neu8">
{section.courseSubject} {section.courseNumber}
</div>
<div className="text-base truncate text-neu6">{section.courseName}</div>
{section.faculty && <div className="text-base truncate text-neu6">{section.faculty}</div>}
<div className="text-base truncate text-neu6">CRN {section.crn}</div>
<div className="text-base mt-1 text-neu6">
{formatTime(meeting.startTime)} - {formatTime(meeting.endTime)}
</div>
</div>
</SectionDetailPopover>
);
});
})}
</div>
</div>
</div>
))}
))}
</div>
</div>
</div>
);
}

67 changes: 1 addition & 66 deletions src/components/scheduler/FilterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import { getCourseColorMap, getCourseKey } from "@/lib/scheduler/courseColors";
import { FilterMultiSelect } from "./FilterMultiSelect";
import { Switch } from "../ui/switch";
import { TimeInput } from "./TimeInput";
import { MoveRightIcon } from "lucide-react";
import FeedbackModal from "../feedback/FeedbackModal";

// Convert time string (e.g., "09:00") to military format (e.g., 900)
function timeStringToMilitary(timeStr: string): number {
Expand Down Expand Up @@ -55,8 +53,6 @@ export function FilterPanel({ filters, onFiltersChange, onGenerateSchedules, isG
}
};

const clearFilters = () => onFiltersChange({ includesOnline: true });

const handleGenerate = () => {
// Parse locked course IDs from input
const lockedCourseIds = lockedCourseIdsInput
Expand Down Expand Up @@ -113,67 +109,6 @@ export function FilterPanel({ filters, onFiltersChange, onGenerateSchedules, isG

<Separator />

<div className="flex items-center justify-between">
<h3 className="text-muted-foreground text-xs font-bold">FILTERS</h3>
<button
onClick={clearFilters}
className="rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
>
Clear All
</button>
</div>

<Separator />

{/* Classes Filter*/}
<div className="flex justify-between items-center">
<h3 className="text-muted-foreground text-xs font-bold">CLASSES</h3>
<button
onClick={() => {}}
aria-label="Edit classes"
title="Edit classes"
className="p-1 border border-transparent text-gray-600 rounded"
>
<Pencil className="w-4 h-4" />
</button>
</div>

<div>
{filteredSchedules && filteredSchedules.length > 0 && (
(() => {
// Build a map of course -> sections
const courseMap = new Map<string, Map<string, SectionWithCourse>>();
for (const schedule of filteredSchedules) {
for (const section of schedule) {
const courseKey = getCourseKey(section);
if (!courseMap.has(courseKey)) courseMap.set(courseKey, new Map());
const inner = courseMap.get(courseKey)!;
if (!inner.has(section.crn)) inner.set(section.crn, section);
}
}

// Sort courses alphabetically
const courseEntries = Array.from(courseMap.entries()).sort((a, b) => a[0].localeCompare(b[0]));

return (
<div className="mt-2">
{courseEntries.map(([courseKey, sectionsMap]) => (
<CourseBox
key={courseKey}
sections={Array.from(sectionsMap.values())}
color={colorMap.get(courseKey)}
/>
))}
</div>
);
})()
)}
</div>

<Separator />

<Separator />

{/* Classes Filter*/}
<div className="flex justify-between items-center">
<h3 className="text-muted-foreground text-xs font-bold">CLASSES</h3>
Expand Down Expand Up @@ -295,7 +230,7 @@ export function FilterPanel({ filters, onFiltersChange, onGenerateSchedules, isG
</div>

{/* Day number buttons */}
<div className="flex gap-2">
<div className="flex gap-2 justify-center">
{[1, 2, 3, 4, 5, 6].map((num) => (
<button
key={num}
Expand Down
Loading