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
15 changes: 15 additions & 0 deletions app/board/board.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions components/board/Editors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ export function PhotosEditor({ onDone }: { onDone: () => void }) {
Replace
<input type="file" accept="image/jpeg,image/png,image/webp" onChange={(e) => upload(e.target.files?.[0], i)} />
</label>
<label className="inline">
<input
type="checkbox"
checked={p.row === 2}
onChange={(e) => setList(list.map((x, j) => (j === i ? (e.target.checked ? { src: x.src, row: 2 as const } : { src: x.src }) : x)))}
/>{" "}
Second row
</label>
<RowTools i={i} n={list.length} onMove={(d) => setList(move(list, i, d))} onRemove={() => setList(list.filter((_, j) => j !== i))} />
</div>
))}
Expand Down
17 changes: 10 additions & 7 deletions components/board/HomeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ export function HomeTab() {
) : (
(settings.photos.length > 0 || admin) && (
<div className="faces-wrap">
{settings.photos.length > 0 && (
<div className="faces" aria-label={`${chapter.title} board photos`}>
{settings.photos.map((p, i) => (
// eslint-disable-next-line @next/next/no-img-element
<img key={p.src + i} src={photoSrc(p.src)} alt="Board member" />
))}
</div>
{[settings.photos.filter((p) => p.row !== 2), settings.photos.filter((p) => p.row === 2)].map(
(row, r) =>
row.length > 0 && (
<div className={r ? "faces faces-row2" : "faces"} key={r} aria-label={`${chapter.title} board photos${r ? ", second row" : ""}`}>
{row.map((p, i) => (
// eslint-disable-next-line @next/next/no-img-element
<img key={p.src + i} src={photoSrc(p.src)} alt="Board member" />
))}
</div>
),
)}
{admin && (
<p className="admin-row">
Expand Down
6 changes: 6 additions & 0 deletions lib/board/chapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }[];
Expand All @@ -54,6 +56,9 @@ export type ChapterConfig = {
myPage: { meetingLine: string | null; teamTrack: Record<string, string>; teamNote: Record<string, string> };
};

// 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);
Expand Down Expand Up @@ -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: [
Expand Down
11 changes: 7 additions & 4 deletions lib/board/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
};
}
Expand Down Expand Up @@ -151,8 +153,9 @@ export function checkSetting(key: SettingKey, v: unknown): Ok<unknown> {
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);
Expand Down
18 changes: 18 additions & 0 deletions lib/board/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<typeof db>) {
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
Expand Down
Binary file added public/board/ucla/board-10.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/board/ucla/board-11.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/board/ucla/board-12.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/board/ucla/board-13.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/board/ucla/board-14.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/board/ucla/board-7.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/board/ucla/board-8.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/board/ucla/board-9.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.