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
9 changes: 9 additions & 0 deletions app/dev-grid.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /><title>grid test</title>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;600;700&display=swap" rel="stylesheet" />
</head>
<body><div id="root" style="max-width: 1100px; margin: 40px auto"></div>
<script type="module" src="/src/client/dev-grid.tsx"></script>
</body>
</html>
19 changes: 17 additions & 2 deletions app/src/client/components/ToolCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,23 @@ export function ToolRender({ view }: { view: ToolView }) {
<div className="course-rows">
{shown.map((c, i) => (
<div key={i} className="course-row">
<span className="code">{c.code}</span>
<span className="ctitle">{c.title}</span>
{c.pcUrl ? (
<a
className="course-link"
href={c.pcUrl}
target="_blank"
rel="noreferrer"
title="Ratings & reviews on PrincetonCourses ↗"
>
<span className="code">{c.code}</span>
<span className="ctitle">{c.title}</span>
</a>
) : (
<>
<span className="code">{c.code}</span>
<span className="ctitle">{c.title}</span>
</>
)}
{c.meta && <span className="meta">{c.meta}</span>}
{c.rating != null && (
<span className="rating-chip">{c.rating.toFixed(2)}</span>
Expand Down
21 changes: 13 additions & 8 deletions app/src/client/components/WeekGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meeting, ScheduleView } from "../lib/tools";
import { tjColor } from "../lib/tools";
import { fmtRange, tjColor } from "../lib/tools";

const DAY_COLUMNS = [
["Monday", "M"],
Expand All @@ -9,7 +9,8 @@ const DAY_COLUMNS = [
["Friday", "F"],
] as const;

const DAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri"];
const DAY_LABELS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"];
const DAY_LABELS_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri"];

const START = 8 * 60; // 8:00 AM

Expand Down Expand Up @@ -81,7 +82,12 @@ export function WeekGrid({
<div>
<div
className={compact ? "week compact" : "week"}
style={{ "--rows": hours } as React.CSSProperties}
style={
{
"--rows": hours,
"--week-h": compact ? `${hours * 26}px` : `${hours * 60}px`,
} as React.CSSProperties
}
>
<div className="axis" aria-hidden>
<div className="day-name">&nbsp;</div>
Expand All @@ -100,7 +106,7 @@ export function WeekGrid({
</div>
{DAY_COLUMNS.map(([full, short], di) => (
<div key={full} className="day">
<div className="day-name">{DAY_LABELS[di]}</div>
<div className="day-name">{compact ? DAY_LABELS_SHORT[di] : DAY_LABELS[di]}</div>
<div className="day-col">
{layoutDay(
schedule.meetings.filter((m) => onDay(m, full, short))
Expand Down Expand Up @@ -135,17 +141,16 @@ export function WeekGrid({
}}
title={`${m.courseCode} ${m.label} · ${m.startLabel}–${m.endLabel}${m.room ? ` · ${m.room}` : ""}${m.confirmed ? "" : " · option — pick in TigerJunction"}`}
>
{!compact && duration >= 60 && cols <= 2 && (
{!compact && duration >= 45 && cols <= 2 && (
<div className="btime">
{m.startLabel.replace(" ", "")}–
{m.endLabel.replace(" ", "")}
{fmtRange(m.startMin, m.endMin)}
</div>
)}
<div className="bcode">
{m.courseCode}
{!compact && cols <= 3 ? ` ${m.label}` : ""}
</div>
{!compact && duration >= 80 && cols === 1 && m.room && (
{!compact && duration >= 75 && cols === 1 && m.room && (
<div className="bwhere">{m.room}</div>
)}
</div>
Expand Down
10 changes: 10 additions & 0 deletions app/src/client/dev-grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createRoot } from "react-dom/client";
import { WeekGrid } from "./components/WeekGrid";
import { extractSchedule } from "./lib/tools";
import fixture from "./real-fixture.json";
import "./styles.css";

const view = extractSchedule(fixture)!;
createRoot(document.getElementById("root")!).render(
<div className="paper-card"><WeekGrid schedule={view} /></div>
);
75 changes: 73 additions & 2 deletions app/src/client/lib/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,36 @@ export function tjColor(index: number, darken = 0): string {

export function parseTime(label: unknown): number | null {
if (typeof label !== "string") return null;
const m = label.trim().match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
// Fractional minutes appear when the engine mis-decodes TigerJunction's
// 10-minute time units (see decodeCompressedTimes below).
const m = label.trim().match(/^(\d{1,2}):(\d{2}(?:\.\d+)?)\s*(AM|PM)$/i);
if (!m) return null;
let hour = parseInt(m[1], 10);
const minute = parseInt(m[2], 10);
const minute = parseFloat(m[2]);
const ampm = m[3].toUpperCase();
if (ampm === "PM" && hour !== 12) hour += 12;
if (ampm === "AM" && hour === 12) hour = 0;
return hour * 60 + minute;
}

/** Minutes-since-midnight → "1:05 PM". */
export function fmtTime(min: number): string {
const h24 = Math.floor(min / 60);
const m = Math.round(min % 60);
const h12 = h24 % 12 || 12;
return `${h12}:${String(m).padStart(2, "0")} ${h24 >= 12 ? "PM" : "AM"}`;
}

/** Minutes → TJ-style in-block range label, "12:15-1:05". */
export function fmtRange(start: number, end: number): string {
const short = (min: number) => {
const h24 = Math.floor(min / 60);
const m = Math.round(min % 60);
return `${h24 % 12 || 12}:${String(m).padStart(2, "0")}`;
};
return `${short(start)}-${short(end)}`;
}

/**
* Recognizes get_schedule_details / verify_schedule–shaped payloads.
*
Expand Down Expand Up @@ -218,6 +238,33 @@ export function extractSchedule(data: unknown): ScheduleView | null {
}
if (raw.length === 0) return null;

// The deployed engine renders TigerJunction's Supabase times as if the
// stored value were minutes-past-8am, but junction actually stores
// 10-minute units (web convert.ts: hour = value/6 + 8). The tell: every
// "time" lands before 10 AM, often with fractional minutes. Undo it:
// real = (mislabeled - 480) * 10 + 480. Harmless once the engine is
// fixed, because genuine schedules never trip the detector.
const timed = raw.filter((r) => r.startMin != null);
const compressed =
timed.length > 0 &&
timed.every(
(r) => r.startMin! < 600 && (r.endMin == null || r.endMin < 600)
);
if (compressed) {
for (const r of timed) {
r.startMin = Math.round((r.startMin! - 480) * 10 + 480);
if (r.endMin != null) r.endMin = Math.round((r.endMin - 480) * 10 + 480);
r.startLabel = fmtTime(r.startMin);
if (r.endMin != null) r.endLabel = fmtTime(r.endMin);
}
} else {
// Normalize any fractional minutes either way.
for (const r of timed) {
r.startMin = Math.round(r.startMin!);
if (r.endMin != null) r.endMin = Math.round(r.endMin);
}
}

// One palette color per course, in order of first appearance.
const colorOf = new Map<string, number>();
for (const r of raw) {
Expand Down Expand Up @@ -342,8 +389,31 @@ export type CourseRowData = {
status?: string;
rating?: number | null;
meta?: string;
/** Deep link to this offering on PrincetonCourses, when derivable. */
pcUrl?: string;
};

const PC_HOME = PI_APPS.find((a) => a.key === "princetoncourses")!.home;

/**
* PrincetonCourses keys courses by registrar guid = term + courseID
* (e.g. 1264002051). The engine hands us "002051-1264" ids or
* listingId + term pairs.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function pcCourseUrl(c: any): string | undefined {
if (typeof c.id === "string") {
const m = c.id.match(/^(\d{6})-(\d{4})$/);
if (m) return `${PC_HOME}/course/${m[2]}${m[1]}`;
}
const listing = c.listingId ?? c.listing_id;
const term = typeof c.term === "number" ? c.term : undefined;
if (typeof listing === "string" && /^\d{6}$/.test(listing) && term) {
return `${PC_HOME}/course/${term}${listing}`;
}
return undefined;
}

export function extractCourses(data: unknown): CourseRowData[] | null {
if (data == null || typeof data !== "object") return null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -379,6 +449,7 @@ export function extractCourses(data: unknown): CourseRowData[] | null {
status: c.status ? String(c.status) : undefined,
rating,
meta: bits.join(" · ") || undefined,
pcUrl: pcCourseUrl(c),
});
}
return rows.length > 0 ? rows : null;
Expand Down
3 changes: 3 additions & 0 deletions app/src/client/pages/AppsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export function AppsPage({
if (err) return { text: `couldn't connect — ${err}`, cls: "status err" };
const server = desk.mcp?.servers?.[key];
if (!settings.apps.includes(key)) return { text: "off", cls: "status" };
if (key === "princetoncourses" && settings.apps.includes("junction")) {
return { text: "covered by TigerJunction", cls: "status on" };
}
if (server?.state === "ready" || server?.state === "connected") {
return { text: "connected", cls: "status on" };
}
Expand Down
Loading
Loading