Skip to content

Commit 7ac1de1

Browse files
authored
half-semester support (#91)
1 parent dd217b1 commit 7ac1de1

File tree

2 files changed

+424
-273
lines changed

2 files changed

+424
-273
lines changed

client/src/routes/calendar/+page.svelte

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,80 @@
202202
{ key: 'sunday', label: 'Sunday', abbr: 'Su', order: 6 }
203203
];
204204
205+
type PositionedMeeting = {
206+
course: Course;
207+
meeting: MeetingTime;
208+
startOffset: number;
209+
width: number;
210+
startTotal: number;
211+
endTotal: number;
212+
bgColor: string;
213+
textColor: string;
214+
stackIndex: number;
215+
overlapCount: number;
216+
};
217+
218+
const stackGapPct = 2;
219+
220+
let stackedMeetings = $derived.by(() => {
221+
if (!processedData) return { byDay: {}, maxStacksByDay: {} };
222+
const byDay: Record<string, PositionedMeeting[]> = {};
223+
const maxStacksByDay: Record<string, number> = {};
224+
for (const { key } of dayOrder) {
225+
byDay[key] = [];
226+
maxStacksByDay[key] = 1;
227+
}
228+
for (const course of processedData) {
229+
const isLab = course.schedule_type.toLowerCase() === 'laboratory';
230+
const bgColorBase = isLab ? labColor : lectureColor;
231+
for (const meeting of course.meeting_times) {
232+
for (const { key } of dayOrder) {
233+
if (!meeting[key as keyof MeetingTime]) continue;
234+
const startHour = parseInt(meeting.begin_time.split(':')[0]);
235+
const startMin = parseInt(meeting.begin_time.split(':')[1]);
236+
const endHour = parseInt(meeting.end_time.split(':')[0]);
237+
const endMin = parseInt(meeting.end_time.split(':')[1]);
238+
const startTotal = startHour * 60 + startMin;
239+
const endTotal = endHour * 60 + endMin;
240+
const startOffset = ((startHour - 8) * 60 + startMin) / 60 * 8;
241+
const width = (endTotal - startTotal) / 60 * 8;
242+
const bgColor = meeting.color ?? bgColorBase;
243+
const textColor = getTextColor(bgColor);
244+
byDay[key].push({ course, meeting, startOffset, width, startTotal, endTotal, bgColor, textColor, stackIndex: 0, overlapCount: 1 });
245+
}
246+
}
247+
}
248+
for (const { key } of dayOrder) {
249+
const arr = byDay[key];
250+
arr.sort((a, b) => (a.startTotal === b.startTotal ? a.endTotal - b.endTotal : a.startTotal - b.startTotal));
251+
const stackEnds: number[] = [];
252+
const active: PositionedMeeting[] = [];
253+
for (const item of arr) {
254+
for (let i = active.length - 1; i >= 0; i--) {
255+
if (item.startTotal >= active[i].endTotal) {
256+
active.splice(i, 1);
257+
}
258+
}
259+
const currentOverlap = active.length + 1;
260+
for (const a of active) {
261+
a.overlapCount = Math.max(a.overlapCount, currentOverlap);
262+
}
263+
item.overlapCount = currentOverlap;
264+
let stack = stackEnds.findIndex((end) => item.startTotal >= end);
265+
if (stack === -1) {
266+
stack = stackEnds.length;
267+
stackEnds.push(item.endTotal);
268+
} else {
269+
stackEnds[stack] = item.endTotal;
270+
}
271+
item.stackIndex = stack;
272+
active.push(item);
273+
}
274+
maxStacksByDay[key] = Math.max(...arr.map((m) => m.overlapCount), 1);
275+
}
276+
return { byDay, maxStacksByDay };
277+
});
278+
205279
function getLatestEndHour(courses: Course[]): number {
206280
let latestHour = 8;
207281
@@ -957,6 +1031,7 @@
9571031
</div>
9581032

9591033
{#each dayOrder.slice(0, 5) as day}
1034+
{@const dayEvents = stackedMeetings.byDay?.[day.key] ?? []}
9601035
<div class="flex flex-row flex-1 min-h-[120px] border-b border-outline-variant relative">
9611036
<div class="w-24 border-r border-outline-variant flex items-center justify-center bg-surface-container-low left-0 z-5">
9621037
<span class="font-medium text-sm">{day.label}</span>
@@ -967,32 +1042,19 @@
9671042
<div class="w-32 border-r border-outline-variant"></div>
9681043
{/each}
9691044

970-
{#each processedData as course}
971-
{#each course.meeting_times as meeting}
972-
{#if meeting[day.key as keyof MeetingTime]}
973-
{@const startHour = parseInt(meeting.begin_time.split(':')[0])}
974-
{@const startMin = parseInt(meeting.begin_time.split(':')[1])}
975-
{@const endHour = parseInt(meeting.end_time.split(':')[0])}
976-
{@const endMin = parseInt(meeting.end_time.split(':')[1])}
977-
{@const startOffset = ((startHour - 8) * 60 + startMin) / 60 * 8}
978-
{@const width = ((endHour * 60 + endMin) - (startHour * 60 + startMin)) / 60 * 8}
979-
{@const isLab = course.schedule_type.toLowerCase() === 'laboratory'}
980-
{@const bgColorBase = isLab ? labColor : lectureColor}
981-
{@const bgColor = meeting.color ?? bgColorBase}
982-
{@const textColor = getTextColor(bgColor)}
983-
984-
985-
<button
986-
class="absolute top-1 bottom-1 rounded px-2 py-1 text-xs overflow-hidden cursor-pointer hover:shadow-md transition-shadow border-t-2"
987-
style="background-color: {bgColor}; color: {textColor}; left: {startOffset}rem; width: {width}rem; border-color: {bgColor};"
988-
onclick={() => {activeCourse = course; activeMeeting = meeting; activeDay = day; getEventPerfs(meeting.id)}}
989-
>
990-
<div class="font-medium truncate">{meeting.title_overrides?.[day.key] ?? course.title}</div>
991-
<div class="opacity-80">{convertTo12Hour(meeting.begin_time)} - {convertTo12Hour(meeting.end_time)}</div>
992-
<div class="opacity-70 text-[10px]">{meeting.location.building.abbreviation} {meeting.location.room}</div>
993-
</button>
994-
{/if}
995-
{/each}
1045+
{#each dayEvents as item (item.meeting.id)}
1046+
{@const overlapCount = Math.max(item.overlapCount ?? 1, 1)}
1047+
{@const heightPct = Math.max((100 - (overlapCount + 1) * stackGapPct) / overlapCount, 0)}
1048+
{@const topPct = stackGapPct + item.stackIndex * (heightPct + stackGapPct)}
1049+
<button
1050+
class="absolute rounded px-2 py-1 text-xs overflow-hidden cursor-pointer hover:shadow-md transition-shadow border-t-2"
1051+
style={`background-color:${item.bgColor}; color:${item.textColor}; left:${item.startOffset}rem; width:${item.width}rem; top:${topPct}%; height:${heightPct}%; border-color:${item.bgColor};`}
1052+
onclick={() => {activeCourse = item.course; activeMeeting = item.meeting; activeDay = day; getEventPerfs(item.meeting.id)}}
1053+
>
1054+
<div class="font-medium truncate">{item.meeting.title_overrides?.[day.key] ?? item.course.title}</div>
1055+
<div class="opacity-80">{convertTo12Hour(item.meeting.begin_time)} - {convertTo12Hour(item.meeting.end_time)}</div>
1056+
<div class="opacity-70 text-[10px]">{item.meeting.location.building.abbreviation} {item.meeting.location.room}</div>
1057+
</button>
9961058
{/each}
9971059
</div>
9981060
</div>

0 commit comments

Comments
 (0)