From 0e1183b060cd269defec13d3476497bf31d4f49e Mon Sep 17 00:00:00 2001 From: Arya Palanivel Date: Fri, 5 Jun 2026 17:38:11 -0700 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E2=9C=A8=20room=20image=20shown=20?= =?UTF-8?q?on=20hover?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.mjs | 15 ++ scripts/fetch-room-images.ts | 131 ++++++++++++++++++ .../studyrooms/heatmap/rooms-heatmap.tsx | 30 +++- src/lib/rooms/room-images.ts | 100 +++++++++++++ 4 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 scripts/fetch-room-images.ts create mode 100644 src/lib/rooms/room-images.ts diff --git a/next.config.mjs b/next.config.mjs index de7a8212b..27388de6e 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,21 @@ /** @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" }, + { + protocol: "https", + hostname: "**.cloudfront.net", + pathname: "/accounts/**", + }, + ], + }, async headers() { return [ // Security headers required/recommended by PWA Builder and Apple's diff --git a/scripts/fetch-room-images.ts b/scripts/fetch-room-images.ts new file mode 100644 index 000000000..e34cc70fd --- /dev/null +++ b/scripts/fetch-room-images.ts @@ -0,0 +1,131 @@ +#!/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_BASE = "https://spaces.lib.uci.edu/space"; +const DAYS_TO_SCAN = 30; + +function nextWeekdays(from: Date, count: number): string[] { + const dates: string[] = []; + const cursor = new Date(from); + while (dates.length < count) { + const day = cursor.getDay(); + if (day !== 0 && day !== 6) { + dates.push(cursor.toISOString().split("T")[0]); + } + cursor.setDate(cursor.getDate() + 1); + } + return dates; +} + +async function fetchRoomsForDate( + date: string, +): Promise> { + try { + const url = `${ANTEATER_API}?dates=${date}×=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 { + try { + const res = await fetch(`${LIBCAL_BASE}/${roomId}`, { + headers: { "User-Agent": "Mozilla/5.0 (compatible; ZotMeet/1.0)" }, + signal: AbortSignal.timeout(8000), + }); + if (!res.ok) return null; + const html = await res.text(); + // Room images are hosted on S3 (path-style or virtual-hosted) or CloudFront via Springshare. + 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, + ); + return match?.[0] ?? null; + } catch { + return null; + } +} + +async function main() { + const dates = nextWeekdays(new Date(), DAYS_TO_SCAN); + 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) { + 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 = {}; + + 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 = {", + ...Object.entries(imageMap).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"); + console.log(`\nWrote ${found} image entries → src/lib/rooms/room-images.ts`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/components/studyrooms/heatmap/rooms-heatmap.tsx b/src/components/studyrooms/heatmap/rooms-heatmap.tsx index 5953d3914..0cf870c32 100644 --- a/src/components/studyrooms/heatmap/rooms-heatmap.tsx +++ b/src/components/studyrooms/heatmap/rooms-heatmap.tsx @@ -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, @@ -213,7 +215,33 @@ export const RoomsHeatmap = ({ > + {ROOM_IMAGES[room.id] && ( + {room.name + )} +

{room.name}

+

+ {formatISOToLocalTime(s.start)} –{" "} + {formatISOToLocalTime(s.end)} +

+ {room.capacity && ( +

Capacity: {room.capacity}

+ )} + {room.description && ( +

+ {room.description.slice(0, 50)} +

+ )} + + } > = { + "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", + "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", +}; From d6321806a408b919e5815658affe1841096e80a6 Mon Sep 17 00:00:00 2001 From: Arya Palanivel Date: Fri, 5 Jun 2026 18:04:53 -0700 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=F0=9F=90=9B=20finds=20ALP=20room=20?= =?UTF-8?q?images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/fetch-room-images.ts | 55 +++++++++++++++++++++++------------- src/lib/rooms/room-images.ts | 22 ++++++++++++++- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/scripts/fetch-room-images.ts b/scripts/fetch-room-images.ts index e34cc70fd..61cbd8eea 100644 --- a/scripts/fetch-room-images.ts +++ b/scripts/fetch-room-images.ts @@ -15,13 +15,21 @@ import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ANTEATER_API = "https://anteaterapi.com/v2/rest/studyRooms"; -const LIBCAL_BASE = "https://spaces.lib.uci.edu/space"; -const DAYS_TO_SCAN = 30; +const LIBCAL_BASES = [ + "https://spaces.lib.uci.edu/space", + "https://scheduler.oit.uci.edu/space", +]; +const DAYS_BACK = 7; +const DAYS_FORWARD = 30; -function nextWeekdays(from: Date, count: number): string[] { +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(from); - while (dates.length < count) { + const cursor = new Date(start); + while (cursor <= end) { const day = cursor.getDay(); if (day !== 0 && day !== 6) { dates.push(cursor.toISOString().split("T")[0]); @@ -46,25 +54,32 @@ async function fetchRoomsForDate( } async function fetchRoomImage(roomId: string): Promise { - try { - const res = await fetch(`${LIBCAL_BASE}/${roomId}`, { - headers: { "User-Agent": "Mozilla/5.0 (compatible; ZotMeet/1.0)" }, - signal: AbortSignal.timeout(8000), - }); - if (!res.ok) return null; - const html = await res.text(); - // Room images are hosted on S3 (path-style or virtual-hosted) or CloudFront via Springshare. - 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, - ); - return match?.[0] ?? null; - } catch { - return 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 = nextWeekdays(new Date(), DAYS_TO_SCAN); + const dates = weekdaysAround(new Date(), DAYS_BACK, DAYS_FORWARD); console.log( `Scanning ${dates.length} weekdays (${dates[0]} → ${dates.at(-1)}) for rooms...\n`, ); diff --git a/src/lib/rooms/room-images.ts b/src/lib/rooms/room-images.ts index 95adc588b..ead88a2d5 100644 --- a/src/lib/rooms/room-images.ts +++ b/src/lib/rooms/room-images.ts @@ -1,6 +1,24 @@ // Auto-generated by scripts/fetch-room-images.ts -// Run `pnpm tsx scripts/fetch-room-images.ts` to regenerate (67/145 rooms have images) +// Run `pnpm tsx scripts/fetch-room-images.ts` to regenerate (77/144 rooms have images) export const ROOM_IMAGES: Record = { + "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", @@ -60,6 +78,8 @@ export const ROOM_IMAGES: Record = { "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": From 7076cc4a4e5540b60a31c90871b0f9c91297e878 Mon Sep 17 00:00:00 2001 From: Arya Palanivel Date: Fri, 5 Jun 2026 18:15:55 -0700 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=F0=9F=90=9B=20config=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/next.config.mjs b/next.config.mjs index 27388de6e..c8390c7d1 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -16,6 +16,14 @@ const nextConfig = { }, ], }, + async rewrites() { + return [ + { + source: "/.well-known/apple-app-site-association", + destination: "/apple-app-site-association", + }, + ]; + }, async headers() { return [ // Security headers required/recommended by PWA Builder and Apple's From 61331e7d5a231a6acef3f33362c256d5f62b5cd1 Mon Sep 17 00:00:00 2001 From: Arya Palanivel Date: Sat, 6 Jun 2026 16:03:20 -0700 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=F0=9F=90=9B=20cubic=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.mjs | 8 ++- scripts/fetch-room-images.ts | 49 ++++++------- .../studyrooms/heatmap/rooms-heatmap.tsx | 10 ++- src/lib/rooms/room-images.ts | 72 +++++++++---------- 4 files changed, 66 insertions(+), 73 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index c8390c7d1..8d9032728 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -8,10 +8,14 @@ const nextConfig = { hostname: "s3.amazonaws.com", pathname: "/libapps/**", }, - { protocol: "https", hostname: "libapps.s3.amazonaws.com" }, { protocol: "https", - hostname: "**.cloudfront.net", + hostname: "libapps.s3.amazonaws.com", + pathname: "/accounts/**", + }, + { + protocol: "https", + hostname: "d2jv02qf7xgjwx.cloudfront.net", pathname: "/accounts/**", }, ], diff --git a/scripts/fetch-room-images.ts b/scripts/fetch-room-images.ts index 61cbd8eea..b74790e0b 100644 --- a/scripts/fetch-room-images.ts +++ b/scripts/fetch-room-images.ts @@ -1,13 +1,4 @@ #!/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"; @@ -19,9 +10,6 @@ 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); @@ -32,7 +20,10 @@ function weekdaysAround(anchor: Date, back: number, forward: number): string[] { while (cursor <= end) { const day = cursor.getDay(); if (day !== 0 && day !== 6) { - dates.push(cursor.toISOString().split("T")[0]); + 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); } @@ -41,15 +32,15 @@ function weekdaysAround(anchor: Date, back: number, forward: number): string[] { async function fetchRoomsForDate( date: string, -): Promise> { +): Promise | null> { try { const url = `${ANTEATER_API}?dates=${date}×=07:00-23:00`; const res = await fetch(url, { signal: AbortSignal.timeout(10000) }); - if (!res.ok) return []; + if (!res.ok) return null; const json = await res.json(); return json.data ?? []; } catch { - return []; + return null; } } @@ -62,8 +53,6 @@ async function fetchRoomImage(roomId: string): Promise { }); 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, ); @@ -71,15 +60,13 @@ async function fetchRoomImage(roomId: string): Promise { const url = match[0].startsWith("//") ? `https:${match[0]}` : match[0]; return url; } - } catch { - // fetch failed for this base URL, try next - } + } catch {} } return null; } async function main() { - const dates = weekdaysAround(new Date(), DAYS_BACK, DAYS_FORWARD); + const dates = weekdaysAround(new Date(), 7, 30); console.log( `Scanning ${dates.length} weekdays (${dates[0]} → ${dates.at(-1)}) for rooms...\n`, ); @@ -93,6 +80,10 @@ async function main() { 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; @@ -104,14 +95,14 @@ async function main() { } } - const unique = [...roomMap.values()]; + const rooms = [...roomMap.values()]; console.log( - `\nDiscovered ${unique.length} unique rooms. Fetching images...\n`, + `\nDiscovered ${rooms.length} unique rooms. Fetching images...\n`, ); const imageMap: Record = {}; - for (const room of unique) { + for (const room of rooms) { process.stdout.write(` ${room.location} / ${room.name ?? room.id} ... `); const url = await fetchRoomImage(room.id); if (url) { @@ -126,11 +117,11 @@ async function main() { 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)`, + `// Run \`pnpm tsx scripts/fetch-room-images.ts\` to regenerate (${found}/${rooms.length} rooms have images)`, "export const ROOM_IMAGES: Record = {", - ...Object.entries(imageMap).map( - ([id, imgUrl]) => `\t"${id}": "${imgUrl}",`, - ), + ...Object.entries(imageMap) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([id, imgUrl]) => `\t"${id}": "${imgUrl}",`), "};", "", ].join("\n"); diff --git a/src/components/studyrooms/heatmap/rooms-heatmap.tsx b/src/components/studyrooms/heatmap/rooms-heatmap.tsx index 0cf870c32..788d4b643 100644 --- a/src/components/studyrooms/heatmap/rooms-heatmap.tsx +++ b/src/components/studyrooms/heatmap/rooms-heatmap.tsx @@ -35,7 +35,7 @@ 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"), ]; @@ -43,11 +43,9 @@ const LOCATION_SORT_ORDER: readonly string[] = [ 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); } diff --git a/src/lib/rooms/room-images.ts b/src/lib/rooms/room-images.ts index ead88a2d5..eb50be019 100644 --- a/src/lib/rooms/room-images.ts +++ b/src/lib/rooms/room-images.ts @@ -1,6 +1,41 @@ // Auto-generated by scripts/fetch-room-images.ts -// Run `pnpm tsx scripts/fetch-room-images.ts` to regenerate (77/144 rooms have images) +// Run `pnpm tsx scripts/fetch-room-images.ts` to regenerate (77/145 rooms have images) export const ROOM_IMAGES: Record = { + "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": @@ -82,39 +117,4 @@ export const ROOM_IMAGES: Record = { "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", };