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
59 changes: 59 additions & 0 deletions web/app/(site)/join/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import Note from "@/components/fx/Note";
// the next action, and a closing "here is what to do next" band underneath a
// half-filled form is an invitation to abandon it.
//
// It does now close on a band, but the distinction above is what that band is
// built to respect — it contains no link, so it argues for the form rather than
// offering somewhere else to be. Anything added down there has to clear the same
// bar: no exits under an unfinished form.
//
// It is also absent from PAGES, so it appears in neither the nav strip nor beside
// the other routes in the footer. The nav carries it as a filled button at the
// other end of the bar — putting the same word in the strip as well would be the
Expand Down Expand Up @@ -180,6 +185,60 @@ export default function Join() {
<JoinGate />
</div>
</section>

{/* ---- The two futures ---------------------------------------------
The closing band, and the exception the comment at the top of this
file now carries: it is not a NextAction. Every other route ends by
handing you somewhere else to go, which under a half-filled form
would be an exit. This band has no link in it at all. It restates
the decision already on the screen and then stops, so the only
thing to do with it is scroll back up and finish.

NOT INSIDE ApplyForm. It lived in the card for one revision and the
card is the wrong container: at 15px inside a 7-unit padded tile it
read as a third disclaimer under the two grey notes about data, and
disclaimers are what people skip. On the page at display-md it is
the loudest type below the hero.

The asymmetry is the whole argument. Both eyebrows take .label, so
they read as one pair rather than a warning and a reward — the
weight sits in the body copy instead: `dust` on the left, `ink` on
the right, plus the accent rule down the right column. The future
where you did nothing is literally the dimmer of the two.
Do not try to colour the eyebrow to sharpen it: .label sets its own
colour and every custom class in globals.css is declared after
@tailwind utilities, so a text-* utility on it silently does
nothing. See the note on .page-top there. */}
<section className="section pb-24 pt-4">
{/* One column until lg, and lg rather than sm because these are
30-word paragraphs at display size — the point where two of them
fit side by side without either dropping to four words a line is
a good deal wider than the point where two columns fit. */}
<div
className="grid gap-10 border-t border-seam pt-12 lg:grid-cols-2 lg:gap-14"
data-reveal-group
>
<div>
<p className="label">If you close this tab without applying</p>
<p className="mt-4 text-display-md font-medium text-dust text-balance">
You&apos;ll open it again in February. The same form, one semester
less, and a batch of students who already know how to review your
code.
</p>
</div>

{/* The accent rule is the second column's left edge at lg and its top
edge below that, so the band never loses the mark that says which
of the two futures the page is pointing at. */}
<div className="border-l-2 border-accent pl-7 lg:pl-14">
<p className="label">If you hit the button</p>
<p className="mt-4 text-display-md font-semibold text-ink text-balance">
Someone reads it this week. You show up regularly. By November
you&apos;re the one answering the questions.
</p>
</div>
</div>
</section>
</main>
);
}
14 changes: 12 additions & 2 deletions web/app/(site)/programmes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,18 @@ function ProgrammeField({ p }: { p: ProgrammeInfo }) {
<div className="border-t border-seam pt-5 sm:col-span-2">
<dt className="label">Who from SST has done it</dt>
<dd className="mt-2 text-sm leading-relaxed">
{/* The written answer, for programmes where a count and a standout
name say more than a list of selections could — see `ours` in
ProgrammeInfo. It sits above the derived names rather than
replacing them, so adding a selection later cannot silently drop
this line, and this line cannot silently hide a selection. */}
{p.ours && <p className="text-ink">{p.ours}</p>}
{ours.length > 0 ? (
<ul className="flex flex-wrap gap-x-5 gap-y-2">
<ul
className={`flex flex-wrap gap-x-5 gap-y-2${
p.ours ? " mt-2" : ""
}`}
>
{ours.map((a) => (
<li key={`${a.name}-${a.year}`} className="text-ink">
{a.url ? (
Expand All @@ -162,7 +172,7 @@ function ProgrammeField({ p }: { p: ProgrammeInfo }) {
</li>
))}
</ul>
) : (
) : p.ours ? null : (
// TIER-AWARE, because one sentence cannot serve both. The first
// version said "the first person from this college to get in has not
// been picked" for every programme — which is meaningless for
Expand Down
6 changes: 4 additions & 2 deletions web/components/CommunityBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
// reason — the numbers and the faces have to be real:
//
// 1. "Join 300+ Contributors". There is no 300 anywhere in this repo, and no
// field that could become one: the club records fifteen published
// selections, eight people running it, and one project with an API-verified
// field that could become one: the club records its published
// selections (SELECTIONS in club.ts, and the count moves as names come in —
// which is why it is not written out here), eight people running it, and
// one project with an API-verified
// merge count. A recruitment page for sixteen-year-olds that inflates its
// community by twentyfold is the kind of claim that costs the club its
// credibility the first time somebody counts. The label reads from
Expand Down
55 changes: 39 additions & 16 deletions web/components/Team.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,6 @@ function Node({
member,
diameter,
designation,
remit,
tone = "quiet",
priority = false,
maxWidth = CARD_MAX_W,
Expand All @@ -376,8 +375,11 @@ function Node({
};
diameter: string;
designation?: string;
/** Overrides member.remit. A desk's remit belongs to the desk, not the person. */
remit?: string;
/* The `remit` override that used to sit here is gone with its one caller. It
existed so a desk could push its own remit onto each member's hover card;
nothing does that any more, and a prop kept for a caller that no longer
exists is the next person's wrong turn. A remit on the card now means the
person holds an office that has one. */
tone?: "loud" | "quiet";
priority?: boolean;
maxWidth?: string;
Expand All @@ -386,6 +388,14 @@ function Node({
/** Which edge it is anchored to, so a card near the chart's edge stays on the page. */
tipAlign?: "start" | "center" | "end";
}) {
// Whether there is anything to say on hover. A node with nothing to say must not
// render a card: `.person-tip` is a styled bubble with padding and a border, so
// an empty one is a small dark rectangle that appears on hover and explains
// nothing. This became reachable the moment the desk stopped passing its remit
// down — three of its four members carry only a name.
const hasTip = Boolean(
member.remit || member.batch || member.highlights?.length,
);
return (
<div
className="mx-auto flex flex-col items-center px-3 text-center"
Expand Down Expand Up @@ -424,18 +434,22 @@ function Node({
is read out from the stacked list below, where it is plain text rather
than a hover state — which is also what a touch device gets, since the
chart only renders at lg+ and hover does not exist on a phone. */}
<div
aria-hidden
className="person-tip"
data-tip={tip}
data-align={tipAlign}
>
{member.batch && (
<p className="person-tip-batch">Batch {member.batch}</p>
)}
<p className="person-tip-remit">{remit ?? member.remit}</p>
<Highlights items={member.highlights} />
</div>
{hasTip && (
<div
aria-hidden
className="person-tip"
data-tip={tip}
data-align={tipAlign}
>
{member.batch && (
<p className="person-tip-batch">Batch {member.batch}</p>
)}
{member.remit && (
<p className="person-tip-remit">{member.remit}</p>
)}
<Highlights items={member.highlights} />
</div>
)}
</div>
<Caption designation={designation} name={member.name} tone={tone} />
{member.github && (
Expand Down Expand Up @@ -709,12 +723,21 @@ export default function Team() {
gridTemplateColumns: `repeat(${DESK.members.length}, minmax(0, 1fr))`,
}}
>
{/* NO REMIT PASSED DOWN, per the club. The desk's remit used to ride
on every member's hover card, which meant four faces in a row each
popping the same paragraph — the sentence is about the desk, not
about the person under the pointer, and reading it four times is
how you learn to stop hovering. It has not been deleted: the desk
label above these four still stands for it in the chart, and the
stacked list below writes it out once, in full, where it is read
aloud and where touch devices get it. What is left on hover is only
what belongs to the person — their batch and their highlights — and
a member with neither now gets no bubble at all. */}
{DESK.members.map((m, i) => (
<Node
key={m.name}
member={m}
diameter={DESK_D}
remit={DESK.remit}
tipAlign={align(DESK_MEMBER_X[i])}
/>
))}
Expand Down
2 changes: 1 addition & 1 deletion web/components/hall/ContribWall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
//
// * It is NOT per-student. One wall under the whole grid reads as a section
// motif; a strip behind each card would read as that person's activity, and
// none of these fifteen has activity data recorded anywhere in this repo.
// none of these students has activity data recorded anywhere in this repo.
//
// * The pattern is DETERMINISTIC, from a tiny integer hash rather than
// Math.random(). Random would differ between the server render and the client
Expand Down
3 changes: 2 additions & 1 deletion web/components/hall/Hall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@ export default function Hall() {

{/* THE OPEN SLOT, last in the grid.
Every other card is a record of something that already happened, which
makes the whole grid readable as a closed set — fifteen people, done. This
makes the whole grid readable as a closed set — this is who got picked,
done. This
one card says the list is still being written, and it is the only card here
that is an invitation rather than evidence.

Expand Down
2 changes: 1 addition & 1 deletion web/components/hero/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
// The evidence did not go anywhere. It moved to where it was already stronger:
// the floating chips still carry the API-verified merge count and the cohort
// size (see Hero.tsx), the figure block under the hero states the total, and the
// hall names all fifteen with links. Nobody lost a credit; the credit stopped
// hall names every one of them with links. Nobody lost a credit; the credit stopped
// being a leaderboard.
//
// NOTHING HERE IS A CLAIM. That is the other thing the change buys. A simulated
Expand Down
65 changes: 59 additions & 6 deletions web/content/club.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,25 @@ const GSOC_2025: [name: string, studyYear: string, org?: string][] = [
["Sauhard Gupta", "3rd year"],
];

/* The first selections outside GSoC, and the reason the wall stops being a GSoC
wall. Same shape and same rules as the arrays above: the org slot stays empty
until the club supplies it, and the card prints "org TBA" rather than a guess.

VIVEK SINGH SOLANKI APPEARS TWICE ON PURPOSE — once for GSoC 2026 above and once
for LFX below. Two selections into two programmes are two facts, and the wall is
a list of selections, not of people. Everything that counts PEOPLE dedupes by
name already (see NumbersStrip), so this adds a card without inflating the
member count. Note also that he is a different person from the "Vivek Singh"
selected into Summer of Bitcoin; the names are close enough that a future editor
will wonder, so: not a duplicate, and not a typo. */
const SOB_2026: [name: string, studyYear: string, org?: string][] = [
["Vivek Singh", "2nd year"],
];

const LFX_2026: [name: string, studyYear: string, org?: string][] = [
["Vivek Singh Solanki", "3rd year", "Besu"],
];

/**
* The published list. These render everywhere — local, preview and production.
*
Expand All @@ -399,16 +418,17 @@ const GSOC_2025: [name: string, studyYear: string, org?: string][] = [
* WHAT IS STILL MISSING, and it belongs here rather than in a ticket: not one of
* these entries has a proof `url`. The page's entire argument is "somebody else
* picked them, go and check", and until each name carries a link — the programme's
* accepted-projects page, the student's proposal, the announcement — these fifteen
* are the only claims on this site a reader cannot verify. The roster prints an em
* dash in the Proof column for each, which is honest but is not evidence. Add `url`
* per entry as the links come in; nothing else has to change.
* accepted-projects page, the student's proposal, the announcement — these are the
* only claims on this site a reader cannot verify. The roster prints an em dash in
* the Proof column for each, which is honest but is not evidence. Add `url` per
* entry as the links come in; nothing else has to change.
*
* The ten LFX / C4GT / SoB rows that used to pad this list to twenty-five are gone,
* deliberately. "Placeholder Seven, Example Foundation" is layout padding, not a
* selected student; publishing it would have put visible nonsense on a public page
* beside fifteen real people and undercut every real name next to it. Those
* programmes get real entries the same way these did.
* beside real people and undercut every real name next to it. Those programmes get
* real entries the same way these did — the SoB and LFX rows below are the first of
* them, supplied by the club per student rather than generated to fill the grid.
*/
export const SELECTIONS: Selection[] = [
...GSOC_2026.map(([name, studyYear, org]) => ({
Expand All @@ -427,6 +447,22 @@ export const SELECTIONS: Selection[] = [
org,
consented: true,
})),
...SOB_2026.map(([name, studyYear, org]) => ({
name,
programme: "SOB" as Programme,
year: "2026",
studyYear,
org,
consented: true,
})),
...LFX_2026.map(([name, studyYear, org]) => ({
name,
programme: "LFX" as Programme,
year: "2026",
studyYear,
org,
consented: true,
})),
];

/**
Expand Down Expand Up @@ -525,6 +561,20 @@ export type ProgrammeInfo = {
pays: string;
/** What OSC specifically does to get you in. This is the product. */
weDo: string;
/**
* What the club has to show for this programme, when a list of names is the
* wrong shape for it.
*
* The "who from SST has done it" row is normally DERIVED from SELECTIONS, and
* that stays the default: a selective programme's answer is a set of people who
* were each individually picked, and deriving it is what stops this page and the
* Hall of Fame disagreeing. But the open tier is a participation programme —
* dozens take part and there is nothing to be selected into — so the honest
* answer is a count plus whoever stood out, which no list of Selection rows can
* express without inventing a selection that never happened. Prose here, names
* in SELECTIONS; when both exist the row shows both.
*/
ours?: string;
url: string;
};

Expand Down Expand Up @@ -589,6 +639,8 @@ export const PROGRAMMES: ProgrammeInfo[] = [
"No stipend. Certificates, swag and a leaderboard — plus mentors, which is the part that is actually worth having.",
weDo:
"Nothing to prepare. Register when it opens and pick a project in a language you can already run. Use it to learn the mechanics — fork, branch, PR, review, merge — so the paid programmes below are not your first time using Git in anger.",
ours:
"30+ students participated in GSSoC '26. Top contributor from SST: Bhumi N Deshpande.",
url: "https://gssoc.girlscript.tech/",
},
{
Expand All @@ -601,6 +653,7 @@ export const PROGRAMMES: ProgrammeInfo[] = [
pays: "Swag, or a tree planted in your name. That is the whole reward.",
weDo:
"Have one project picked and built on your machine before 1 October, so you spend the month contributing rather than setting up. Come to a session in September and we will do it with you.",
ours: "Many of us had our first PRs merged here.",
url: "https://hacktoberfest.com/",
},
];
Expand Down
Loading