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
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
6 changes: 4 additions & 2 deletions BOARD_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 0 additions & 4 deletions app/board/board.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion app/board/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <BoardNotice text="The board couldn't reach its database. Try again in a minute; if it keeps happening, tell Rana." />;
Expand Down
49 changes: 46 additions & 3 deletions components/board/AssignmentsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AssignmentStatus, string> = {
Expand All @@ -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<Set<string>>(new Set());
const [ticks, setTicks] = useState<Record<string, boolean>>({});
Expand Down Expand Up @@ -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[] = [
Expand Down Expand Up @@ -127,6 +128,48 @@ export function AssignmentsTab() {
/>
)}

{unassigned.length > 0 && (
<details className="assign" id="as-unassigned" open>
<summary>
<span className="who">
Unassigned <span className="role">· only admins see this</span>
</span>
<span />
<span className="st-prog">{unassigned.length} to assign</span>
</summary>
<div className="body">
<p>Edit a task and choose a person to give it to them.</p>
<ul className="tasks">
{unassigned.map((a) => (
<li key={a.id}>
<div>
<span className="task-text">{a.task}</span>
<div className="proof">
<strong>Proof:</strong> {a.proof}
</div>
<div className="admin-row">
<button type="button" className="linkbtn" onClick={() => setEditing(a.id)}>
Edit task
</button>
</div>
{editingTask?.id === a.id && (
<RowEditor
title="Edit task"
table="assignments"
id={a.id}
fields={taskFields}
initial={{ person: a.person, personRole: a.personRole, task: a.task, proof: a.proof, dueDate: a.dueDate ?? "", status: a.status }}
onClose={() => setEditing(null)}
/>
)}
</div>
</li>
))}
</ul>
</div>
</details>
)}

<div>
{shown.map((p) => {
const subs = data.submissions.filter((s) => s.person === p.person && (!s.assignmentId || p.items.some((i) => i.id === s.assignmentId)));
Expand Down
6 changes: 3 additions & 3 deletions components/board/SocialsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = { b: "Berkeley", x: "UCSC × UCB", o: "Online", org: "Org date" };
const TAG: Record<string, string> = { b: "Berkeley", o: "Online", org: "Org date" };

const SOCIAL_FIELDS: FieldDef[] = [
{ key: "title", label: "Name" },
Expand Down Expand Up @@ -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)}
/>
Expand Down Expand Up @@ -233,7 +233,7 @@ export function SocialsTab() {
);
})}
</ul>
<p style={{ marginTop: 12 }}>Status runs Idea → Planning → Confirmed → Completed. Pierce leads socials unless noted.</p>
<p style={{ marginTop: 12 }}>Status runs Idea → Planning → Confirmed → Completed.</p>
</section>
);
}
9 changes: 7 additions & 2 deletions components/board/derive.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string, PersonTasks>();
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 };
Expand All @@ -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;
41 changes: 18 additions & 23 deletions docs/board-preview.html
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand Down Expand Up @@ -306,7 +305,7 @@ <h3 id="m-label" aria-live="polite"></h3>

<h3 style="margin-top:28px">All socials</h3>
<ul class="agenda" id="agenda"></ul>
<p class="muted small" style="margin-top:12px">Status runs Idea → Planning → Confirmed → Completed. Pierce leads socials unless noted.</p>
<p class="muted small" style="margin-top:12px">Status runs Idea → Planning → Confirmed → Completed.</p>
</section>

<!-- MEETINGS -->
Expand Down Expand Up @@ -351,7 +350,6 @@ <h3>Board meeting · Sep 21, 2026</h3>
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 = {};
Expand Down Expand Up @@ -423,18 +421,14 @@ <h3>Board meeting · Sep 21, 2026</h3>
{ 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"),
Expand All @@ -447,7 +441,6 @@ <h3>Board meeting · Sep 21, 2026</h3>
["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`;
Expand Down Expand Up @@ -520,21 +513,23 @@ <h3>Board meeting · Sep 21, 2026</h3>
}

/* ---------- 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];
Expand Down Expand Up @@ -567,7 +562,7 @@ <h3>Board meeting · Sep 21, 2026</h3>

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) => `<option value="${v}" ${v === cur ? "selected" : ""}>${label}</option>`;
card.hidden = false;
card.innerHTML = `<h3>${editEv === "new" ? "Add a social" : "Edit " + esc(s.title)}</h3>
Expand All @@ -579,7 +574,7 @@ <h3>Board meeting · Sep 21, 2026</h3>
</div>
<label for="e-where">Location<input type="text" id="e-where" value="${esc(s.where)}" placeholder="Place name and address"></label>
<div class="evgrid">
<label for="e-kind">Type<select id="e-kind">${opt("b", s.kind, "Berkeley")}${opt("x", s.kind, "UCSC × UCB")}${opt("o", s.kind, "Online")}</select></label>
<label for="e-kind">Type<select id="e-kind">${opt("b", s.kind, "Berkeley")}${opt("o", s.kind, "Online")}</select></label>
<label for="e-status">Status<select id="e-status">${["Idea","Planning","Confirmed","Completed"].map(x => opt(x, s.status, x)).join("")}</select></label>
</div>
<label for="e-lead">Lead<input type="text" id="e-lead" value="${esc(s.lead)}"></label>
Expand All @@ -591,7 +586,7 @@ <h3>Board meeting · Sep 21, 2026</h3>
${editEv !== "new" ? `<button class="chip" type="button" data-edelete="1" style="margin-left:auto">${confirmDel ? "Click again to delete" : "Delete"}</button>` : ""}
</div>
<p id="e-err" hidden style="font-weight:400"></p>
<p class="small muted" style="margin:0">Preview: edits save in this browser only. On the live site, Pierce and Rana can edit and everyone sees the change.</p>
<p class="small muted" style="margin:0">Preview: edits save in this browser only. On the live site, the admin can edit and everyone sees the change.</p>
</form>`;
}
else if (openEv === null) card.hidden = true;
Expand All @@ -608,7 +603,7 @@ <h3>Board meeting · Sep 21, 2026</h3>
<div><button class="linkbtn" data-eedit="${openEv}">Edit details or location</button></div>`;
}

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 `<li class="${past ? "muted" : ""}"><span class="when">${fmt(d, { weekday: "short", month: "short", day: "numeric" })}</span>
Expand Down
Loading