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
19 changes: 19 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
serverExternalPackages: ["@node-rs/argon2"],
images: {
remotePatterns: [
{
protocol: "https",
hostname: "s3.amazonaws.com",
pathname: "/libapps/**",
},
{
protocol: "https",
hostname: "libapps.s3.amazonaws.com",
pathname: "/accounts/**",
},
{
protocol: "https",
hostname: "d2jv02qf7xgjwx.cloudfront.net",
pathname: "/accounts/**",
},
],
},
async rewrites() {
return [
{
Expand Down
137 changes: 137 additions & 0 deletions scripts/fetch-room-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env tsx
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const ANTEATER_API = "https://anteaterapi.com/v2/rest/studyRooms";
const LIBCAL_BASES = [
"https://spaces.lib.uci.edu/space",
"https://scheduler.oit.uci.edu/space",
];
function weekdaysAround(anchor: Date, back: number, forward: number): string[] {
const start = new Date(anchor);
start.setDate(start.getDate() - back);
const end = new Date(anchor);
end.setDate(end.getDate() + forward);
const dates: string[] = [];
const cursor = new Date(start);
while (cursor <= end) {
const day = cursor.getDay();
if (day !== 0 && day !== 6) {
const yyyy = cursor.getFullYear();
const mm = String(cursor.getMonth() + 1).padStart(2, "0");
const dd = String(cursor.getDate()).padStart(2, "0");
dates.push(`${yyyy}-${mm}-${dd}`);
}
cursor.setDate(cursor.getDate() + 1);
}
return dates;
}

async function fetchRoomsForDate(
date: string,
): Promise<Array<{ id: string; name: string; location: string }> | null> {
try {
const url = `${ANTEATER_API}?dates=${date}&times=07:00-23:00`;
const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
if (!res.ok) return null;
const json = await res.json();
return json.data ?? [];
} catch {
return null;
}
}

async function fetchRoomImage(roomId: string): Promise<string | null> {
for (const base of LIBCAL_BASES) {
try {
const res = await fetch(`${base}/${roomId}`, {
headers: { "User-Agent": "Mozilla/5.0 (compatible; ZotMeet/1.0)" },
signal: AbortSignal.timeout(8000),
});
if (!res.ok) continue;
const html = await res.text();
const match = html.match(
/(?:https?:)?\/\/(?:s3\.amazonaws\.com\/libapps|libapps\.s3\.amazonaws\.com|[a-z0-9]+\.cloudfront\.net\/accounts)\/[^"'\s]+\.(?:jpg|jpeg|png|webp)/i,
);
if (match?.[0]) {
const url = match[0].startsWith("//") ? `https:${match[0]}` : match[0];
return url;
}
} catch {}
}
return null;
}

async function main() {
const dates = weekdaysAround(new Date(), 7, 30);
console.log(
`Scanning ${dates.length} weekdays (${dates[0]} → ${dates.at(-1)}) for rooms...\n`,
);

const roomMap = new Map<
string,
{ id: string; name: string; location: string }
>();

let emptyStreak = 0;
for (const date of dates) {
process.stdout.write(` ${date} ... `);
const rooms = await fetchRoomsForDate(date);
if (rooms === null) {
console.log("fetch failed, skipping");
continue;
}
const before = roomMap.size;
for (const r of rooms) roomMap.set(r.id, r);
const added = roomMap.size - before;
console.log(`+${added} new (${roomMap.size} total)`);
emptyStreak = added === 0 ? emptyStreak + 1 : 0;
if (emptyStreak >= 3) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
console.log(" No new rooms for 3 consecutive days — stopping early.\n");
break;
}
}

const rooms = [...roomMap.values()];
console.log(
`\nDiscovered ${rooms.length} unique rooms. Fetching images...\n`,
);

const imageMap: Record<string, string> = {};

for (const room of rooms) {
process.stdout.write(` ${room.location} / ${room.name ?? room.id} ... `);
const url = await fetchRoomImage(room.id);
if (url) {
imageMap[room.id] = url;
process.stdout.write("✓\n");
} else {
process.stdout.write("✗\n");
}
await new Promise((r) => setTimeout(r, 150));
}

const found = Object.keys(imageMap).length;
const lines = [
"// Auto-generated by scripts/fetch-room-images.ts",
`// Run \`pnpm tsx scripts/fetch-room-images.ts\` to regenerate (${found}/${rooms.length} rooms have images)`,
"export const ROOM_IMAGES: Record<string, string> = {",
...Object.entries(imageMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([id, imgUrl]) => `\t"${id}": "${imgUrl}",`),
"};",
"",
].join("\n");

const outPath = path.resolve(__dirname, "../src/lib/rooms/room-images.ts");
await fs.writeFile(outPath, lines, "utf-8");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unconditional file write can clobber committed image data with empty/partial output on transient network failures. Add a guard to fail fast before writing when results are unexpectedly empty.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/fetch-room-images.ts, line 130:

<comment>Unconditional file write can clobber committed image data with empty/partial output on transient network failures. Add a guard to fail fast before writing when results are unexpectedly empty.</comment>

<file context>
@@ -0,0 +1,137 @@
+	].join("\n");
+
+	const outPath = path.resolve(__dirname, "../src/lib/rooms/room-images.ts");
+	await fs.writeFile(outPath, lines, "utf-8");
+	console.log(`\nWrote ${found} image entries → src/lib/rooms/room-images.ts`);
+}
</file context>

console.log(`\nWrote ${found} image entries → src/lib/rooms/room-images.ts`);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
40 changes: 33 additions & 7 deletions src/components/studyrooms/heatmap/rooms-heatmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
Tooltip,
} from "@mui/material";
import Box from "@mui/material/Box";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useMemo, useRef, useState } from "react";
import { ROOM_IMAGES } from "@/lib/rooms/room-images";
import {
buildHalfHourIntervals,
formatISOToLocalTime,
Expand All @@ -37,19 +39,17 @@ interface RoomsHeatmapProps {

type SortKey = "default" | "capacity" | "availability";

const LOCATION_SORT_ORDER: readonly string[] = [
const LOCATION_SORT_ORDER = [
"Science Library",
...BUILDINGS.filter((b) => b !== "Science Library"),
];

function compareLocationSort(a: string, b: string): number {
const ia = LOCATION_SORT_ORDER.indexOf(a);
const ib = LOCATION_SORT_ORDER.indexOf(b);
const aKnown = ia >= 0;
const bKnown = ib >= 0;
if (aKnown && bKnown) return ia - ib;
if (aKnown && !bKnown) return -1;
if (!aKnown && bKnown) return 1;
if (ia >= 0 && ib >= 0) return ia - ib;
if (ia >= 0) return -1;
if (ib >= 0) return 1;
return a.localeCompare(b);
}

Expand Down Expand Up @@ -217,7 +217,33 @@ export const RoomsHeatmap = ({
>
<Tooltip
placement="top"
title={`${formatISOToLocalTime(s.start)} - ${formatISOToLocalTime(s.end)}`}
title={
<div className="w-44 space-y-1 p-1">
{ROOM_IMAGES[room.id] && (
<Image
src={ROOM_IMAGES[room.id]}
alt={room.name ?? "Room"}
width={176}
height={120}
loading="eager"
className="rounded object-cover"
/>
)}
<p className="font-semibold">{room.name}</p>
<p>
{formatISOToLocalTime(s.start)} –{" "}
{formatISOToLocalTime(s.end)}
</p>
{room.capacity && (
<p>Capacity: {room.capacity}</p>
)}
{room.description && (
<p className="opacity-75">
{room.description.slice(0, 50)}
</p>
)}
</div>
}
>
<span
className={cn(
Expand Down
120 changes: 120 additions & 0 deletions src/lib/rooms/room-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Auto-generated by scripts/fetch-room-images.ts
// Run `pnpm tsx scripts/fetch-room-images.ts` to regenerate (77/145 rooms have images)
export const ROOM_IMAGES: Record<string, string> = {
"102070":
"https://d2jv02qf7xgjwx.cloudfront.net/accounts/88849/images/presentation_studio_study_space_locator.jpg",
"111030":
"https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"111031":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"117629":
"https://libapps.s3.amazonaws.com/accounts/157453/images/20210907_101945.jpg",
"117634":
"https://libapps.s3.amazonaws.com/accounts/157453/images/20210907_101945.jpg",
"131259":
"https://libapps.s3.amazonaws.com/accounts/88849/images/DSC07786__1_.jpg",
"131264": "https://libapps.s3.amazonaws.com/accounts/88849/images/LL360.jpg",
"155343":
"https://libapps.s3.amazonaws.com/accounts/88849/images/IMG_2499.jpg",
"155344":
"https://libapps.s3.amazonaws.com/accounts/88849/images/IMG_2499.jpg",
"168432":
"https://libapps.s3.amazonaws.com/accounts/88849/images/IMG_2499.jpg",
"168433":
"https://libapps.s3.amazonaws.com/accounts/88849/images/IMG_2499.jpg",
"168434":
"https://libapps.s3.amazonaws.com/accounts/88849/images/IMG_2499.jpg",
"168435":
"https://libapps.s3.amazonaws.com/accounts/88849/images/IMG_2499.jpg",
"168436":
"https://libapps.s3.amazonaws.com/accounts/88849/images/IMG_2499.jpg",
"168437":
"https://libapps.s3.amazonaws.com/accounts/88849/images/IMG_2499.jpg",
"168438":
"https://libapps.s3.amazonaws.com/accounts/88849/images/IMG_2499.jpg",
"178733":
"https://d2jv02qf7xgjwx.cloudfront.net/accounts/88849/images/IMG_2499.jpg",
"178734":
"https://d2jv02qf7xgjwx.cloudfront.net/accounts/88849/images/IMG_2499.jpg",
"34680":
"https://libapps.s3.amazonaws.com/accounts/190801/images/ALP_2510.jpg",
"34681":
"https://libapps.s3.amazonaws.com/accounts/190801/images/ALP_2210.jpg",
"34682":
"https://libapps.s3.amazonaws.com/accounts/190801/images/ALP_2610.jpg",
"34683":
"https://libapps.s3.amazonaws.com/accounts/190801/images/ALP_2710.jpg",
"37288":
"https://libapps.s3.amazonaws.com/accounts/190523/images/ALP3710.JPG",
"37289":
"https://libapps.s3.amazonaws.com/accounts/190523/images/ALP_3720.JPG",
"37290":
"https://libapps.s3.amazonaws.com/accounts/190523/images/ALP_3730.JPG",
"37291":
"https://libapps.s3.amazonaws.com/accounts/190523/images/ALP_3740.JPG",
"37292":
"https://libapps.s3.amazonaws.com/accounts/190523/images/ALP_3750.JPG",
"44667":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"44668": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44669": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44670": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44671": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44672": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44673": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44674": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44675": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44676": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44677": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44678": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44679": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44680": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44681": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44683":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"44684":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"44685":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"44686":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"44687":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"44688":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"44689":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"44690":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"44691":
"https://libapps.s3.amazonaws.com/accounts/88849/images/PXL_20220927_152801735.MP_75.jpg",
"44693": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44694": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44695": "https://s3.amazonaws.com/libapps/customers/926/images/ASL_586_.jpg",
"44696": "https://s3.amazonaws.com/libapps/accounts/86460/images/LL_392.jpg",
"44697": "https://s3.amazonaws.com/libapps/accounts/86460/images/LL_382.jpg",
"44698": "https://s3.amazonaws.com/libapps/accounts/86460/images/LL_392.jpg",
"44699": "https://s3.amazonaws.com/libapps/accounts/86460/images/LL_392.jpg",
"44700": "https://s3.amazonaws.com/libapps/accounts/86460/images/LL_392.jpg",
"44701": "https://s3.amazonaws.com/libapps/accounts/86460/images/LL_392.jpg",
"44702": "https://s3.amazonaws.com/libapps/accounts/86460/images/LL_392.jpg",
"44703": "https://s3.amazonaws.com/libapps/accounts/86460/images/LL_392.jpg",
"44704": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-1.jpg",
"44705": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-1.jpg",
"44706": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-1.jpg",
"44707": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-1.jpg",
"44708": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-1.jpg",
"44709": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-1.jpg",
"44710": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-1.jpg",
"44711": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-11.jpg",
"44712": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-11.jpg",
"44713": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-1.jpg",
"44714": "https://s3.amazonaws.com/libapps/accounts/86460/images/LGSC-1.jpg",
"44717": "https://libapps.s3.amazonaws.com/accounts/144488/images/SL172.jpg",
"44718":
"https://libapps.s3.amazonaws.com/accounts/144488/images/SL173April2022.jpg",
"51792":
"https://libapps.s3.amazonaws.com/accounts/144488/images/mrc_pres_studio_2.jpg",
"59309":
"https://libapps.s3.amazonaws.com/accounts/144488/images/SL174SPq2022_0.jpg",
};
Loading