Skip to content
Open
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
5 changes: 4 additions & 1 deletion app/esoc/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Suspense } from "react";
import { CohortScope } from "@/components/ui/cohort-scope";
import { EsocRecord } from "@/components/esoc-record";

export const metadata = {
Expand All @@ -9,7 +10,9 @@ export const metadata = {
export default function ESocPage() {
return (
<Suspense fallback={null}>
<EsocRecord />
<CohortScope>
<EsocRecord />
</CohortScope>
</Suspense>
);
}
5 changes: 4 additions & 1 deletion app/gsoc/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Suspense } from "react";
import { CohortScope } from "@/components/ui/cohort-scope";
import { GsocStats } from "@/components/gsoc-stats";

export const metadata = {
Expand All @@ -11,7 +12,9 @@ export default function GsocPage() {
<div className="bg-transparent text-white selection:bg-cyan-400 selection:text-black">
{/* The year lives in the query string; reading it needs a boundary. */}
<Suspense fallback={null}>
<GsocStats />
<CohortScope>
<GsocStats />
</CohortScope>
</Suspense>
</div>
);
Expand Down
5 changes: 4 additions & 1 deletion app/gssoc/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Suspense } from "react";
import { CohortScope } from "@/components/ui/cohort-scope";
import { GSSoCHallOfFame } from "@/components/gssoc-hall-of-fame";

export const metadata = {
Expand All @@ -9,7 +10,9 @@ export const metadata = {
export default function GSSoCPage() {
return (
<Suspense fallback={null}>
<GSSoCHallOfFame />
<CohortScope>
<GSSoCHallOfFame />
</CohortScope>
</Suspense>
);
}
5 changes: 4 additions & 1 deletion app/opensource/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Suspense } from "react";
import { CohortScope } from "@/components/ui/cohort-scope";
import { OpenSourceImpact } from "@/components/open-source-impact";

export const metadata = {
Expand All @@ -11,7 +12,9 @@ export default function OpenSourcePage() {
// boundary; without it the whole route would opt out of prerendering.
return (
<Suspense fallback={null}>
<OpenSourceImpact />
<CohortScope>
<OpenSourceImpact />
</CohortScope>
</Suspense>
);
}
5 changes: 4 additions & 1 deletion app/pr-stats/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Suspense } from "react";
import { CohortScope } from "@/components/ui/cohort-scope";
import { PRStats } from "@/components/pr-stats";

export const metadata = {
Expand All @@ -12,7 +13,9 @@ export default function PRStatsPage() {
{/* The year lives in the query string, and reading it needs a boundary
so the rest of the page can still be prerendered. */}
<Suspense fallback={null}>
<PRStats />
<CohortScope>
<PRStats />
</CohortScope>
</Suspense>
</div>
);
Expand Down
5 changes: 4 additions & 1 deletion app/quality-prs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Suspense } from "react";
import { CohortScope } from "@/components/ui/cohort-scope";
import { QualityPRsList } from "@/components/quality-prs-list";

export const metadata = {
Expand All @@ -11,7 +12,9 @@ export default function QualityPRsPage() {
<div className="bg-transparent text-white selection:bg-cyan-400 selection:text-black">
{/* The year lives in the query string; reading it needs a boundary. */}
<Suspense fallback={null}>
<QualityPRsList />
<CohortScope>
<QualityPRsList />
</CohortScope>
</Suspense>
</div>
);
Expand Down
10 changes: 2 additions & 8 deletions components/gsoc-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,7 @@ export function GsocStats() {
fetchData();
}, [cohort.id]);

// Derived rather than cleared in the effect, so the previous year group's
// breakdown never renders under the new heading while the fetch is in flight.
const stale = data !== null && data.year !== cohort.id;

if (loading || error || !data || stale) {
if (loading || error || !data) {
return (
<div className="min-h-screen bg-gradient-to-b from-black via-neutral-950 to-black">
<div className="flex justify-center pt-24">
Expand All @@ -105,9 +101,7 @@ export function GsocStats() {
);
}

// A member picked in the other year group is not in this list; falling back
// to "all" beats rendering an empty breakdown that looks like real data.
const activeMember = data.members.some(m => m.github === selectedMember) ? selectedMember : 'all';
const activeMember = selectedMember;
const filteredGsocPRs = activeMember === 'all'
? data.members.flatMap(m => m.gsocPRs)
: data.members.find(m => m.github === activeMember)?.gsocPRs || [];
Expand Down
8 changes: 1 addition & 7 deletions components/pr-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,7 @@ export function PRStats() {
return () => clearInterval(refreshInterval);
}, [cohort.id]);

// While the next year group is still loading, `stats` still holds the last
// one. Derived rather than cleared in the effect: clearing would mean a
// setState during render-effect, and the question "is this data for the year
// the URL asks for?" is answerable from what we already have.
const stale = stats !== null && stats.year !== cohort.id;

if (loading || error || !stats || stale) {
if (loading || error || !stats) {
return (
<div className="min-h-screen bg-gradient-to-b from-black via-neutral-950 to-black">
{/* The switcher stays on screen while loading. Counting the second
Expand Down
11 changes: 2 additions & 9 deletions components/quality-prs-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,7 @@ export function QualityPRsList() {
fetchPRs();
}, [cohort.id]);

// Derived, not cleared in the effect: while the next year group loads, the
// previous one's pull requests are still in state and must not render under
// the new heading.
const stale = data !== null && data.year !== cohort.id;

if (loading || error || !data || stale) {
if (loading || error || !data) {
return (
<div className="min-h-screen bg-gradient-to-b from-black via-neutral-950 to-black">
<div className="flex justify-center pt-24">
Expand All @@ -87,9 +82,7 @@ export function QualityPRsList() {

// Get unique authors for filter
const authors = Array.from(new Set(data.prs.map(pr => pr.author.github)));
// A filter left over from the other year group names somebody who is not in
// this list, which would silently render zero results. Fall back to "all".
const activeAuthor = authors.includes(filterAuthor) ? filterAuthor : 'all';
const activeAuthor = filterAuthor;

// Filter and sort PRs
let filteredPRs = activeAuthor === 'all'
Expand Down
28 changes: 28 additions & 0 deletions components/ui/cohort-scope.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import { Fragment, type ReactNode } from "react";
import { useSearchParams } from "next/navigation";
import { cohortFromParam } from "@/lib/cohorts";

/**
* Remounts its subtree when the selected year group changes.
*
* Switching year replaces every piece of state a page holds: the fetched
* numbers, whether a fetch is in flight, any error from the last one, and the
* author or member filters, which list people who are not in the other cohort.
* Resetting those one by one inside an effect is how this broke — clearing the
* data but not the loading flag left the page rendering neither a spinner nor
* a result, so it went blank for as long as the next fetch took.
*
* Keying the subtree is React's own answer to "reset all state when an input
* changes", and it cannot drift: new state is whatever the component starts
* with, so a value added later is covered without anybody remembering to reset
* it here.
*
* It reads the year on the client so the pages above it stay prerenderable;
* reading searchParams in the page itself would make the whole route dynamic.
*/
export function CohortScope({ children }: { children: ReactNode }) {
const cohort = cohortFromParam(useSearchParams().get("year"));
return <Fragment key={cohort.id}>{children}</Fragment>;
}