Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
23 changes: 23 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
/** @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" },
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
{
protocol: "https",
hostname: "**.cloudfront.net",
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
pathname: "/accounts/**",
},
],
},
async rewrites() {
return [
{
source: "/.well-known/apple-app-site-association",
destination: "/apple-app-site-association",
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
},
];
},
async headers() {
return [
// Security headers required/recommended by PWA Builder and Apple's
Expand Down
146 changes: 146 additions & 0 deletions scripts/fetch-room-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env tsx
/**
* Fetches room image URLs from UCI Libraries space pages and writes a static map
* to src/lib/rooms/room-images.ts.
*
* Queries the AnteaterAPI across the next 30 days (weekdays only) to discover
* all unique room IDs, then fetches each room's image from UCI Libraries.
*
* Run with: pnpm tsx scripts/fetch-room-images.ts
*/
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",
];
const DAYS_BACK = 7;
const DAYS_FORWARD = 30;

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) {
dates.push(cursor.toISOString().split("T")[0]);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}
cursor.setDate(cursor.getDate() + 1);
}
return dates;
}

async function fetchRoomsForDate(
date: string,
): Promise<Array<{ id: string; name: string; location: string }>> {
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 [];
const json = await res.json();
return json.data ?? [];
} catch {
return [];
}
}

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();
// Room images are hosted on S3 (path-style or virtual-hosted) or CloudFront via Springshare.
// Some pages use protocol-relative URLs (//host/path) — normalize to https.
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 {
// fetch failed for this base URL, try next
}
}
return null;
}

async function main() {
const dates = weekdaysAround(new Date(), DAYS_BACK, DAYS_FORWARD);
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);
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 unique = [...roomMap.values()];
console.log(
`\nDiscovered ${unique.length} unique rooms. Fetching images...\n`,
);

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

for (const room of unique) {
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}/${unique.length} rooms have images)`,
"export const ROOM_IMAGES: Record<string, string> = {",
...Object.entries(imageMap).map(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
([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);
});
30 changes: 29 additions & 1 deletion 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 Down Expand Up @@ -213,7 +215,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/144 rooms have images)
export const ROOM_IMAGES: Record<string, string> = {
"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",
"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",
};