diff --git a/app/board/board.css b/app/board/board.css index 819d7c1..858155d 100644 --- a/app/board/board.css +++ b/app/board/board.css @@ -776,6 +776,21 @@ max-height: 80px; } } +/* The second photo row (headshots) sits under the first, a little smaller + so the faces match the full-body cutouts above, and wraps on phones. */ +.board .faces.faces-row2 { + flex-wrap: wrap; + gap: 6px 8px; + padding-top: 10px; +} +.board .faces.faces-row2 img { + max-height: 96px; +} +@media (max-width: 640px) { + .board .faces.faces-row2 img { + max-height: 60px; + } +} .board .st-excused { color: var(--deep); diff --git a/components/board/Editors.tsx b/components/board/Editors.tsx index 5d5dc32..234c328 100644 --- a/components/board/Editors.tsx +++ b/components/board/Editors.tsx @@ -210,6 +210,14 @@ export function PhotosEditor({ onDone }: { onDone: () => void }) { Replace upload(e.target.files?.[0], i)} /> + setList(move(list, i, d))} onRemove={() => setList(list.filter((_, j) => j !== i))} /> ))} diff --git a/components/board/HomeTab.tsx b/components/board/HomeTab.tsx index 8feae09..f83ab98 100644 --- a/components/board/HomeTab.tsx +++ b/components/board/HomeTab.tsx @@ -37,13 +37,16 @@ export function HomeTab() { ) : ( (settings.photos.length > 0 || admin) && (
- {settings.photos.length > 0 && ( -
- {settings.photos.map((p, i) => ( - // eslint-disable-next-line @next/next/no-img-element - Board member - ))} -
+ {[settings.photos.filter((p) => p.row !== 2), settings.photos.filter((p) => p.row === 2)].map( + (row, r) => + row.length > 0 && ( +
+ {row.map((p, i) => ( + // eslint-disable-next-line @next/next/no-img-element + Board member + ))} +
+ ), )} {admin && (

diff --git a/lib/board/chapters.ts b/lib/board/chapters.ts index 46de12a..376b41b 100644 --- a/lib/board/chapters.ts +++ b/lib/board/chapters.ts @@ -35,6 +35,8 @@ export type ChapterConfig = { quickLinks: { label?: string; text: string; href?: string | null; tab?: Tab }[]; keyDates: [date: string, text: string][]; photos: string[]; + // A second row of photos under the first (optional). + photosRow2?: string[]; socials: { heading: string; kinds: { key: "b" | "o"; label: string }[]; @@ -54,6 +56,9 @@ export type ChapterConfig = { myPage: { meetingLine: string | null; teamTrack: Record; teamNote: Record }; }; +// UCLA's second photo row (added Oct 15). +export const UCLA_ROW2 = [7, 8, 9, 10, 11, 12, 13, 14].map((n) => `/board/ucla/board-${n}.webp`); + const shortDay = (iso: string, o: Intl.DateTimeFormatOptions) => { const [y, m, d] = iso.split("-").map(Number); return new Date(y, m - 1, d).toLocaleDateString("en-US", o); @@ -167,6 +172,7 @@ const UCLA: ChapterConfig = { ["2026-11-22", "Crosscheck demo and research writeups (week 10)"], ], photos: [1, 2, 3, 4, 5, 6].map((n) => `/board/ucla/board-${n}.webp`), + photosRow2: UCLA_ROW2, socials: { heading: "Fall 2026 Events", kinds: [ diff --git a/lib/board/settings.ts b/lib/board/settings.ts index 26d5f4a..be99da2 100644 --- a/lib/board/settings.ts +++ b/lib/board/settings.ts @@ -7,7 +7,9 @@ import { TABS, type Tab } from "./types"; export type QuickLink = { label: string; text: string; href: string | null; tab: Tab | "" }; export type KeyDate = { date: string; text: string }; -export type Photo = { src: string }; +// `row: 2` puts a photo in a second row under the first, so adding people +// never shrinks the photos already in the first row. +export type Photo = { src: string; row?: 2 }; export type Projects = ChapterConfig["projects"]; export type Settings = { @@ -48,7 +50,7 @@ export function defaultSettings(chapter: ChapterId): Settings { .filter((l) => l !== meeting) .map((l) => ({ label: l.label ?? "", text: l.text, href: l.href ?? null, tab: l.tab ?? "" })), keyDates: c.keyDates.map(([date, text]) => ({ date, text })), - photos: c.photos.map((src) => ({ src })), + photos: [...c.photos.map((src) => ({ src })), ...(c.photosRow2 ?? []).map((src) => ({ src, row: 2 as const }))], projects: c.projects, }; } @@ -151,8 +153,9 @@ export function checkSetting(key: SettingKey, v: unknown): Ok { return { ok: true, value: out }; } case "photos": { - if (!Array.isArray(v) || v.length > 12 || !v.every((p) => p && typeof p.src === "string" && isPhotoSrc(p.src))) return fail("Check the photos."); - return { ok: true, value: v.map((p) => ({ src: p.src })) }; + if (!Array.isArray(v) || v.length > 24 || !v.every((p) => p && typeof p.src === "string" && isPhotoSrc(p.src) && (p.row === undefined || p.row === 1 || p.row === 2))) + return fail("Check the photos."); + return { ok: true, value: v.map((p) => (p.row === 2 ? { src: p.src, row: 2 as const } : { src: p.src })) }; } case "projects": return projects(v); diff --git a/lib/board/setup.ts b/lib/board/setup.ts index 508413f..4149da3 100644 --- a/lib/board/setup.ts +++ b/lib/board/setup.ts @@ -6,6 +6,7 @@ import starter from "./starter-data.json"; import ucla from "./starter-ucla.json"; import { seedSettings } from "./settings-server"; import { CHAPTER_SERVER } from "./chapter-server"; +import { UCLA_ROW2 } from "./chapters"; // Makes the board database ready on first use, so nobody has to run // commands: creates the tables (scripts/board-schema.sql, safe to re-run) @@ -70,6 +71,23 @@ async function setup() { await markHubAdmins(sql); await renameEvanceline(sql); await adminsOnlyToHubAdmins(sql); + await uclaPhotoRow2(sql); +} + +// Adds UCLA's second photo row under the existing photos, once. The first +// row is left exactly as it is. +async function uclaPhotoRow2(sql: ReturnType) { + await sql.begin(async (tx) => { + const [first] = await tx`INSERT INTO board_changes (name) VALUES ('2026-10-15-ucla-photo-row-2') + ON CONFLICT DO NOTHING RETURNING name`; + if (!first) return; + const [row] = await tx`SELECT value FROM board_settings WHERE chapter = 'ucla' AND key = 'photos'`; + const photos = (row?.value ?? []) as { src: string; row?: 2 }[]; + const have = new Set(photos.map((p) => p.src)); + const added = UCLA_ROW2.filter((src) => !have.has(src)).map((src) => ({ src, row: 2 as const })); + if (!row || added.length === 0) return; + await tx`UPDATE board_settings SET value = ${tx.json([...photos, ...added] as never)} WHERE chapter = 'ucla' AND key = 'photos'`; + }); } // Announcements marked "admins only" before "Send to" existed are for Hub diff --git a/public/board/ucla/board-10.webp b/public/board/ucla/board-10.webp new file mode 100644 index 0000000..0ef788a Binary files /dev/null and b/public/board/ucla/board-10.webp differ diff --git a/public/board/ucla/board-11.webp b/public/board/ucla/board-11.webp new file mode 100644 index 0000000..7e33c5b Binary files /dev/null and b/public/board/ucla/board-11.webp differ diff --git a/public/board/ucla/board-12.webp b/public/board/ucla/board-12.webp new file mode 100644 index 0000000..8b71688 Binary files /dev/null and b/public/board/ucla/board-12.webp differ diff --git a/public/board/ucla/board-13.webp b/public/board/ucla/board-13.webp new file mode 100644 index 0000000..9352e95 Binary files /dev/null and b/public/board/ucla/board-13.webp differ diff --git a/public/board/ucla/board-14.webp b/public/board/ucla/board-14.webp new file mode 100644 index 0000000..faa6192 Binary files /dev/null and b/public/board/ucla/board-14.webp differ diff --git a/public/board/ucla/board-7.webp b/public/board/ucla/board-7.webp new file mode 100644 index 0000000..56350f0 Binary files /dev/null and b/public/board/ucla/board-7.webp differ diff --git a/public/board/ucla/board-8.webp b/public/board/ucla/board-8.webp new file mode 100644 index 0000000..73a3e85 Binary files /dev/null and b/public/board/ucla/board-8.webp differ diff --git a/public/board/ucla/board-9.webp b/public/board/ucla/board-9.webp new file mode 100644 index 0000000..e45bf7a Binary files /dev/null and b/public/board/ucla/board-9.webp differ