cedarstalking with keyboard shortcuts
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: better schedule formatting and add boxes

+81 -7
+4
src/api.ts
··· 78 78 concentrations: { code: string; desc: string }[]; 79 79 advisors: { advisor: { id: string; name: string; email: string }; advisement: { type: string } }[]; 80 80 term: { key: string | null; description: string | null }; 81 + nonScheduledCourses: { code: string; title: string; methods: string }[]; 82 + }; 83 + person: { 84 + box: string | null; 81 85 }; 82 86 } 83 87
+77 -7
src/search-directory.tsx
··· 123 123 return `${hour > 12 ? hour - 12 : hour || 12}:${m} ${hour >= 12 ? "PM" : "AM"}`; 124 124 } 125 125 126 + const DAY_ORDER = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; 127 + const DAY_ABBR: Record<string, string> = { 128 + Monday: "M", Tuesday: "T", Wednesday: "W", Thursday: "Th", 129 + Friday: "F", Saturday: "Sa", Sunday: "Su", 130 + }; 131 + const TYPE_LABEL: Record<string, string> = { 132 + Lecture: "", 133 + Laboratory: "Lab", 134 + "Instructional Laboratory": "Lab", 135 + "Participation Course": "Participation", 136 + }; 137 + 138 + function buildScheduleText(items: ScheduleItem[], demo: boolean): string { 139 + // Collapse repeated per-day entries into one slot per unique time+type 140 + const slotMap = new Map<string, { item: ScheduleItem; days: string[] }>(); 141 + for (const item of items) { 142 + const key = `${item.title}|${item.startTime}|${item.endTime}|${item.type}`; 143 + if (!slotMap.has(key)) slotMap.set(key, { item, days: [] }); 144 + slotMap.get(key)!.days.push(item.day); 145 + } 146 + 147 + // Group slots by course title so lecture + lab appear under one heading 148 + const courseMap = new Map<string, { item: ScheduleItem; days: string[] }[]>(); 149 + for (const slot of slotMap.values()) { 150 + if (!courseMap.has(slot.item.title)) courseMap.set(slot.item.title, []); 151 + courseMap.get(slot.item.title)!.push(slot); 152 + } 153 + 154 + // Sort courses by earliest day of week 155 + const courses = [...courseMap.entries()].sort((a, b) => { 156 + const earliest = (slots: { days: string[] }[]) => 157 + Math.min(...slots.flatMap((s) => s.days.map((d) => DAY_ORDER.indexOf(d)))); 158 + return earliest(a[1]) - earliest(b[1]); 159 + }); 160 + 161 + return courses 162 + .map(([courseTitle, slots]) => { 163 + const course = demo ? "DEPT 000" : courseTitle; 164 + const desc = demo ? "Course Name" : slots[0].item.description; 165 + 166 + const sortedSlots = [...slots].sort((a, b) => { 167 + const aFirst = Math.min(...a.days.map((d) => DAY_ORDER.indexOf(d))); 168 + const bFirst = Math.min(...b.days.map((d) => DAY_ORDER.indexOf(d))); 169 + return aFirst - bFirst || a.item.startTime.localeCompare(b.item.startTime); 170 + }); 171 + 172 + const timeLines = sortedSlots.map(({ item, days }) => { 173 + const sortedDays = [...days].sort((a, b) => DAY_ORDER.indexOf(a) - DAY_ORDER.indexOf(b)); 174 + const dayStr = sortedDays.map((d) => DAY_ABBR[d] ?? d).join(""); 175 + const timeStr = `${formatTime(item.startTime)}–${formatTime(item.endTime)}`; 176 + const typeStr = TYPE_LABEL[item.type] ?? item.type; 177 + return `- ${dayStr} ${timeStr}${typeStr ? ` *(${typeStr})*` : ""}`; 178 + }); 179 + 180 + return `**${course}** — ${desc}\n${timeLines.join("\n")}`; 181 + }) 182 + .join("\n\n"); 183 + } 184 + 126 185 function PersonDetail({ 127 186 person, 128 187 photoPath, ··· 207 266 ? info.faculty.term?.description 208 267 : info?.student?.term?.description; 209 268 210 - if (scheduleItems.length) { 211 - md.push(`## Schedule${termDesc ? ` — ${termDesc}` : ""}`); 212 - for (const item of scheduleItems) { 213 - const title = demo ? "DEPT 000" : item.title; 214 - const desc = demo ? "Course Name" : item.description; 269 + const nonScheduled = info?.student?.isStudent ? (info.student.nonScheduledCourses ?? []) : []; 270 + 271 + if (scheduleItems.length || nonScheduled.length) { 272 + md.push(`## Schedule${termDesc ? ` — ${termDesc}` : ""}\n`); 273 + if (scheduleItems.length) md.push(buildScheduleText(scheduleItems, demo)); 274 + if (nonScheduled.length) { 275 + md.push("**Online / Unscheduled**"); 215 276 md.push( 216 - `**${title}** — ${desc} \n${item.day} ${formatTime(item.startTime)}–${formatTime(item.endTime)}`, 277 + nonScheduled 278 + .map((c) => `- ${demo ? "DEPT 000" : c.code} — ${demo ? "Course Name" : c.title} *(${c.methods})*`) 279 + .join("\n"), 217 280 ); 218 281 } 219 282 } ··· 286 349 )} 287 350 {!!(person.DormName || 288 351 person.OfficeBuildingName || 289 - person.OfficePhone) && <Detail.Metadata.Separator />} 352 + person.OfficePhone || 353 + info?.person?.box) && <Detail.Metadata.Separator />} 290 354 {person.DormName && ( 291 355 <Detail.Metadata.Label 292 356 title="Dorm" ··· 297 361 ? `${person.DormName}, Room ${person.DormRoom}` 298 362 : person.DormName 299 363 } 364 + /> 365 + )} 366 + {info?.person?.box && ( 367 + <Detail.Metadata.Label 368 + title="Box" 369 + text={demo ? "#0000" : `#${info.person.box}`} 300 370 /> 301 371 )} 302 372 {person.OfficeBuildingCode && (