diff --git a/.env.example b/.env.example index 9c9073e..7ba3cd7 100644 --- a/.env.example +++ b/.env.example @@ -6,9 +6,9 @@ NEXT_PUBLIC_SITE_URL=https://www.getvindicated.org # Postgres service and reference it from the website service). DATABASE_URL= # Passwords for /board. Everyone on the board gets BOARD_PASSWORD (view and -# submit). Rana gets BOARD_ADMIN_PASSWORD (edit everything). Pierce gets -# BOARD_SOCIALS_PASSWORD (edit socials). Changing one signs out everyone -# who used it. +# submit). Rana gets BOARD_ADMIN_PASSWORD (edit everything). Whoever runs +# socials gets BOARD_SOCIALS_PASSWORD (edit socials); no one has it right +# now, so leave it empty. Changing one signs out everyone who used it. BOARD_PASSWORD= BOARD_ADMIN_PASSWORD= BOARD_SOCIALS_PASSWORD= diff --git a/BOARD_SETUP.md b/BOARD_SETUP.md index 005585d..53dfb39 100644 --- a/BOARD_SETUP.md +++ b/BOARD_SETUP.md @@ -9,10 +9,12 @@ need a password. | Railway variable | Who gets it | What it unlocks | | ------------------------ | -------------------- | --------------------------------------- | | `BOARD_PASSWORD` | everyone on the board | view everything, submit assignments | -| `BOARD_SOCIALS_PASSWORD` | Pierce | also add, edit and delete socials | +| `BOARD_SOCIALS_PASSWORD` | no one yet | also add, edit and delete socials | | `BOARD_ADMIN_PASSWORD` | Rana | edit everything | -Use three different passwords. People stay signed in on a device for 30 +No one edits socials right now, so leave `BOARD_SOCIALS_PASSWORD` unset +until Rana names someone; until then only the admin can change socials. +Use different passwords for each role. People stay signed in on a device for 30 days. To remove someone's access, change the password they used; that signs out everyone who used it, and you share the new one with the rest. diff --git a/app/board/board.css b/app/board/board.css index dd5f5cf..8619906 100644 --- a/app/board/board.css +++ b/app/board/board.css @@ -651,10 +651,6 @@ cursor: pointer; overflow-wrap: anywhere; } -.board .ev.x { - border-left-color: #1f5f8b; - background: #eef4f9; -} .board .ev.o { border-left-color: var(--deep); background: #f3f2f4; diff --git a/app/board/page.tsx b/app/board/page.tsx index b80f812..16822d0 100644 --- a/app/board/page.tsx +++ b/app/board/page.tsx @@ -20,7 +20,7 @@ export default async function BoardPage() { } let data; try { - data = await loadBoard(); + data = await loadBoard(user.role); } catch (e) { console.error("[board] Could not load the board:", e); return ; diff --git a/components/board/AssignmentsTab.tsx b/components/board/AssignmentsTab.tsx index 056685b..ad4d4d0 100644 --- a/components/board/AssignmentsTab.tsx +++ b/components/board/AssignmentsTab.tsx @@ -2,11 +2,11 @@ import { useEffect, useMemo, useRef, useState } from "react"; import type { Assignment, AssignmentStatus } from "@/lib/board/types"; -import { ARCHIVE_ROUND, CURRENT_ROUND, RULE } from "@/lib/board/static"; +import { ARCHIVE_ROUND, CURRENT_ROUND, RULE, UNASSIGNED } from "@/lib/board/static"; import { useBoard } from "./BoardApp"; import { RowEditor, type FieldDef } from "./RowEditor"; import { SubmitForm } from "./SubmitForm"; -import { currentRound } from "./derive"; +import { currentRound, unassignedTasks } from "./derive"; import { api, daysBetween, fmt } from "./util"; const STATUS_WORD: Record = { @@ -24,6 +24,7 @@ export function AssignmentsTab() { const { data, user, today, focus, setFocus, refresh } = useBoard(); const admin = user.role === "admin"; const people = currentRound(data); + const unassigned = admin ? unassignedTasks(data) : []; const [filter, setFilter] = useState<"All" | "Not submitted" | "Submitted">("All"); const [open, setOpen] = useState>(new Set()); const [ticks, setTicks] = useState>({}); @@ -66,7 +67,7 @@ export function AssignmentsTab() { const archiveDone = archive.filter((a) => a.status !== "missing" && a.status !== "pending").length; const memberOptions = useMemo( - () => data.members.map((m) => ({ value: m.firstName, label: m.name })), + () => [...data.members.map((m) => ({ value: m.firstName, label: m.name })), { value: UNASSIGNED, label: "Unassigned (admins only)" }], [data.members], ); const taskFields: FieldDef[] = [ @@ -127,6 +128,48 @@ export function AssignmentsTab() { /> )} + {unassigned.length > 0 && ( +
+ + + Unassigned · only admins see this + + + {unassigned.length} to assign + +
+

Edit a task and choose a person to give it to them.

+
    + {unassigned.map((a) => ( +
  • +
    + {a.task} +
    + Proof: {a.proof} +
    +
    + +
    + {editingTask?.id === a.id && ( + setEditing(null)} + /> + )} +
    +
  • + ))} +
+
+
+ )} +
{shown.map((p) => { const subs = data.submissions.filter((s) => s.person === p.person && (!s.assignmentId || p.items.some((i) => i.id === s.assignmentId))); diff --git a/components/board/SocialsTab.tsx b/components/board/SocialsTab.tsx index 68189bb..ddff1f1 100644 --- a/components/board/SocialsTab.tsx +++ b/components/board/SocialsTab.tsx @@ -9,7 +9,7 @@ import { RowEditor, type FieldDef } from "./RowEditor"; import { D, fmt, isoDate } from "./util"; const FILTERS = [{ key: "All", label: "All" }, ...SOCIAL_KINDS.map((k) => ({ key: k.key as string, label: k.label }))]; -const TAG: Record = { b: "Berkeley", x: "UCSC × UCB", o: "Online", org: "Org date" }; +const TAG: Record = { b: "Berkeley", o: "Online", org: "Org date" }; const SOCIAL_FIELDS: FieldDef[] = [ { key: "title", label: "Name" }, @@ -142,7 +142,7 @@ export function SocialsTab() { title="Add a social" table="socials" fields={SOCIAL_FIELDS} - initial={{ title: "", date: "", time: "", location: "", kind: "b", status: "Idea", lead: "Pierce", cost: "", note: "" }} + initial={{ title: "", date: "", time: "", location: "", kind: "b", status: "Idea", lead: "TBD", cost: "", note: "" }} onClose={() => setEditing(null)} onSaved={(v) => jumpTo(v.date)} /> @@ -233,7 +233,7 @@ export function SocialsTab() { ); })} -

Status runs Idea → Planning → Confirmed → Completed. Pierce leads socials unless noted.

+

Status runs Idea → Planning → Confirmed → Completed.

); } diff --git a/components/board/derive.ts b/components/board/derive.ts index b880ff1..3cd2bcf 100644 --- a/components/board/derive.ts +++ b/components/board/derive.ts @@ -1,7 +1,7 @@ "use client"; import type { Assignment, BoardData, Member } from "@/lib/board/types"; -import { CURRENT_ROUND } from "@/lib/board/static"; +import { CURRENT_ROUND, UNASSIGNED } from "@/lib/board/static"; export type PersonTasks = { person: string; @@ -12,10 +12,11 @@ export type PersonTasks = { }; // Current-round tasks grouped by person, in the order they were added. +// Tasks with no owner yet are left out (see unassignedTasks). export function currentRound(data: BoardData): PersonTasks[] { const map = new Map(); for (const a of data.assignments) { - if (a.round !== CURRENT_ROUND.round) continue; + if (a.round !== CURRENT_ROUND.round || a.person === UNASSIGNED) continue; let p = map.get(a.person); if (!p) { p = { person: a.person, role: a.personRole, items: [], due: a.dueDate, submitted: false }; @@ -31,4 +32,8 @@ export function currentRound(data: BoardData): PersonTasks[] { return [...map.values()]; } +// Current-round tasks with no owner yet. Only admins receive these. +export const unassignedTasks = (data: BoardData) => + data.assignments.filter((a) => a.round === CURRENT_ROUND.round && a.person === UNASSIGNED); + export const inLeadership = (m: Member) => m.isLead || m.tier !== null; diff --git a/docs/board-preview.html b/docs/board-preview.html index b49f96f..8ec85c1 100644 --- a/docs/board-preview.html +++ b/docs/board-preview.html @@ -140,7 +140,6 @@ .month .day.today .dn { background: var(--fg); color: var(--bg); padding: 0 5px; } .month .dn { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; align-self: flex-start; } .ev { font: inherit; font-size: 13px; line-height: 1.3; text-align: left; border: 0; border-left: 3px solid var(--accent); background: var(--tint); color: var(--fg); padding: 3px 5px; cursor: pointer; border-radius: 0; overflow-wrap: anywhere; } -.ev.x { border-left-color: #1f5f8b; background: #eef4f9; } .ev.o { border-left-color: var(--soft); background: #f3f2f4; } .ev.org { border-left-color: var(--prog); background: #fbf5e9; cursor: default; } .ev[aria-pressed="true"] { outline: 2px solid var(--accent); } @@ -306,7 +305,7 @@

All socials

    -

    Status runs Idea → Planning → Confirmed → Completed. Pierce leads socials unless noted.

    +

    Status runs Idea → Planning → Confirmed → Completed.

    @@ -351,7 +350,6 @@

    Board meeting · Sep 21, 2026

    P("Yunchuan Wang","Research Analyst","Research & Legal","member17@example.com","(555) 555-0117"), P("Maria Miranda Nadsady","Legal Research","Research & Legal","member18@example.com","(555) 555-0118"), P("Chloe Yuching Lin","Outreach Lead · Social Media","Outreach","member19@example.com","(555) 555-0119",{ lead: true }), - P("Pierce Matthew Kosobayashi","Socials","Outreach","member20@example.com","(555) 555-0120"), P("Rami K Dakak","Research Analyst","Research & Legal","member21@example.com","(555) 555-0121") ]; const openSeats = {}; @@ -423,18 +421,14 @@

    Board meeting · Sep 21, 2026

    { name:"Noirit", role:"IVP · Price Is Right Lead", items:[ T("Price Is Right week 2 check: pipeline started, database set up, game screens running with placeholder data","GitHub commit links and a screenshot of the screens"), T("Sunday night: message every board member who hasn't submitted yet","Screenshot of the messages")] }, - { name:"Pierce", role:"Socials", items:[ - T("VINdiBasketball: set the date and book an RSF court","Booking confirmation"), - T("RSVP form for basketball, sent to Chloe to post","Form link"), - T("Send invites for every social on the Socials calendar to the full member email list","Screenshot of the sent email showing the recipients"), - T("Keep the Socials calendar current: confirm each location and update it on the page","The updated calendar")] }, { name:"Ian", role:"Software Lead", items:[ T("Get access to the website repo from Rizwaan or Ray","Screenshot showing access"), T("Assign 3 website-fix pull requests to Ryan and Ethan: write each fix as a GitHub issue with what's broken and what done looks like","Links to the 3 issues, each assigned to Ryan or Ethan")] }, { name:"Halima", role:"EVP · Partnerships", items:[ T("Reach out to 10 Berkeley student orgs that make videos, starting with Cal TV, about filming or collaborating with us","Screenshot of each sent email or DM, plus a list of the 10 orgs and who replied")] }, { name:"Daryen", role:"Communications Lead", items:[ - T("Call 5 local mechanics about recording a short video with us (carried over from round 1)","Call log: shop name, number, time called, what they said")] }, + T("Call 5 local mechanics about recording a short video with us (carried over from round 1)","Call log: shop name, number, time called, what they said"), + T("Book an RSF court for VINdiBasketball, Sat Oct 17, 2–4pm","Booking confirmation")] }, { name:"Jordan", role:"Data Lead", items:[ T("Project spec, including how many people you need (carried over)","Doc link"), T("Message James Cheng about the project (carried over)","Screenshot of the message"), @@ -447,7 +441,6 @@

    Board meeting · Sep 21, 2026

    ["Ian","Follow up with Rizwaan, project spec","Marked completed"], ["Chloe","Event ideas, 5 mechanic calls, 10 high schools + YMCA","Marked completed"], ["Daryen","Help Halima, 5 mechanic calls","Not done"], - ["Pierce","Event ideas doc, workshop timeline, connect with Halima","Not done"], ["Jordan","Project spec, connect with James, answer Ray","Not done"] ]; document.getElementById("r1-count").textContent = `${round1.filter(r => r[2] !== "Not done").length} of ${round1.length} done`; @@ -520,21 +513,23 @@

    Board meeting · Sep 21, 2026

    } /* ---------- socials ---------- */ -// kind: b = Berkeley, x = UCSC x UCB, o = online, org = org date (not a social) +// kind: b = Berkeley, o = online, org = org date (not a social) const socials = [ - { date:"2026-10-17", time:"2:00–4:00pm", title:"VINdiBasketball", where:"RSF", kind:"b", lead:"Pierce", status:"Planning", cost:"Free. Optional boba after, self-pay.", note:"First Berkeley social. Court booking and RSVP form due Mon, Oct 12." }, - { date:"2026-10-21", time:"9:00–10:00pm", title:"Skribbl night", where:"Discord", kind:"o", lead:"Chloe", status:"Planning", cost:"Free", note:"Open to all three chapters. Chloe posts the link that afternoon." }, - { date:"2026-10-24", time:"Afternoon", title:"UCSC × UCB social", where:"San Francisco · spot TBD", kind:"x", lead:"Rana + Ashwin", status:"Idea", cost:"Self-pay", note:"Combined social with UCSC. Location still being decided." }, - { date:"2026-10-30", time:"5:00–8:00pm", title:"Glade Night + Chipotle", where:"Memorial Glade", kind:"b", lead:"Pierce", status:"Planning", cost:"Chipotle catering from the $240, if the Oct 13 budget allows", note:"Halloween eve, costumes optional. Last week of daylight before clocks change." }, - { date:"2026-11-06", time:"", title:"Price Is Right demo", where:"", kind:"org", lead:"Noirit", status:"", cost:"", note:"Not a social. No socials Nov 5–15." }, - { date:"2026-11-18", time:"8:00–10:30pm", title:"Movie Night + Menchie's", where:"TBD", kind:"b", lead:"Daryen (movie), Chloe (Menchie's)", status:"Idea", cost:"Self-pay froyo", note:"" }, - { date:"2026-11-21", time:"1:00–4:00pm", title:"UCSC × UCB basketball", where:"Irvington Park, 41825 Blacow Rd, Fremont", kind:"x", lead:"Pierce + UCSC lead", status:"Idea", cost:"Free", note:"Meet-in-the-middle game, separate from VINdiBasketball. Outdoor court, first come. Backup: Karl Nordvik Park." }, + { date:"2026-10-17", time:"2:00–4:00pm", title:"VINdiBasketball", where:"RSF", kind:"b", lead:"TBD", status:"Planning", cost:"Free", note:"" }, + { date:"2026-10-21", time:"9:00–10:00pm", title:"Skribbl night", where:"Discord", kind:"o", lead:"TBD", status:"Planning", cost:"Free", note:"" }, + { date:"2026-10-24", time:"9:30am–12:00pm", title:"Grizzly Peak hike", where:"Meet at the Big C trailhead, Stadium Rim Way", kind:"b", lead:"TBD", status:"Idea", cost:"Free", note:"Steep in parts, so bring water. Moves to Nov 21 if it rains." }, + { date:"2026-10-27", time:"7:00–9:00pm", title:"Pumpkin carving", where:"Campus room TBD", kind:"b", lead:"TBD", status:"Idea", cost:"About $5 a pumpkin, from the $240", note:"" }, + { date:"2026-10-30", time:"5:00–8:00pm", title:"Glade Night + Chipotle", where:"Memorial Glade", kind:"b", lead:"TBD", status:"Planning", cost:"Chipotle from the $240 if the budget allows", note:"" }, + { date:"2026-11-06", time:"", title:"Price Is Right demo", where:"", kind:"org", lead:"TBD", status:"", cost:"", note:"Not a social." }, + { date:"2026-11-18", time:"8:00–10:30pm", title:"Movie Night + Menchie's", where:"TBD", kind:"b", lead:"Daryen", status:"Idea", cost:"Self-pay froyo", note:"" }, + { date:"2026-11-21", time:"2:00–4:00pm", title:"Uno on the Glade", where:"Memorial Glade", kind:"b", lead:"TBD", status:"Idea", cost:"Free", note:"Ends before sunset. Rain backup is a campus room." }, + { date:"2026-12-04", time:"7:00–9:00pm", title:"Karaoke night", where:"Jaguar Karaoke, 2516 Durant Ave", kind:"b", lead:"TBD", status:"Idea", cost:"Self-pay, about $7 per person per hour", note:"Last social before RRR week." }, ]; const SOCIALS_KEY = "vin-socials-v1"; try { const saved = JSON.parse(localStorage.getItem(SOCIALS_KEY) || "null"); if (Array.isArray(saved) && saved.length) socials.splice(0, socials.length, ...saved); } catch (_) {} const saveSocials = () => { try { localStorage.setItem(SOCIALS_KEY, JSON.stringify(socials)); } catch (_) {} }; let editEv = null; // index being edited, or "new" -const kinds = { All:null, Berkeley:"b", "UCSC × UCB":"x", Online:"o" }; +const kinds = { All:null, Berkeley:"b", Online:"o" }; let socialFilter = "All", openEv = null; let viewMonth = (() => { const t = today.getFullYear() === 2026 && today.getMonth() >= 9 && today.getMonth() <= 11 ? today : D("2026-10-01"); return new Date(t.getFullYear(), t.getMonth(), 1); })(); const showEv = s => s.kind === "org" || socialFilter === "All" || s.kind === kinds[socialFilter]; @@ -567,7 +562,7 @@

    Board meeting · Sep 21, 2026

    const card = document.getElementById("ev-card"); if (editEv !== null) { - const s = editEv === "new" ? { date: "", time: "", title: "", where: "", kind: "b", lead: "Pierce", status: "Idea", cost: "", note: "" } : socials[editEv]; + const s = editEv === "new" ? { date: "", time: "", title: "", where: "", kind: "b", lead: "TBD", status: "Idea", cost: "", note: "" } : socials[editEv]; const opt = (v, cur, label) => ``; card.hidden = false; card.innerHTML = `

    ${editEv === "new" ? "Add a social" : "Edit " + esc(s.title)}

    @@ -579,7 +574,7 @@

    Board meeting · Sep 21, 2026

    - +
    @@ -591,7 +586,7 @@

    Board meeting · Sep 21, 2026

    ${editEv !== "new" ? `` : ""} -

    Preview: edits save in this browser only. On the live site, Pierce and Rana can edit and everyone sees the change.

    +

    Preview: edits save in this browser only. On the live site, the admin can edit and everyone sees the change.

    `; } else if (openEv === null) card.hidden = true; @@ -608,7 +603,7 @@

    Board meeting · Sep 21, 2026

    `; } - const tagName = { b: "Berkeley", x: "UCSC × UCB", o: "Online", org: "Org date" }; + const tagName = { b: "Berkeley", o: "Online", org: "Org date" }; document.getElementById("agenda").innerHTML = socials.filter(showEv).map(s => { const d = D(s.date), past = d < today; return `
  • ${fmt(d, { weekday: "short", month: "short", day: "numeric" })} diff --git a/lib/board/data.ts b/lib/board/data.ts index 13304e0..6561b96 100644 --- a/lib/board/data.ts +++ b/lib/board/data.ts @@ -1,9 +1,12 @@ import "server-only"; import { db } from "./db"; import { ensureSetup } from "./setup"; +import type { BoardRole } from "./roles"; +import { UNASSIGNED } from "./static"; import type { BoardData } from "./types"; -export async function loadBoard(): Promise { +// Tasks with no owner yet go to admins only; everyone else never gets them. +export async function loadBoard(role: BoardRole): Promise { await ensureSetup(); const sql = db(); const [members, assignments, submissions, socials, announcements, meetings] = await Promise.all([ @@ -12,7 +15,8 @@ export async function loadBoard(): Promise { FROM board_members ORDER BY sort, id`, sql`SELECT id, round, person, person_role AS "personRole", task, proof_required AS proof, due_date AS "dueDate", status, status_note AS "statusNote" - FROM board_assignments ORDER BY round DESC, sort, id`, + FROM board_assignments ${role === "admin" ? sql`` : sql`WHERE person <> ${UNASSIGNED}`} + ORDER BY round DESC, sort, id`, sql`SELECT id, person, assignment_id AS "assignmentId", task_label AS "taskLabel", filenames, created_at AS "createdAt" FROM board_submissions ORDER BY created_at`, diff --git a/lib/board/roles.ts b/lib/board/roles.ts index cebb4cd..4ea66cd 100644 --- a/lib/board/roles.ts +++ b/lib/board/roles.ts @@ -1,7 +1,7 @@ // Who can do what on /board. People sign in with one of three passwords, // all set as environment variables in Railway: // BOARD_PASSWORD everyone on the board: view and submit -// BOARD_SOCIALS_PASSWORD socials editor (Pierce): also add/edit/delete socials +// BOARD_SOCIALS_PASSWORD socials editor (no one yet; set it to add one): also add/edit/delete socials // BOARD_ADMIN_PASSWORD admin (Rana): edit everything // Where assignment submissions are emailed. diff --git a/lib/board/setup.ts b/lib/board/setup.ts index a8e3661..2c06601 100644 --- a/lib/board/setup.ts +++ b/lib/board/setup.ts @@ -55,6 +55,7 @@ async function setup() { if (await empty("board_meetings")) { for (const m of starter.meetings) await sql`INSERT INTO board_meetings ${sql(m)}`; } + await octoberUpdates(sql); } const OLD_IAN = "Start the /board page on getvindicated.org from this preview"; @@ -82,3 +83,56 @@ async function retaskRound2(sql: ReturnType) { } }); } + +type Round2 = [string, string, [string, string][]][]; + +// Board changes from Oct 12, applied once to a database that was set up +// before them (a fresh one already has them from starter-data.json): +// Pierce leaves the board; his court booking goes to Daryen and his other +// tasks wait in the admin-only Unassigned group; four members' roles are +// set; the socials calendar is replaced and the UCSC x UCB type is retired. +async function octoberUpdates(sql: ReturnType) { + const round2 = starter.round2 as Round2; + const tasksOf = (person: string) => round2.find(([p]) => p === person)!; + await sql.begin(async (tx) => { + const [first] = await tx`INSERT INTO board_changes (name) VALUES ('2026-10-12-board-updates') + ON CONFLICT DO NOTHING RETURNING name`; + if (!first) return; + + // Pierce + await tx`DELETE FROM board_members + WHERE lower(email) = 'pierce.kosobayashi@berkeley.edu' OR name ILIKE '%Kosobayashi%'`; + await tx`DELETE FROM board_submissions WHERE person = 'Pierce'`; + await tx`DELETE FROM board_assignments WHERE person = 'Pierce'`; + + // His round 2 tasks: Daryen's court booking, then the unowned three. + const add = async (person: string, task: string, proof: string, role: string, sort: number) => { + const [exists] = await tx`SELECT 1 FROM board_assignments WHERE round = 2 AND person = ${person} AND task = ${task}`; + if (exists) return; + await tx`INSERT INTO board_assignments (round, person, person_role, task, proof_required, due_date, sort) + VALUES (2, ${person}, ${role}, ${task}, ${proof}, ${starter.dueRound2}, ${sort})`; + }; + const [{ max }] = await tx`SELECT coalesce(max(sort), 0) AS max FROM board_assignments WHERE round = 2 AND person = 'Daryen'`; + const [, daryenRole, daryen] = tasksOf("Daryen"); + const court = daryen[daryen.length - 1]; + await add("Daryen", court[0], court[1], daryenRole, max + 1); + const [unassigned, , unowned] = tasksOf("Unassigned"); + for (const [i, [task, proof]] of unowned.entries()) await add(unassigned, task, proof, "", -unowned.length + i); + + // Roles on the chart + await tx`UPDATE board_members SET role = 'Outreach Lead · Social Media', team = 'Outreach', is_lead = true, tier = NULL + WHERE name ILIKE 'Chloe%Lin'`; + await tx`UPDATE board_members SET role = 'Research Analyst', team = 'Research & Legal', is_lead = false, tier = NULL + WHERE name ILIKE 'Rami%Dakak'`; + await tx`UPDATE board_members SET role = 'President · Research Lead', team = 'Executive', tier = 0, also_team = 'Research & Legal' + WHERE name ILIKE 'Rana%Darwich'`; + await tx`UPDATE board_members SET role = 'Software Lead', team = 'Software', is_lead = true, tier = NULL + WHERE name ILIKE 'Ian%Houssaye'`; + + // Socials: exactly the starter list, and no more UCSC x UCB type. + await tx`DELETE FROM board_socials`; + for (const s of starter.socials) await tx`INSERT INTO board_socials ${tx(s)}`; + await tx`ALTER TABLE board_socials DROP CONSTRAINT IF EXISTS board_socials_kind_check`; + await tx`ALTER TABLE board_socials ADD CONSTRAINT board_socials_kind_check CHECK (kind IN ('b', 'o', 'org'))`; + }); +} diff --git a/lib/board/starter-data.json b/lib/board/starter-data.json index cc08a14..eec1c2c 100644 --- a/lib/board/starter-data.json +++ b/lib/board/starter-data.json @@ -1,6 +1,24 @@ { "dueRound2": "2026-10-12", "round2": [ + [ + "Unassigned", + "", + [ + [ + "RSVP form for VINdiBasketball", + "Form link" + ], + [ + "Send invites for every social to the full member email list", + "Screenshot of the sent email showing recipients" + ], + [ + "Keep the Socials calendar current", + "The updated calendar" + ] + ] + ], [ "Chloe", "Outreach Lead · Social Media", @@ -33,28 +51,6 @@ ] ] ], - [ - "Pierce", - "Socials", - [ - [ - "VINdiBasketball: set the date and book an RSF court", - "Booking confirmation" - ], - [ - "RSVP form for basketball, sent to Chloe to post", - "Form link" - ], - [ - "Send invites for every social on the Socials calendar to the full member email list", - "Screenshot of the sent email showing the recipients" - ], - [ - "Keep the Socials calendar current: confirm each location and update it on the page", - "The updated calendar" - ] - ] - ], [ "Ian", "Software Lead", @@ -86,6 +82,10 @@ [ "Call 5 local mechanics about recording a short video with us (carried over from round 1)", "Call log: shop name, number, time called, what they said" + ], + [ + "Book an RSF court for VINdiBasketball, Sat Oct 17, 2–4pm", + "Booking confirmation" ] ] ], @@ -139,12 +139,6 @@ "missing", "Not done" ], - [ - "Pierce", - "Event ideas doc, workshop timeline, connect with Halima", - "missing", - "Not done" - ], [ "Jordan", "Project spec, connect with James, answer Ray", @@ -159,10 +153,10 @@ "title": "VINdiBasketball", "location": "RSF", "kind": "b", - "lead": "Pierce", + "lead": "TBD", "status": "Planning", - "cost": "Free. Optional boba after, self-pay.", - "note": "First Berkeley social. Court booking and RSVP form due Mon, Oct 12." + "cost": "Free", + "note": "" }, { "date": "2026-10-21", @@ -170,21 +164,32 @@ "title": "Skribbl night", "location": "Discord", "kind": "o", - "lead": "Chloe", + "lead": "TBD", "status": "Planning", "cost": "Free", - "note": "Open to all three chapters. Chloe posts the link that afternoon." + "note": "" }, { "date": "2026-10-24", - "time": "Afternoon", - "title": "UCSC × UCB social", - "location": "San Francisco · spot TBD", - "kind": "x", - "lead": "Rana + Ashwin", + "time": "9:30am–12:00pm", + "title": "Grizzly Peak hike", + "location": "Meet at the Big C trailhead, Stadium Rim Way", + "kind": "b", + "lead": "TBD", + "status": "Idea", + "cost": "Free", + "note": "Steep in parts, so bring water. Moves to Nov 21 if it rains." + }, + { + "date": "2026-10-27", + "time": "7:00–9:00pm", + "title": "Pumpkin carving", + "location": "Campus room TBD", + "kind": "b", + "lead": "TBD", "status": "Idea", - "cost": "Self-pay", - "note": "Combined social with UCSC. Location still being decided." + "cost": "About $5 a pumpkin, from the $240", + "note": "" }, { "date": "2026-10-30", @@ -192,10 +197,10 @@ "title": "Glade Night + Chipotle", "location": "Memorial Glade", "kind": "b", - "lead": "Pierce", + "lead": "TBD", "status": "Planning", - "cost": "Chipotle catering from the $240, if the Oct 13 budget allows", - "note": "Halloween eve, costumes optional. Last week of daylight before clocks change." + "cost": "Chipotle from the $240 if the budget allows", + "note": "" }, { "date": "2026-11-06", @@ -203,10 +208,10 @@ "title": "Price Is Right demo", "location": "", "kind": "org", - "lead": "Noirit", + "lead": "TBD", "status": "", "cost": "", - "note": "Not a social. No socials Nov 5–15." + "note": "Not a social." }, { "date": "2026-11-18", @@ -214,21 +219,32 @@ "title": "Movie Night + Menchie's", "location": "TBD", "kind": "b", - "lead": "Daryen (movie), Chloe (Menchie's)", + "lead": "Daryen", "status": "Idea", "cost": "Self-pay froyo", "note": "" }, { "date": "2026-11-21", - "time": "1:00–4:00pm", - "title": "UCSC × UCB basketball", - "location": "Irvington Park, 41825 Blacow Rd, Fremont", - "kind": "x", - "lead": "Pierce + UCSC lead", + "time": "2:00–4:00pm", + "title": "Uno on the Glade", + "location": "Memorial Glade", + "kind": "b", + "lead": "TBD", "status": "Idea", "cost": "Free", - "note": "Meet-in-the-middle game, separate from VINdiBasketball. Outdoor court, first come. Backup: Karl Nordvik Park." + "note": "Ends before sunset. Rain backup is a campus room." + }, + { + "date": "2026-12-04", + "time": "7:00–9:00pm", + "title": "Karaoke night", + "location": "Jaguar Karaoke, 2516 Durant Ave", + "kind": "b", + "lead": "TBD", + "status": "Idea", + "cost": "Self-pay, about $7 per person per hour", + "note": "Last social before RRR week." } ], "announcements": [ diff --git a/lib/board/static.ts b/lib/board/static.ts index ee542ba..dc044ba 100644 --- a/lib/board/static.ts +++ b/lib/board/static.ts @@ -60,8 +60,10 @@ export const WEEKS: { n: number; start: string; end: string; label: string; goal // Socials calendar: months people can page through (0-based month index). export const SOCIAL_MONTHS = { year: 2026, first: 9, last: 11 }; export const SOCIAL_STATUSES = ["Idea", "Planning", "Confirmed", "Completed"] as const; -export const SOCIAL_KINDS: { key: "b" | "x" | "o"; label: string }[] = [ +export const SOCIAL_KINDS: { key: "b" | "o"; label: string }[] = [ { key: "b", label: "Berkeley" }, - { key: "x", label: "UCSC × UCB" }, { key: "o", label: "Online" }, ]; + +// Round tasks with no owner yet. Only admins see them. +export const UNASSIGNED = "Unassigned"; diff --git a/lib/board/tables.ts b/lib/board/tables.ts index a5e8e16..4032da6 100644 --- a/lib/board/tables.ts +++ b/lib/board/tables.ts @@ -46,7 +46,7 @@ export const TABLES = { date: { column: "date", check: date("a date") }, time: { column: "time", check: text(100), onCreate: "" }, location: { column: "location", check: text(300), onCreate: "" }, - kind: { column: "kind", check: oneOf(["b", "x", "o", "org"], "a type"), onCreate: "b" }, + kind: { column: "kind", check: oneOf(["b", "o", "org"], "a type"), onCreate: "b" }, lead: { column: "lead", check: text(200), onCreate: "" }, status: { column: "status", check: oneOf(["", "Idea", "Planning", "Confirmed", "Completed"], "a status"), onCreate: "Idea" }, cost: { column: "cost", check: text(300), onCreate: "" }, diff --git a/lib/board/types.ts b/lib/board/types.ts index 87bcc9d..43720d3 100644 --- a/lib/board/types.ts +++ b/lib/board/types.ts @@ -36,7 +36,7 @@ export type Submission = { createdAt: string; }; -export type SocialKind = "b" | "x" | "o" | "org"; +export type SocialKind = "b" | "o" | "org"; export type Social = { id: number; diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index bcacd9f..2a3d169 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -629,10 +629,6 @@ "position": "Technical Writing Lead", "bio": "Brisa Gómez is a fourth-year Psychology major at UCLA and the Technical Writing Lead at UCLA. Through their writing and translation projects, Brisa wants to make resources that are easy to use and inclusive of vulnerable populations." }, - "Pierce Kosobayashi": { - "position": "Project Manager", - "bio": "Pierce Kosobayashi is a fourth-year Media Studies and Education student at UC Berkeley. He brings a background in digital and retail automotive sales and finance, along with experience in OEM sales analysis and marketing, to his work as Project Manager at VINdicated. A car enthusiast, Pierce wants to use what he learned in the industry to help future buyers get through the process. Outside VINdicated, he enjoys ocean swimming, hiking, and spending time with his dog and cat." - }, "Chino Sawatyanont": { "position": "Software Engineer", "bio": "Chino is an Electrical and Computer Engineering major at UC Berkeley and a Software Engineer for the VINdicated chapter there. He brings extensive experience combining technical knowledge with societal impact, having previously focused on supporting underprivileged communities in Thailand. Chino hopes to bring that same drive for impact to VINdicated's mission." diff --git a/lib/team.ts b/lib/team.ts index c7e0f86..80b2165 100644 --- a/lib/team.ts +++ b/lib/team.ts @@ -175,12 +175,6 @@ export const team: TeamMember[] = [ photo: "/team/brisa.png", chapter: "ucla", }, - { - name: "Pierce Kosobayashi", - major: "Media Studies & Education", - photo: "/team/pierce.jpg", - chapter: "ucb", - }, { name: "Chino Sawatyanont", major: "Electrical & Computer Engineering", diff --git a/public/team/pierce.jpg b/public/team/pierce.jpg deleted file mode 100644 index 8a82dbb..0000000 Binary files a/public/team/pierce.jpg and /dev/null differ diff --git a/scripts/board-members.example.json b/scripts/board-members.example.json index 22268b4..b6cdcb3 100644 --- a/scripts/board-members.example.json +++ b/scripts/board-members.example.json @@ -141,13 +141,6 @@ "phone": "(555) 555-0118", "lead": true }, - { - "name": "Pierce Matthew Kosobayashi", - "role": "Socials", - "team": "Outreach", - "email": "member20@example.com", - "phone": "(555) 555-0119" - }, { "name": "Rami K Dakak", "role": "Research Analyst", @@ -155,4 +148,4 @@ "email": "member21@example.com", "phone": "(555) 555-0120" } -] \ No newline at end of file +] diff --git a/scripts/board-schema.sql b/scripts/board-schema.sql index 0b72b77..d9b629f 100644 --- a/scripts/board-schema.sql +++ b/scripts/board-schema.sql @@ -45,7 +45,7 @@ CREATE TABLE IF NOT EXISTS board_socials ( time text NOT NULL DEFAULT '', title text NOT NULL, location text NOT NULL DEFAULT '', - kind text NOT NULL DEFAULT 'b' CHECK (kind IN ('b', 'x', 'o', 'org')), + kind text NOT NULL DEFAULT 'b' CHECK (kind IN ('b', 'o', 'org')), lead text NOT NULL DEFAULT '', status text NOT NULL DEFAULT '', cost text NOT NULL DEFAULT '', @@ -69,3 +69,9 @@ CREATE TABLE IF NOT EXISTS board_meetings ( summary text NOT NULL DEFAULT '', recording_url text NOT NULL DEFAULT '' ); + +-- One-time data changes already applied to this database (see lib/board/setup.ts). +CREATE TABLE IF NOT EXISTS board_changes ( + name text PRIMARY KEY, + applied_at timestamptz NOT NULL DEFAULT now() +);