diff --git a/CLAUDE.md b/CLAUDE.md
index de21906c..45edc319 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,9 @@
Full spec + rollout checklist: **`docs/refined-design-system.md`** (read this before extending the look to more pages). Migrated so far: `/` (`src/pages/index.js`), `/projects` (`src/components/ProjectList/*`), the global NavBar (incl. a refined mobile layout — centered logo + refined dropdown), `/about`, `/about/judges`, `/about/mentors` (`Mentorship` rewritten as refined sections), `/about/success-stories`, `/about/completion`, `/sponsor`, `/nonprofits` (`NonProfitList` + new calm card `NonProfitListTileRefined`; original `NonProfitListTile` stays for event pages), `/blog` (`BlogPage`; reused `News` list kept), `/onboarding` (refined chrome; wizard logic intact; **July 2026 content overhaul** — 9 steps: Welcome, Mission, How It Works [full lifecycle incl. post-hackathon Definition of Done + project-status ladder], Get Involved [hacker/mentor/judge/volunteer/nonprofit role cards], Using the Site [site map + first-steps guide + "Anatomy of a project page" numbered walkthrough (problem → code → plan → people; mirrors ProblemStatement.js section order — keep in sync) + "public portfolio" band: GitHub/Slack/demo-videos/public-profile are what recruiters can review; work is documented as GitHub Issues for public credit — no issues in a repo → pull the code and write them like a PM], Slack, Introduce Yourself, FAQs, Feedback. New sections: `HowItWorksSection`/`RolesSection`/`WebsiteTourSection` in `src/components/Onboarding/`; `JudgingOverview`/`MentoringOverview`/`BuddySystem` are now orphaned — don't re-add them or their claims. FAQ answers must reflect real flows: there are NO project leads, NO commenting on projects, NO buddy system — joining a project = "Want to help?" toggle / its Slack channel / GitHub repo (write Issues like a PM if none exist) / next hackathon. **Design contract:** step content renders inside a scoped `onboardingTheme` ThemeProvider (in `pages/onboarding/index.js` — Fraunces headings, Hanken body, navy/terracotta palette, flat hairline Papers/Cards; don't reintroduce elevation shadows, hover-lifts, or rainbow chips) and every step opens with the shared `StepHeader` (renders `
+
+ )}
+
+ {progress.summary.orphan_submitted > 0 && (
+
+ {progress.summary.orphan_submitted} submitted score
+ {progress.summary.orphan_submitted === 1 ? " is" : "s are"} from a
+ judge who is no longer assigned to that team. They are not counted
+ in Results.
+
+ )}
+
+
+ {updatedAt ? `Updated ${updatedAt.toLocaleTimeString()}` : ""}
+ {!complete &&
+ " · refreshes every 30 seconds while this tab is open"}
+
+ >
+ ) : null}
+
+ !lockBusy && setConfirm(null)}
+ maxWidth="xs"
+ fullWidth
+ >
+
+ {confirm === "lock"
+ ? `Lock ${label} scoring?`
+ : `Unlock ${label} scoring?`}
+
+
+
+ {confirm === "lock"
+ ? `Judges will no longer be able to submit or change ${label} scores. Scores already submitted are kept. You can unlock at any time.`
+ : `Judges will be able to submit and change ${label} scores again.`}
+
+
+
+ setConfirm(null)} disabled={lockBusy}>
+ Cancel
+
+ applyLock(confirm === "lock")}
+ disabled={lockBusy}
+ startIcon={
+ lockBusy ? (
+
+ ) : confirm === "lock" ? (
+
+ ) : (
+
+ )
+ }
+ >
+ {confirm === "lock" ? "Lock scoring" : "Unlock scoring"}
+
+
+
+
+ );
+};
+
+export default JudgeProgressPanel;
diff --git a/src/components/admin/judging/__tests__/JudgeProgressPanel.test.js b/src/components/admin/judging/__tests__/JudgeProgressPanel.test.js
new file mode 100644
index 00000000..7fb3fe91
--- /dev/null
+++ b/src/components/admin/judging/__tests__/JudgeProgressPanel.test.js
@@ -0,0 +1,192 @@
+import React from "react";
+import {
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+ waitForElementToBeRemoved,
+ within,
+} from "@testing-library/react";
+
+jest.mock("../../../../lib/ga", () => ({
+ trackStructuredEvent: jest.fn(),
+ EventCategory: { ADMIN: "admin" },
+}));
+jest.mock("../../../../lib/judgeApi", () => {
+ const api = {
+ getJudgingProgress: jest.fn(),
+ setRoundLock: jest.fn(),
+ getRoundLocks: jest.fn(),
+ };
+ return {
+ __esModule: true,
+ default: api,
+ isNotFound: (e) => e?.status === 404,
+ };
+});
+jest.mock("../../ExportCsvButton", () => () => null);
+
+import judgeApi from "../../../../lib/judgeApi";
+import JudgeProgressPanel from "../JudgeProgressPanel";
+
+const api = judgeApi;
+const progress = {
+ lock: { locked: false },
+ judges: [
+ {
+ judge_id: "j1",
+ name: "Ada",
+ email: "ada@x.org",
+ assigned: 3,
+ submitted: 1,
+ complete: false,
+ assigned_team_ids: ["t1", "t2", "t3"],
+ submitted_team_ids: ["t1"],
+ draft_team_ids: ["t2"],
+ missing_team_ids: ["t3"],
+ last_submitted_at: "2026-10-11T20:00:00Z",
+ },
+ {
+ judge_id: "j2",
+ name: "Bob",
+ assigned: 2,
+ submitted: 2,
+ complete: true,
+ assigned_team_ids: ["t1", "t2"],
+ submitted_team_ids: ["t1", "t2"],
+ draft_team_ids: [],
+ missing_team_ids: [],
+ },
+ ],
+ teams: [
+ {
+ team_id: "t1",
+ name: "Alpha",
+ assigned: 2,
+ submitted: 2,
+ assigned_judge_ids: ["j1", "j2"],
+ submitted_judge_ids: ["j1", "j2"],
+ },
+ {
+ team_id: "t3",
+ name: "Gamma",
+ assigned: 1,
+ submitted: 0,
+ assigned_judge_ids: ["j1"],
+ }, // submitted_judge_ids missing on purpose
+ ],
+ summary: {
+ judges_total: 2,
+ judges_complete: 1,
+ assignments_total: 5,
+ submitted_total: 3,
+ drafts_total: 1,
+ orphan_submitted: 1,
+ },
+};
+
+const props = {
+ eventId: "fall-2026",
+ round: "round2",
+ accessToken: "tok",
+ orgId: "org",
+ onSnack: jest.fn(),
+};
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ api.getJudgingProgress.mockResolvedValue(progress);
+ api.setRoundLock.mockResolvedValue({
+ success: true,
+ lock: {
+ locked: true,
+ locked_at: "2026-10-11T21:00:00Z",
+ locked_by_name: "Greg",
+ },
+ });
+});
+
+describe("JudgeProgressPanel", () => {
+ it("renders who is done, who is missing teams, drafts and the orphan note", async () => {
+ render();
+ expect(
+ await screen.findByText(/judges complete 1\/2/i),
+ ).toBeInTheDocument();
+ expect(screen.getByText(/scores in 3\/5/i)).toBeInTheDocument();
+ expect(screen.getByText(/drafts in progress 1/i)).toBeInTheDocument();
+ const ada = screen.getByText("Ada").closest("tr");
+ expect(within(ada).getByText("Gamma")).toBeInTheDocument();
+ expect(
+ within(ada).getByText(/Beta|t2 \(draft\)|\(draft\)/),
+ ).toBeInTheDocument();
+ const bob = screen.getByText("Bob").closest("tr");
+ expect(within(bob).getByText(/done/i)).toBeInTheDocument();
+ expect(screen.getByText(/not counted in results/i)).toBeInTheDocument();
+ // By-team view tolerates a row missing submitted_judge_ids
+ fireEvent.click(screen.getByRole("button", { name: /by team/i }));
+ const gamma = screen.getByText("Gamma").closest("tr");
+ expect(within(gamma).getByText("Ada")).toBeInTheDocument();
+ });
+
+ it("locks the round through the confirm dialog and updates the chip", async () => {
+ render();
+ expect(
+ await screen.findByText(/round 2 scoring open/i),
+ ).toBeInTheDocument();
+ fireEvent.click(
+ screen.getByRole("button", { name: /lock round 2 scoring/i }),
+ );
+ const dialog = await screen.findByRole("dialog");
+ expect(dialog).toHaveTextContent(
+ /judges will no longer be able to submit/i,
+ );
+ fireEvent.click(
+ within(dialog).getByRole("button", { name: /^lock scoring$/i }),
+ );
+ await waitFor(() =>
+ expect(api.setRoundLock).toHaveBeenCalledWith(
+ "fall-2026",
+ "round2",
+ true,
+ "tok",
+ "org",
+ ),
+ );
+ // The confirm dialog hides the page behind aria-hidden until its exit transition ends.
+ await waitForElementToBeRemoved(() => screen.queryByRole("dialog"));
+ expect(
+ await screen.findByText(/round 2 scoring locked/i),
+ ).toBeInTheDocument();
+ expect(screen.getByText(/locked by greg/i)).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: /unlock round 2 scoring/i }),
+ ).toBeInTheDocument();
+ expect(props.onSnack).toHaveBeenCalledWith(
+ expect.stringMatching(/now locked/i),
+ "success",
+ );
+ });
+
+ it("explains when the backend predates the progress route", async () => {
+ api.getJudgingProgress.mockResolvedValue(null);
+ render();
+ expect(
+ await screen.findByText(/needs the latest backend/i),
+ ).toBeInTheDocument();
+ });
+
+ it("keeps the lock toggle disabled while the lock state is unknown, then uses the public locks route", async () => {
+ api.getJudgingProgress.mockRejectedValue(new Error("boom"));
+ api.getRoundLocks.mockResolvedValue({
+ locks: { round2: { locked: true, locked_at: "2026-10-11T21:00:00Z" } },
+ });
+ render();
+ expect(await screen.findByRole("alert")).toHaveTextContent(/boom/);
+ expect(
+ await screen.findByText(/round 2 scoring locked/i),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: /unlock round 2 scoring/i }),
+ ).toBeEnabled();
+ });
+});
diff --git a/src/components/admin/judging/__tests__/resultsRanking.test.js b/src/components/admin/judging/__tests__/resultsRanking.test.js
new file mode 100644
index 00000000..6772e8b2
--- /dev/null
+++ b/src/components/admin/judging/__tests__/resultsRanking.test.js
@@ -0,0 +1,117 @@
+import {
+ rankRound2Teams,
+ compareSubmittedAt,
+ formatSubmittedAt,
+ AVERAGE_PRECISION,
+} from "../resultsRanking";
+
+const row = (
+ name,
+ avg,
+ { sd = 0, submitted = undefined, judges = 3 } = {},
+) => ({
+ team: { id: name, name, project_submitted_at: submitted },
+ averageScore: avg,
+ standardDeviation: sd,
+ judgeCount: judges,
+});
+
+describe("rankRound2Teams", () => {
+ it("orders by average score, scored teams before unscored", () => {
+ const ranked = rankRound2Teams([
+ row("Low", 20),
+ row("None", 0, { judges: 0 }),
+ row("High", 36),
+ ]);
+ expect(ranked.map((r) => r.team.name)).toEqual(["High", "Low", "None"]);
+ expect(ranked.map((r) => r.rank)).toEqual([1, 2, 3]);
+ expect(ranked.every((r) => r.tieGroup === null)).toBe(true);
+ });
+
+ it("breaks a tie by the EARLIEST project submission", () => {
+ const ranked = rankRound2Teams([
+ row("Later", 30, { submitted: "2026-10-11T22:10:00Z", sd: 0.5 }),
+ row("Earlier", 30, { submitted: "2026-10-11T21:58:07Z", sd: 2.5 }),
+ ]);
+ expect(ranked.map((r) => r.team.name)).toEqual(["Earlier", "Later"]);
+ expect(ranked[0].tieGroup).toBe(1);
+ expect(ranked[0].tieBrokenBy).toBeNull();
+ expect(ranked[1].tieGroup).toBe(1);
+ expect(ranked[1].tieBrokenBy).toBe("project_submitted_at");
+ });
+
+ it("a team with no submission timestamp loses the tie to one that has it", () => {
+ const ranked = rankRound2Teams([
+ row("NoStamp", 30),
+ row("Stamped", 30, { submitted: "2026-10-11T23:59:00Z" }),
+ ]);
+ expect(ranked.map((r) => r.team.name)).toEqual(["Stamped", "NoStamp"]);
+ expect(ranked[1].tieBrokenBy).toBe("project_submitted_at");
+ });
+
+ it("falls back to lower std dev, then name, when submissions can't separate", () => {
+ const ranked = rankRound2Teams([
+ row("Beta", 30, { sd: 1.0 }),
+ row("Alpha", 30, { sd: 1.0 }),
+ row("Consistent", 30, { sd: 0.2 }),
+ ]);
+ expect(ranked.map((r) => r.team.name)).toEqual([
+ "Consistent",
+ "Alpha",
+ "Beta",
+ ]);
+ expect(ranked[1].tieBrokenBy).toBe("std_dev");
+ expect(ranked[2].tieBrokenBy).toBe("name");
+ });
+
+ it("compares averages at the displayed precision (two decimals), never with an epsilon", () => {
+ expect(AVERAGE_PRECISION).toBe(2);
+ // 14 vs 15 judges: 425/14 = 30.357… and 455/15 = 30.333… display as
+ // 30.36 and 30.33 — different averages, so NOT a tie, whatever the
+ // submission times say.
+ const ranked = rankRound2Teams([
+ row("Fifteen", 455 / 15, { judges: 15, submitted: "2026-10-11T18:00:00Z" }),
+ row("Fourteen", 425 / 14, { judges: 14, submitted: "2026-10-11T20:00:00Z" }),
+ ]);
+ expect(ranked.map((r) => r.team.name)).toEqual(["Fourteen", "Fifteen"]);
+ expect(ranked.every((r) => r.tieGroup === null)).toBe(true);
+ });
+
+ it("averages that round to the same two decimals are tied, and groups are independent", () => {
+ const ranked = rankRound2Teams([
+ row("A", 30.004, { submitted: "2026-10-11T20:00:00Z" }),
+ row("B", 30.0, { submitted: "2026-10-11T19:00:00Z" }),
+ row("C", 25, { submitted: "2026-10-11T18:00:00Z" }),
+ row("D", 25, { submitted: "2026-10-11T18:30:00Z" }),
+ ]);
+ expect(ranked.map((r) => r.team.name)).toEqual(["B", "A", "C", "D"]);
+ expect(ranked.map((r) => r.tieGroup)).toEqual([1, 1, 2, 2]);
+ });
+
+ it("does not mutate the input", () => {
+ const input = [row("X", 10), row("Y", 20)];
+ const copy = JSON.parse(JSON.stringify(input));
+ rankRound2Teams(input);
+ expect(input).toEqual(copy);
+ });
+});
+
+describe("compareSubmittedAt / formatSubmittedAt", () => {
+ it("sorts earlier first and missing last", () => {
+ const a = { team: { project_submitted_at: "2026-10-11T20:00:00Z" } };
+ const b = { team: { project_submitted_at: "2026-10-11T21:00:00Z" } };
+ const none = { team: {} };
+ expect(compareSubmittedAt(a, b)).toBeLessThan(0);
+ expect(compareSubmittedAt(b, a)).toBeGreaterThan(0);
+ expect(compareSubmittedAt(a, none)).toBeLessThan(0);
+ expect(compareSubmittedAt(none, none)).toBe(0);
+ });
+
+ it("formats in the event timezone with seconds and returns null for garbage", () => {
+ expect(formatSubmittedAt(null)).toBeNull();
+ expect(formatSubmittedAt("not a date")).toBeNull();
+ const out = formatSubmittedAt("2026-10-11T21:58:07Z", "America/Phoenix");
+ expect(out).toMatch(/2:58:07/);
+ expect(out).toMatch(/MST/);
+ });
+});
diff --git a/src/components/admin/judging/resultsRanking.js b/src/components/admin/judging/resultsRanking.js
new file mode 100644
index 00000000..fe5c1247
--- /dev/null
+++ b/src/components/admin/judging/resultsRanking.js
@@ -0,0 +1,128 @@
+/**
+ * Final-results ranking for Round 2 (pure, unit-tested).
+ *
+ * Order: teams with at least one submitted score first, by average score
+ * descending. Averages are compared at the precision the table DISPLAYS
+ * (two decimals) — never with an epsilon: an epsilon wider than the smallest
+ * real gap (1/210 with 14 vs 15 judges) would reorder teams whose displayed
+ * averages differ, and epsilon-equality isn't transitive, which makes a sort
+ * undefined. Ties at that precision are broken by the EARLIEST
+ * `project_submitted_at` — the first time the team pressed "Submit your
+ * project" on its dashboard (the backend only stamps it once). A team with no
+ * submission timestamp loses the tie to one that has it. If that still ties
+ * (same instant, or neither submitted), lower standard deviation wins, then
+ * team name. Teams with no scores sort last, by name.
+ */
+
+export const AVERAGE_PRECISION = 2;
+const SCALE = 10 ** AVERAGE_PRECISION;
+
+export const parseSubmittedAt = (team) => {
+ const raw = team?.project_submitted_at;
+ if (!raw) return null;
+ const t = new Date(raw).getTime();
+ return Number.isNaN(t) ? null : t;
+};
+
+/** Earlier timestamp first; missing timestamps last; equal → 0. */
+export function compareSubmittedAt(a, b) {
+ const ta = parseSubmittedAt(a?.team);
+ const tb = parseSubmittedAt(b?.team);
+ if (ta === null && tb === null) return 0;
+ if (ta === null) return 1;
+ if (tb === null) return -1;
+ return ta - tb;
+}
+
+/** The average as an integer at display precision, or null when unscored. */
+export const averageKey = (row) =>
+ row?.judgeCount > 0
+ ? Math.round((Number(row.averageScore) || 0) * SCALE)
+ : null;
+
+const sdOf = (row) => Number(row?.standardDeviation) || 0;
+const nameOf = (row) => String(row?.team?.name || "");
+
+const tiedOnAverage = (a, b) => {
+ const x = averageKey(a);
+ const y = averageKey(b);
+ return x !== null && y !== null && x === y;
+};
+
+function compareRows(a, b) {
+ const x = averageKey(a);
+ const y = averageKey(b);
+ if (x === null && y === null) return nameOf(a).localeCompare(nameOf(b));
+ if (x === null) return 1;
+ if (y === null) return -1;
+ if (x !== y) return y - x;
+ const bySubmission = compareSubmittedAt(a, b);
+ if (bySubmission !== 0) return bySubmission;
+ const bySd = sdOf(a) - sdOf(b);
+ if (bySd !== 0) return bySd;
+ return nameOf(a).localeCompare(nameOf(b));
+}
+
+/** Which rule separated `a` (ranked higher) from `b` inside a tie group. */
+function tieBreakReason(a, b) {
+ const bySubmission = compareSubmittedAt(a, b);
+ if (bySubmission !== 0) return "project_submitted_at";
+ if (sdOf(a) !== sdOf(b)) return "std_dev";
+ return "name";
+}
+
+/**
+ * Returns a NEW array of rows (shallow copies) sorted for the results table,
+ * each annotated with:
+ * rank 1-based position
+ * tieGroup shared id for rows tied on average (null when not tied)
+ * tieBrokenBy 'project_submitted_at' | 'std_dev' | 'name' | null —
+ * the rule that placed this row relative to the row above
+ * it within the same tie group (null for the group's first)
+ */
+export function rankRound2Teams(teamScores) {
+ const sorted = [...(teamScores || [])].sort(compareRows);
+ let groupId = 0;
+ return sorted.map((row, index) => {
+ const prev = index > 0 ? sorted[index - 1] : null;
+ const next = index < sorted.length - 1 ? sorted[index + 1] : null;
+ const tiedWithPrev = prev ? tiedOnAverage(prev, row) : false;
+ const tiedWithNext = next ? tiedOnAverage(row, next) : false;
+ if (!tiedWithPrev && tiedWithNext) groupId += 1;
+ const inGroup = tiedWithPrev || tiedWithNext;
+ return {
+ ...row,
+ rank: index + 1,
+ tieGroup: inGroup ? groupId : null,
+ tieBrokenBy: tiedWithPrev ? tieBreakReason(prev, row) : null,
+ };
+ });
+}
+
+export const TIE_BREAK_LABELS = {
+ project_submitted_at: "earlier project submission",
+ std_dev: "more consistent judge scores (lower std dev)",
+ name: "team name (no other separator)",
+};
+
+/** "Sat Oct 11, 2:58:07 PM MST" — seconds matter when breaking a tie. */
+export function formatSubmittedAt(iso, timezone) {
+ if (!iso) return null;
+ const date = new Date(iso);
+ if (Number.isNaN(date.getTime())) return null;
+ const tz = timezone || "America/Phoenix";
+ try {
+ return date.toLocaleString("en-US", {
+ timeZone: tz,
+ weekday: "short",
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ second: "2-digit",
+ timeZoneName: "short",
+ });
+ } catch (e) {
+ return date.toLocaleString("en-US");
+ }
+}
diff --git a/src/components/content/BootcampContent.js b/src/components/content/BootcampContent.js
index 156b84a3..bf5051f7 100644
--- a/src/components/content/BootcampContent.js
+++ b/src/components/content/BootcampContent.js
@@ -2,7 +2,7 @@
import { Typography, Box } from "@mui/material";
import { LinkStyled } from "../../styles/nonprofit/styles";
-const style = { fontSize: "14px" };
+const style = {}; // sizing comes from the page (.ohx-prose)
const BootcampContent = () => (
<>
@@ -13,7 +13,7 @@ const BootcampContent = () => (
practical application through meaningful nonprofit projects.
-
+
Why Nonprofit Projects Are Perfect for Bootcamp Grads
@@ -48,7 +48,7 @@ const BootcampContent = () => (
-
+
Getting Started
@@ -80,7 +80,7 @@ const BootcampContent = () => (
-
+
Success Tips
diff --git a/src/components/content/CareerTransitionContent.js b/src/components/content/CareerTransitionContent.js
index e41d68c0..23cfc3b2 100644
--- a/src/components/content/CareerTransitionContent.js
+++ b/src/components/content/CareerTransitionContent.js
@@ -2,7 +2,7 @@
import { Typography, Box } from "@mui/material";
import { LinkStyled } from "../../styles/nonprofit/styles";
-const style = { fontSize: "14px" };
+const style = {}; // sizing comes from the page (.ohx-prose)
const CareerTransitionContent = () => (
<>
@@ -12,7 +12,7 @@ const CareerTransitionContent = () => (
by providing real development experience through nonprofit projects.
-
+
Why Nonprofit Projects Are Perfect for Career Changers
@@ -47,7 +47,7 @@ const CareerTransitionContent = () => (
-
+
Leveraging Your Previous Experience
@@ -72,7 +72,7 @@ const CareerTransitionContent = () => (
-
+
Getting Started
@@ -104,7 +104,7 @@ const CareerTransitionContent = () => (
-
+
Building Your Portfolio
diff --git a/src/components/content/CodingForGoodContent.js b/src/components/content/CodingForGoodContent.js
index 5bd58b70..e8d709a3 100644
--- a/src/components/content/CodingForGoodContent.js
+++ b/src/components/content/CodingForGoodContent.js
@@ -2,203 +2,219 @@
import { Typography, Box } from "@mui/material";
import { LinkStyled } from "../../styles/nonprofit/styles";
-const style = { fontSize: "14px" };
+const style = {}; // sizing comes from the page (.ohx-prose)
const CodingForGoodContent = () => (
<>
- Discover how coding for nonprofits can transform communities while advancing your career.
- Whether you're a seasoned developer or just starting out, your programming skills can help
- nonprofits amplify their impact and better serve their communities through technology.
+ Discover how coding for nonprofits can transform communities while
+ advancing your career. Whether you're a seasoned developer or just
+ starting out, your programming skills can help nonprofits amplify their
+ impact and better serve their communities through technology.
-
- Why Code for Nonprofits?
+
+ Why Code for Nonprofits?
- Coding nonprofits need talented developers to build solutions that matter. When you contribute
- your technical skills to nonprofit organizations, you're not just writing code - you're creating
- tools that feed families, educate children, and strengthen communities.
+ Coding nonprofits need talented developers to build solutions that matter.
+ When you contribute your technical skills to nonprofit organizations,
+ you're not just writing code - you're creating tools that feed families,
+ educate children, and strengthen communities.
- Real-World Impact: See your code directly improve lives and communities
+ Real-World Impact: See your code directly improve
+ lives and communities
- Portfolio Building: Develop impressive projects that showcase your skills to employers
+ Portfolio Building: Develop impressive projects that
+ showcase your skills to employers
- Network Growth: Connect with like-minded developers and nonprofit leaders
+ Network Growth: Connect with like-minded developers
+ and nonprofit leaders
- Skill Development: Learn new technologies while solving unique challenges
+ Skill Development: Learn new technologies while
+ solving unique challenges
-
+
High-Impact Coding Projects for Nonprofits
- Nonprofits need coding solutions across many areas. Here are the most requested types
- of projects where your programming skills can make an immediate difference:
+ Nonprofits need coding solutions across many areas. Here are the most
+ requested types of projects where your programming skills can make an
+ immediate difference:
- Donation Management Systems: Build platforms that help nonprofits track,
- process, and acknowledge donations efficiently
+ Donation Management Systems: Build platforms that
+ help nonprofits track, process, and acknowledge donations efficiently
- Volunteer Coordination Tools: Create systems for scheduling, tracking,
- and managing volunteer activities and hours
+ Volunteer Coordination Tools: Create systems for
+ scheduling, tracking, and managing volunteer activities and hours
- Client Service Platforms: Develop secure systems for managing client
- information and tracking service delivery
+ Client Service Platforms: Develop secure systems for
+ managing client information and tracking service delivery
- Impact Analytics Dashboards: Build tools to help nonprofits measure,
- visualize, and communicate their community impact
+ Impact Analytics Dashboards: Build tools to help
+ nonprofits measure, visualize, and communicate their community impact
- Mobile-First Applications: Create accessible mobile apps that bring
- services directly to communities in need
+ Mobile-First Applications: Create accessible mobile
+ apps that bring services directly to communities in need
-
+
How to Start Coding for Nonprofits Today
- Browse active nonprofit projects and find
- causes that align with your values and interests
+ Browse{" "}
+ active nonprofit projects{" "}
+ and find causes that align with your values and interests
- Join our developer community to connect with
- nonprofits actively seeking coding help
+ Join our developer community{" "}
+ to connect with nonprofits actively seeking coding help
- Attend our weekly office hours to learn
+ Attend our weekly{" "}
+ office hours to learn
about urgent coding needs and get matched with projects
- Start with a small contribution to understand the nonprofit's needs before taking on larger projects
+ Start with a small contribution to understand the nonprofit's needs
+ before taking on larger projects
-
+
Best Practices for Nonprofit Coding Projects
- Successful coding nonprofits projects require thoughtful planning and execution. Follow these
- guidelines to ensure your contributions create lasting value:
+ Successful coding nonprofits projects require thoughtful planning and
+ execution. Follow these guidelines to ensure your contributions create
+ lasting value:
- Sustainability First: Build solutions that nonprofits can maintain
- with limited technical resources
+ Sustainability First: Build solutions that nonprofits
+ can maintain with limited technical resources
- Comprehensive Documentation: Write clear documentation for future
- volunteers and nonprofit staff who will use your code
+ Comprehensive Documentation: Write clear
+ documentation for future volunteers and nonprofit staff who will use
+ your code
- Security & Privacy: Implement robust security measures to protect
- sensitive donor and client information
+ Security & Privacy: Implement robust security
+ measures to protect sensitive donor and client information
- User Experience Focus: Design with accessibility in mind, considering
- diverse user groups and varying technical skills
+ User Experience Focus: Design with accessibility in
+ mind, considering diverse user groups and varying technical skills
- Budget-Conscious Development: Choose technologies and hosting solutions
- that fit nonprofit budget constraints
+ Budget-Conscious Development: Choose technologies and
+ hosting solutions that fit nonprofit budget constraints
-
+
Your Coding Impact on Nonprofits
- When you dedicate your coding skills to nonprofits, you become part of a movement that's
- revolutionizing how social good organizations operate and serve their communities:
+ When you dedicate your coding skills to nonprofits, you become part of a
+ movement that's revolutionizing how social good organizations operate and
+ serve their communities:
- Amplify Nonprofit Efficiency: Help organizations serve more people
- with limited resources through automation and optimization
+ Amplify Nonprofit Efficiency: Help organizations
+ serve more people with limited resources through automation and
+ optimization
- Create Lasting Solutions: Build technology that continues helping
- communities long after your initial contribution
+ Create Lasting Solutions: Build technology that
+ continues helping communities long after your initial contribution
- Bridge the Digital Divide: Bring modern technology to nonprofits
- that couldn't otherwise afford professional development services
+ Bridge the Digital Divide: Bring modern technology to
+ nonprofits that couldn't otherwise afford professional development
+ services
- Mentor the Next Generation: Guide other developers interested in
- using their skills for social good
+ Mentor the Next Generation: Guide other developers
+ interested in using their skills for social good
- Ready to start coding for nonprofits? Join thousands of developers who are using their
- programming skills to create positive change in their communities. Your next commit could
- help feed a family, educate a child, or strengthen an entire community.
+ Ready to start coding for nonprofits? Join thousands of developers who are
+ using their programming skills to create positive change in their
+ communities. Your next commit could help feed a family, educate a child,
+ or strengthen an entire community.
>
);
diff --git a/src/components/content/ExperienceAfterLayoffContent.js b/src/components/content/ExperienceAfterLayoffContent.js
index 69f8c24e..8e0b391a 100644
--- a/src/components/content/ExperienceAfterLayoffContent.js
+++ b/src/components/content/ExperienceAfterLayoffContent.js
@@ -2,7 +2,7 @@
import { Typography, Box } from "@mui/material";
import { LinkStyled } from "../../styles/nonprofit/styles";
-const style = { fontSize: "14px" };
+const style = {}; // sizing comes from the page (.ohx-prose)
const ExperienceAfterLayoffContent = () => (
<>
@@ -12,7 +12,7 @@ const ExperienceAfterLayoffContent = () => (
projects, you can make a positive impact while building your portfolio.
-
+
Why Nonprofit Projects Are Ideal After a Layoff
@@ -48,7 +48,7 @@ const ExperienceAfterLayoffContent = () => (
-
+
How to Get Started
diff --git a/src/components/content/FindNonprofitProjectContent.js b/src/components/content/FindNonprofitProjectContent.js
index 90753f73..34c94f82 100644
--- a/src/components/content/FindNonprofitProjectContent.js
+++ b/src/components/content/FindNonprofitProjectContent.js
@@ -2,7 +2,7 @@
import { Typography, Box } from "@mui/material";
import { LinkStyled } from "../../styles/nonprofit/styles";
-const style = { fontSize: "14px" };
+const style = {}; // sizing comes from the page (.ohx-prose)
const FindNonprofitProjectContent = () => (
<>
@@ -12,7 +12,7 @@ const FindNonprofitProjectContent = () => (
them to your skills, and make meaningful contributions.
-
+
Assess Your Skills and Interests
@@ -43,7 +43,7 @@ const FindNonprofitProjectContent = () => (
-
+
Project Evaluation Checklist
@@ -73,7 +73,7 @@ const FindNonprofitProjectContent = () => (
-
+
Where to Find Projects
@@ -108,7 +108,7 @@ const FindNonprofitProjectContent = () => (
-
+
Project Types by Experience Level
@@ -148,7 +148,7 @@ const FindNonprofitProjectContent = () => (
-
+
Getting Started Successfully
@@ -179,7 +179,7 @@ const FindNonprofitProjectContent = () => (
-
+
Red Flags to Watch For
@@ -206,7 +206,7 @@ const FindNonprofitProjectContent = () => (
-
+
Tips for Long-term Success
diff --git a/src/components/content/InternshipContent.js b/src/components/content/InternshipContent.js
index 235caef4..4903e962 100644
--- a/src/components/content/InternshipContent.js
+++ b/src/components/content/InternshipContent.js
@@ -1,26 +1,26 @@
// src/components/content/InternshipContent.js
-import { Grid, Typography, Box } from '@mui/material';
-import CheckCircleIcon from '@mui/icons-material/CheckCircle';
-import SchoolIcon from '@mui/icons-material/School';
-import WorkIcon from '@mui/icons-material/Work';
-import CodeIcon from '@mui/icons-material/Code';
-import GroupsIcon from '@mui/icons-material/Groups';
-import { LinkStyled } from '../../styles/nonprofit/styles';
+import { Grid, Typography, Box } from "@mui/material";
+import CheckCircleIcon from "@mui/icons-material/CheckCircle";
+import SchoolIcon from "@mui/icons-material/School";
+import WorkIcon from "@mui/icons-material/Work";
+import CodeIcon from "@mui/icons-material/Code";
+import GroupsIcon from "@mui/icons-material/Groups";
+import { LinkStyled } from "../../styles/nonprofit/styles";
-const style = { fontSize: 14 };
+const style = {}; // sizing comes from the page (.ohx-prose)
const BenefitCard = ({ icon: Icon, title, description }) => (
-
-
-
+
+ {title}
@@ -32,36 +32,47 @@ const BenefitCard = ({ icon: Icon, title, description }) => (
const benefitCards = [
{
icon: SchoolIcon,
- title: 'Real-World Experience',
- description: 'Work on actual problems faced by organizations, not just classroom assignments'
+ title: "Real-World Experience",
+ description:
+ "Work on actual problems faced by organizations, not just classroom assignments",
},
{
icon: WorkIcon,
- title: 'Professional Development',
- description: 'Learn to communicate with stakeholders and work in team environments'
+ title: "Professional Development",
+ description:
+ "Learn to communicate with stakeholders and work in team environments",
},
{
icon: CodeIcon,
- title: 'Technical Growth',
- description: 'Build full-stack applications using modern tools and best practices'
+ title: "Technical Growth",
+ description:
+ "Build full-stack applications using modern tools and best practices",
},
{
icon: GroupsIcon,
- title: 'Networking',
- description: 'Connect with experienced developers, mentors and nonprofit leaders'
- }
+ title: "Networking",
+ description:
+ "Connect with experienced developers, mentors and nonprofit leaders",
+ },
];
const InternshipContent = () => (
-
+
The Challenge: Standing Out in Internship Applications
-
+
- As a college student pursuing a tech internship, you're competing against thousands of applicants
- with similar coursework and basic projects. The key to landing your dream internship is demonstrating
- real-world experience and initiative - exactly what you gain by working on nonprofit projects.
+ As a college student pursuing a tech internship, you're competing against
+ thousands of applicants with similar coursework and basic projects. The
+ key to landing your dream internship is demonstrating real-world
+ experience and initiative - exactly what you gain by working on nonprofit
+ projects.
@@ -72,27 +83,32 @@ const InternshipContent = () => (
))}
-
+
Why Nonprofit Projects Make Your Application Stand Out
{[
- 'Experience with real codebases and development workflows',
- 'Initiative and self-direction in solving complex problems',
- 'Ability to work with cross-functional teams',
- 'Understanding of deployment and production environments',
- 'Portfolio of work that demonstrates real impact'
+ "Experience with real codebases and development workflows",
+ "Initiative and self-direction in solving complex problems",
+ "Ability to work with cross-functional teams",
+ "Understanding of deployment and production environments",
+ "Portfolio of work that demonstrates real impact",
].map((point, index) => (
-
-
+
{point}
@@ -100,23 +116,36 @@ const InternshipContent = () => (
))}
-
+
Getting Started with Opportunity Hack
- Ready to build your portfolio while making a difference? Here's how to get started:
+ Ready to build your portfolio while making a difference? Here's how to get
+ started:
-
+
- Browse our nonprofit projects and find one that matches your interests and skills
+
+ Browse our nonprofit projects
+ {" "}
+ and find one that matches your interests and skills
- Join our Slack community to connect with mentors and other volunteers
+ Join our{" "}
+
+ Slack community
+ {" "}
+ to connect with mentors and other volunteers
+
+
- )}
-
-
- It looks like this page is taking a little nap. But don't worry, there are still plenty of opportunities to hack for social good while we wake it up!
-
-
-
-
- Go to Homepage
-
-
-
-
-
- Browse Nonprofit Projects
-
-
-
-
-
-
- Learn About Opportunity Hack
-
-
-
-
-
-
- If you believe this is an error, please{' '}
- let us know.
-
-
-
-
+
+ Think this is our mistake?{" "}
+
+ Let us know
+
+ .
+
+
+ {/* Reserved 4:3 box so the random image never shifts the layout. */}
+
+ {randomImage && (
+
+ )}
+
+
+
+ >
);
};
-export default Custom404;
\ No newline at end of file
+export default Custom404;
diff --git a/src/pages/about/privacy-policy/index.js b/src/pages/about/privacy-policy/index.js
index e3144648..6356c7ff 100644
--- a/src/pages/about/privacy-policy/index.js
+++ b/src/pages/about/privacy-policy/index.js
@@ -1,147 +1,181 @@
-import React from 'react';
-import Head from 'next/head';
-import { Typography, Box, Button, Divider, Link as MuiLink } from '@mui/material';
-import { LayoutContainer, TitleContainer, ProjectsContainer } from '../../../styles/nonprofit/styles';
-import Link from 'next/link';
+import React from "react";
+import Head from "next/head";
+import Link from "next/link";
+import {
+ RefinedPage,
+ PageMasthead,
+ Prose,
+ LinkRow,
+} from "../../../components/design/RefinedPage";
-const PrivacyPolicy = () => {
- return (
-
-
- Privacy Policy - Opportunity Hack
-
-
-
-
-
- Privacy Policy
-
-
-
-
- At Opportunity Hack, we are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy outlines our practices concerning the collection, use, and protection of your data.
-
+// Date the policy TEXT last changed (contact email → /contact page, Oct 8
+// 2026). Bump it only with a wording change — the previous page printed
+// today's date, which read as a daily revision.
+const LAST_UPDATED = "October 8, 2026";
-
- 1. About Opportunity Hack
-
-
- Opportunity Hack is a nonprofit organization dedicated to leveraging technology for social good. Our mission is to connect skilled volunteers with nonprofits to create innovative solutions that make a positive impact on society.
-
+const PrivacyPolicy = () => (
+ <>
+
+ Privacy Policy - Opportunity Hack
+
+
+
+
+
+
+
+
1. About Opportunity Hack
+
+ Opportunity Hack is a nonprofit organization dedicated to leveraging
+ technology for social good. Our mission is to connect skilled
+ volunteers with nonprofits to create innovative solutions that make a
+ positive impact on society.
+
-
- 2. Information We Collect
-
-
- We collect information that you provide directly to us, such as when you register for an event, sign up for our newsletter, or participate in our hackathons. This may include:
-
-
+
2. Information we collect
+
+ We collect information that you provide directly to us, such as when
+ you register for an event, sign up for our newsletter, or participate
+ in our hackathons. This may include:
+
+
Name and contact information
Skills and areas of expertise
Demographic information
Participation history in our events
-
+
-
- 3. How We Use Your Information
-
-
- We use the information we collect to:
-
-
+
3. How we use your information
+
We use the information we collect to:
+
Organize and manage our hackathons and events
Communicate with you about upcoming opportunities
Improve our programs and services
Analyze the impact of our initiatives
-
+
-
- 4. Information Sharing and Disclosure
-
-
- As a nonprofit organization committed to transparency and trust, we want to assure you that we will not share any information about our participants without their explicit agreement. Your privacy is paramount to us, and we treat your personal information with the utmost respect and confidentiality.
-
+
4. Information sharing and disclosure
+
+ As a nonprofit organization committed to transparency and trust, we
+ want to assure you that we will not share any information about our
+ participants without their explicit agreement. Your privacy is
+ paramount to us, and we treat your personal information with the
+ utmost respect and confidentiality.
+
-
- 5. Data Security
-
-
- We implement a variety of security measures to maintain the safety of your personal information. However, no method of transmission over the Internet or electronic storage is 100% secure. While we strive to use commercially acceptable means to protect your personal information, we cannot guarantee its absolute security.
-
+
5. Data security
+
+ We implement a variety of security measures to maintain the safety of
+ your personal information. However, no method of transmission over the
+ Internet or electronic storage is 100% secure. While we strive to use
+ commercially acceptable means to protect your personal information, we
+ cannot guarantee its absolute security.
+
-
- 6. Open Source Commitment
-
-
- At Opportunity Hack, we believe in the power of open source and transparency. We invite you to review our open-source GitHub code to help improve our security and privacy measures. As this is a public good, we welcome contributions from the community to enhance the protection of user data and the overall security of our platform.
-
-
- You can find our GitHub repository at:{' '}
-
- https://github.com/opportunity-hack
-
-
+
6. Open source commitment
+
+ At Opportunity Hack, we believe in the power of open source and
+ transparency. We invite you to review our open-source GitHub code to
+ help improve our security and privacy measures. As this is a public
+ good, we welcome contributions from the community to enhance the
+ protection of user data and the overall security of our platform.
+
-
- 7. Cookies and Website Analysis
-
-
- We use cookies and similar tracking technologies to analyze the quality and impact of our website. These tools help us understand how visitors use our site, which pages are most popular, and how we can improve the user experience. The data collected is aggregated and anonymized, and we do not use it to identify individual users.
-
-
- By using our website, you consent to the use of cookies for these purposes. You can control and/or delete cookies as you wish through your browser settings. Removing or blocking cookies may impact your user experience on our site.
-
+
7. Cookies and website analysis
+
+ We use cookies and similar tracking technologies to analyze the
+ quality and impact of our website. These tools help us understand how
+ visitors use our site, which pages are most popular, and how we can
+ improve the user experience. The data collected is aggregated and
+ anonymized, and we do not use it to identify individual users.
+
+
+ By using our website, you consent to the use of cookies for these
+ purposes. You can control and/or delete cookies as you wish through
+ your browser settings. Removing or blocking cookies may impact your
+ user experience on our site.
+
-
- 8. GDPR Compliance
-
-
- Opportunity Hack is committed to complying with the General Data Protection Regulation (GDPR). If you are a resident of the European Economic Area (EEA), you have certain data protection rights, including:
-
-
-
The right to access, update, or delete your personal information
+
8. GDPR compliance
+
+ Opportunity Hack is committed to complying with the General Data
+ Protection Regulation (GDPR). If you are a resident of the European
+ Economic Area (EEA), you have certain data protection rights,
+ including:
+
+
+
+ The right to access, update, or delete your personal information
+
The right to data portability
-
The right to restrict or object to our processing of your personal data
-
The right to withdraw consent at any time for data processing based on consent
-
-
- To exercise these rights, please contact us through our contact page at ohack.dev/contact. We will respond to your request within 30 days.
-
+
+ The right to restrict or object to our processing of your personal
+ data
+
+
+ The right to withdraw consent at any time for data processing based
+ on consent
+
+
+
+ To exercise these rights, please contact us through our{" "}
+ contact page. We will respond to your
+ request within 30 days.
+
-
- 9. Your Rights
-
-
- Regardless of your location, you have the right to access, correct, or delete your personal information at any time. If you would like to exercise these rights or have any questions about our privacy practices, please contact us through our contact page at ohack.dev/contact.
-
+
9. Your rights
+
+ Regardless of your location, you have the right to access, correct, or
+ delete your personal information at any time. If you would like to
+ exercise these rights or have any questions about our privacy
+ practices, please contact us through our{" "}
+ contact page.
+
-
- 10. Changes to This Policy
-
-
- We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last Updated" date below.
-
+
10. Changes to this policy
+
+ We may update this Privacy Policy from time to time. We will notify
+ you of any changes by posting the new Privacy Policy on this page and
+ updating the "Last updated" date below.
+
-
+
+
+ Last updated: {LAST_UPDATED}
+
+
+
+
+ >
+);
-
- Last Updated: {new Date().toLocaleDateString()}
-
-
-
-
- Become a Sponsor
-
-
- Learn about Mentoring
-
-
- Become a Judge
-
-
-
-
- );
-};
-
-export default PrivacyPolicy;
\ No newline at end of file
+export default PrivacyPolicy;
diff --git a/src/pages/about/success-stories/index.js b/src/pages/about/success-stories/index.js
index d5953a4e..4c17b6e8 100644
--- a/src/pages/about/success-stories/index.js
+++ b/src/pages/about/success-stories/index.js
@@ -5,7 +5,11 @@ import Image from "next/image";
import Link from "next/link";
import { Box } from "@mui/material";
import { initFacebookPixel, trackEvent } from "../../../lib/ga";
-import { RefinedRoot, RefinedFonts, Eyebrow, Arrow } from "../../../components/design/refined";
+import {
+ RefinedRoot,
+ Eyebrow,
+ Arrow,
+} from "../../../components/design/refined";
const successStories = [
{
@@ -224,7 +228,6 @@ const trackPresentationClick = (title) => {
export default function SuccessStories() {
const router = useRouter();
-
useEffect(() => {
initFacebookPixel();
}, []);
@@ -245,77 +248,269 @@ export default function SuccessStories() {
return (
<>
- Opportunity Hack Success Stories: Tech Innovation for Social Good
-
-
-
-
+
+ Opportunity Hack Success Stories: Tech Innovation for Social Good
+
+
+
+
+
-
-
-
+
+
{/* HERO */}
-
- Success stories
-
+
+
+
+ Success stories
+
+
+
Real impact, shipped.
-
- How skilled tech volunteers and nonprofits team up at Opportunity Hack to build solutions that
- keep working long after the hackathon ends.
+
+ How skilled tech volunteers and nonprofits team up at Opportunity
+ Hack to build solutions that keep working long after the hackathon
+ ends.
+
>
);
};
@@ -174,11 +232,7 @@ export async function getStaticPaths() {
const paths = Object.keys(whyPages).map((slug) => ({
params: { title: slug },
}));
-
- return {
- paths,
- fallback: true,
- };
+ return { paths, fallback: true };
}
export async function getStaticProps({ params = {} } = {}) {
@@ -192,13 +246,12 @@ export async function getStaticProps({ params = {} } = {}) {
}
return {
- props: {
+ props: {
title: pageData.title,
- openGraphData: openGraphData,
- structuredData
+ openGraphData,
+ structuredData,
},
};
-
}
export default WhyPage;
diff --git a/src/pages/about/why/index.js b/src/pages/about/why/index.js
index 274dd387..08497fd5 100644
--- a/src/pages/about/why/index.js
+++ b/src/pages/about/why/index.js
@@ -1,88 +1,64 @@
import dynamic from "next/dynamic";
import Link from "next/link";
-import {
- Button,
- Typography,
- Grid,
- Card,
- CardContent,
- Box,
- Chip,
-} from "@mui/material";
-import {
- School as SchoolIcon,
- Work as WorkIcon,
- Code as CodeIcon,
- CloudQueue as CloudIcon,
- Groups as TeamsIcon,
- TrendingUp as CareerIcon,
- Lightbulb as ImpactIcon,
- Search as FindProjectIcon,
-} from "@mui/icons-material";
-
+import Head from "next/head";
import { whyPages } from "../../../data/why-content";
+import { serializeJsonLd } from "../../../lib/jsonLd";
import {
- TitleContainer,
- LayoutContainer,
- ProjectsContainer,
-} from "../../../styles/nonprofit/styles";
-import Head from "next/head";
+ RefinedPage,
+ PageMasthead,
+} from "../../../components/design/RefinedPage";
+import { Eyebrow } from "../../../components/design/refined";
const LoginOrRegister = dynamic(
() => import("../../../components/LoginOrRegister/LoginOrRegister2"),
- { ssr: false }
+ { ssr: false, loading: () => },
);
-const style = { fontSize: 13 };
-
// Organized content by career stage/goal
const careerPathContent = [
{
- title: "Early Career",
- icon: SchoolIcon,
- description: "Build your foundation in tech through real-world experience",
+ title: "Early career",
+ description: "Build your foundation in tech through real-world experience.",
links: [
{
slug: "internship-success-build-portfolio-through-nonprofit-projects",
- title: "Stand Out for Summer Internships",
+ title: "Stand out for summer internships",
},
{
slug: "bootcamp-projects-real-world-experience",
- title: "Bootcamp Success: Build Real Projects",
+ title: "Bootcamp success: build real projects",
},
],
},
{
- title: "Career Transition",
- icon: CareerIcon,
- description: "Make your move into tech meaningful and impactful",
+ title: "Career transition",
+ description: "Make your move into tech meaningful and impactful.",
links: [
{
slug: "career-transition-tech-nonprofit",
- title: "Transition to Tech Through Nonprofit Projects",
+ title: "Transition to tech through nonprofit projects",
},
{
slug: "remote-work-experience-nonprofit",
- title: "Gain Remote Work Experience",
+ title: "Gain remote work experience",
},
{
- "slug": "how-to-get-experience-coding-when-laid-off",
- "title": "Build your Portfolio After a Layoff"
- }
+ slug: "how-to-get-experience-coding-when-laid-off",
+ title: "Build your portfolio after a layoff",
+ },
],
},
{
- title: "Leadership Growth",
- icon: TeamsIcon,
- description: "Develop leadership skills while making a difference",
+ title: "Leadership growth",
+ description: "Develop leadership skills while making a difference.",
links: [
{
slug: "team-leadership-nonprofit-projects",
- title: "Lead Technical Teams for Nonprofits",
+ title: "Lead technical teams for nonprofits",
},
{
slug: "senior-developer-impact",
- title: "Senior Developers: Create Lasting Impact",
+ title: "Senior developers: create lasting impact",
},
],
},
@@ -92,163 +68,103 @@ const careerPathContent = [
const impactPathways = [
{
slug: "coding-for-good-how-to-use-your-programming-skills-to-make-a-difference-in-the-world",
- title: "Code for Social Impact",
- icon: ImpactIcon,
+ title: "Code for social impact",
description:
- "Use your programming skills to create positive change in communities",
+ "Use your programming skills to create positive change in communities.",
},
{
slug: "how-to-find-and-work-on-nonprofit-projects-that-match-your-coding-interests-and-expertise",
- title: "Find Your Perfect Project",
- icon: FindProjectIcon,
+ title: "Find your perfect project",
description:
- "Match your skills and interests with impactful nonprofit projects",
+ "Match your skills and interests with impactful nonprofit projects.",
},
{
slug: "open-source-contribution",
- title: "Contribute to Open Source",
- icon: CodeIcon,
+ title: "Contribute to open source",
description:
- "Make meaningful open source contributions that help nonprofits",
+ "Make meaningful open source contributions that help nonprofits.",
},
{
slug: "remote-work-experience-nonprofit",
- title: "Master Remote Collaboration",
- icon: CloudIcon,
+ title: "Master remote collaboration",
description:
- "Build essential remote work skills through nonprofit projects",
+ "Build essential remote work skills through nonprofit projects.",
},
];
-const CareerPathCard = ({ title, icon: Icon, description, links }) => (
-
-
-
-
-
- {title}
-
-
-
- {description}
-
-
- {links.map((link) => (
-
- {link.title}
- {whyPages[link.slug]?.highlights && (
-
- {whyPages[link.slug].highlights
- .slice(0, 2)
- .map((highlight, i) => (
-
- ))}
-
- )}
-
- ))}
-
-
-
-);
-
-const ImpactPathwayCard = ({ slug, title, icon: Icon, description }) => (
-
-
- }
- component={Link}
- href={`/about/why/${slug}`}
- sx={{
- justifyContent: "flex-start",
- textAlign: "left",
- flexDirection: "column",
- alignItems: "flex-start",
- py: 2,
- }}
- >
-
- {title}
-
-
- {description}
-
- {whyPages[slug]?.highlights && (
-
- {whyPages[slug].highlights.slice(0, 2).map((highlight, i) => (
-
- ))}
-
- )}
-
-
-
-);
-
const structuredData = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: careerPathContent.map((path) => ({
"@type": "Question",
name: `Why choose Opportunity Hack for ${path.title.toLowerCase()}?`,
- acceptedAnswer: {
- "@type": "Answer",
- text: path.description,
- },
+ acceptedAnswer: { "@type": "Answer", text: path.description },
})),
};
+const Highlights = ({ slug }) => {
+ const items = whyPages[slug]?.highlights?.slice(0, 2) || [];
+ if (items.length === 0) return null;
+ return (
+
+ {items.map((h) => (
+
+ {h}
+
+ ))}
+
+ );
+};
+
+const CareerPathCard = ({ title, description, links }) => (
+
+
+ {title}
+
+
+ {description}
+
+
+ {links.map((link) => (
+
+
+ {link.title}{" "}
+
+ →
+
+
+
+
+ ))}
+
+
+);
+
export default function WhyIndex() {
return (
<>
@@ -264,52 +180,92 @@ export default function WhyIndex() {
name="keywords"
content="tech volunteering, coding bootcamp projects, remote work experience, tech leadership, career transition, nonprofit technology, social impact coding"
/>
+
-
-
-
- Why Code with Opportunity Hack?
-
-
-
- Whether you're starting your journey in tech, making a career
- transition, or looking to grow as a leader - Opportunity Hack
- provides real-world experience building technology that makes a
- difference. Work on meaningful projects, learn from experienced
- mentors, and build a portfolio that stands out.
-
-
-
-
-
- {careerPathContent.map((content, index) => (
-
-
-
- ))}
-
+
+
-
- Make an Impact
-
+
+ {careerPathContent.map((content) => (
+
+ ))}
+
-
- {impactPathways.map((pathway, index) => (
-
+
+ Make an impact
+
+ Pick the path that fits you.
+
+
+ {impactPathways.map((pathway) => (
+
+
+ {pathway.title}
+
+
+ {pathway.description}
+
+
+
))}
-
+
+
+
-
-
+
+
>
);
}
@@ -330,11 +286,7 @@ export async function getStaticProps() {
"Join Opportunity Hack to build real-world experience, transition careers, or grow leadership skills while creating technology that matters. Perfect for students, career changers, and tech leaders.",
key: "ogdesc",
},
- {
- property: "og:type",
- content: "website",
- key: "ogtype",
- },
+ { property: "og:type", content: "website", key: "ogtype" },
{
property: "og:image",
content: "https://cdn.ohack.dev/ohack.dev/2023_hackathon_1.webp",
diff --git a/src/pages/contact/index.js b/src/pages/contact/index.js
index b3268a1c..e2d145ee 100644
--- a/src/pages/contact/index.js
+++ b/src/pages/contact/index.js
@@ -1,46 +1,39 @@
-import React, { useState, useEffect } from "react";
-import ReCaptchaProvider from "../../components/ReCaptchaProvider";
-import { initFacebookPixel, trackEvent } from '../../lib/ga';
+import React, { useEffect, useRef, useState } from "react";
+import Head from "next/head";
+import Link from "next/link";
+import { useRouter } from "next/router";
+import axios from "axios";
import {
- Container,
- Typography,
Box,
- TextField,
Button,
- MenuItem,
- Paper,
- Grid,
- FormControlLabel,
Checkbox,
CircularProgress,
- Alert,
- Card,
- CardContent,
- Divider,
- Link,
- Chip,
Dialog,
- DialogTitle,
+ DialogActions,
DialogContent,
DialogContentText,
- DialogActions,
- useTheme,
- useMediaQuery,
+ DialogTitle,
+ FormControlLabel,
+ MenuItem,
+ TextField,
} from "@mui/material";
-import Head from "next/head";
-import { useRouter } from "next/router";
+import { ThemeProvider } from "@mui/material/styles";
+import ReCaptchaProvider from "../../components/ReCaptchaProvider";
import { useRecaptcha } from "../../hooks/use-recaptcha";
-import axios from "axios";
import { useEnv } from "../../context/env.context";
+import { initFacebookPixel, trackEvent } from "../../lib/ga";
+import { RefinedPage, PageMasthead } from "../../components/design/RefinedPage";
+import { Eyebrow } from "../../components/design/refined";
import {
- FaCheck,
- FaPaperPlane,
- FaExternalLinkAlt,
- FaEnvelope,
- FaTrophy,
-} from "react-icons/fa";
-
-// Inquiry types with associated links and content
+ refinedFormTheme,
+ refinedFieldSx,
+ refinedChoiceSx,
+ refinedSelectMenuProps,
+} from "../../components/ApplicationForm/refinedStyles";
+
+// Inquiry types with associated links and content. Keep in sync with
+// `INQUIRY_TYPE_OPTIONS` in admin/broadcast/BroadcastSourcePicker.js and the
+// backend's INQUIRY_TYPE_DISPLAY map (reply threading).
const INQUIRY_TYPES = [
{
value: "general",
@@ -112,7 +105,6 @@ const INQUIRY_TYPES = [
},
];
-// Initial form state
const initialFormState = {
firstName: "",
lastName: "",
@@ -123,12 +115,16 @@ const initialFormState = {
receiveUpdates: false,
};
+const fieldSx = { ...refinedFieldSx, mb: 0 };
+const noteCardStyle = { padding: "14px 16px", background: "var(--surface-2)" };
+const asideCardStyle = { padding: 20 };
+
/**
- * Contact form page component
+ * Contact form page. Logic (validation, reCAPTCHA, mentor/judge read-first
+ * confirmation, ?type= prefill, GA events) is unchanged from the previous
+ * version — only the presentation moved to the refined design.
*/
const ContactPage = () => {
- const theme = useTheme();
- const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const router = useRouter();
const { apiServerUrl } = useEnv();
const {
@@ -136,18 +132,23 @@ const ContactPage = () => {
isLoading: recaptchaLoading,
error: recaptchaError,
} = useRecaptcha();
+ const formRef = useRef(null);
- useEffect(() => { initFacebookPixel(); }, []);
+ useEffect(() => {
+ initFacebookPixel();
+ }, []);
- // Form state
const [formState, setFormState] = useState(initialFormState);
const [formErrors, setFormErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
const [submitError, setSubmitError] = useState("");
- const [confirmDialog, setConfirmDialog] = useState({ open: false, pendingType: null });
- // Derived: the full INQUIRY_TYPES entry for the pending confirmation
- const pendingConfirmType = INQUIRY_TYPES.find((t) => t.value === confirmDialog.pendingType) ?? null;
+ const [confirmDialog, setConfirmDialog] = useState({
+ open: false,
+ pendingType: null,
+ });
+ const pendingConfirmType =
+ INQUIRY_TYPES.find((t) => t.value === confirmDialog.pendingType) ?? null;
// Get inquiry type from URL query if available
useEffect(() => {
@@ -157,14 +158,10 @@ const ContactPage = () => {
) {
const inquiryType = router.query.type;
const hearts = router.query.hearts;
-
let message = "";
-
- // Pre-populate message for reward claims
if (inquiryType === "claim_reward" && hearts) {
message = `Hello! I have earned ${hearts} hearts and would like to claim my milestone rewards. Please let me know what steps I need to take to receive my earned rewards.\n\nThank you!`;
}
-
setFormState((prev) => ({
...prev,
inquiryType,
@@ -173,7 +170,6 @@ const ContactPage = () => {
}
}, [router.query]);
- // Handle form input changes
const handleChange = (e) => {
const { name, value, checked } = e.target;
const newValue = e.target.type === "checkbox" ? checked : value;
@@ -187,100 +183,94 @@ const ContactPage = () => {
}
}
- setFormState((prev) => ({
- ...prev,
- [name]: newValue,
- }));
-
- // Clear field error when user types
+ setFormState((prev) => ({ ...prev, [name]: newValue }));
if (formErrors[name]) {
- setFormErrors((prev) => ({
- ...prev,
- [name]: "",
- }));
+ setFormErrors((prev) => ({ ...prev, [name]: "" }));
}
};
const handleConfirmRead = () => {
- setFormState((prev) => ({ ...prev, inquiryType: String(confirmDialog.pendingType) }));
+ setFormState((prev) => ({
+ ...prev,
+ inquiryType: String(confirmDialog.pendingType),
+ }));
setFormErrors((prev) => ({ ...prev, inquiryType: "" }));
setConfirmDialog({ open: false, pendingType: null });
};
- const handleCancelConfirm = () => {
+ const handleCancelConfirm = () =>
setConfirmDialog({ open: false, pendingType: null });
- };
- // Validate form
const validateForm = () => {
const errors = {};
-
- if (!formState.firstName.trim()) {
+ if (!formState.firstName.trim())
errors.firstName = "First name is required";
- }
-
if (!formState.email.trim()) {
errors.email = "Email is required";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formState.email.trim())) {
errors.email = "Please enter a valid email address";
}
-
- if (!formState.inquiryType) {
+ if (!formState.inquiryType)
errors.inquiryType = "Please select an inquiry type";
- }
-
if (!formState.message.trim()) {
errors.message = "Message is required";
} else if (formState.message.trim().length < 10) {
errors.message = "Message is too short (minimum 10 characters)";
}
-
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
- // Handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
-
- if (!validateForm()) {
- return;
- }
-
+ if (!validateForm()) return;
setIsSubmitting(true);
setSubmitError("");
-
try {
- // Get reCAPTCHA token
const recaptchaToken = await getRecaptchaToken();
if (!recaptchaToken && process.env.NODE_ENV !== "development") {
setSubmitError("Failed to verify you are human. Please try again.");
setIsSubmitting(false);
return;
}
-
- // Mock API call - replace with actual API endpoint when available
- const response = await axios.post(`${apiServerUrl}/api/contact`, {
+ await axios.post(`${apiServerUrl}/api/contact`, {
...formState,
recaptchaToken,
});
-
- // Set success state
setSubmitSuccess(true);
- trackEvent({ action: 'contact_form_submit', params: { event_label: formState.inquiryType, page: 'contact' } });
-
- // Reset form after submission
+ trackEvent({
+ action: "contact_form_submit",
+ params: { event_label: formState.inquiryType, page: "contact" },
+ });
setFormState(initialFormState);
} catch (error) {
console.error("Error submitting contact form:", error);
- trackEvent({ action: 'contact_form_error', params: { event_label: error.message, page: 'contact' } });
+ trackEvent({
+ action: "contact_form_error",
+ params: { event_label: error.message, page: "contact" },
+ });
setSubmitError("Failed to submit your message. Please try again later.");
} finally {
setIsSubmitting(false);
}
};
- // Get selected inquiry type info
+ // Same rules as the dropdown: mentor/judge go through the read-first
+ // dialog instead of being set directly.
+ const pickInquiry = (value) => {
+ const type = INQUIRY_TYPES.find((t) => t.value === value);
+ if (type?.confirmUrl) {
+ setConfirmDialog({ open: true, pendingType: value });
+ } else {
+ setFormState((prev) => ({ ...prev, inquiryType: value }));
+ setFormErrors((prev) => ({ ...prev, inquiryType: "" }));
+ }
+ // Below the md breakpoint the form sits above the aside; bring it back into view.
+ if (typeof window !== "undefined" && window.innerWidth < 900) {
+ formRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
+ }
+ };
+
const selectedInquiryType = INQUIRY_TYPES.find(
(type) => type.value === formState.inquiryType,
);
@@ -293,429 +283,415 @@ const ContactPage = () => {
name="description"
content="Contact Opportunity Hack for questions about hackathons, mentorship, sponsorship, or general inquiries. We're here to help!"
/>
+
-
-
-
- Contact Us
-
-
+
+
+
+
- We'd love to hear from you. How can we help?
-
-
- {/* Main content area */}
-
- {/* Contact form */}
-
-
- {submitSuccess ? (
-
-
-
-
-
- Message Sent!
-
-
- Thank you for reaching out. We've received your message
- and will get back to you soon.
-
- {
- setSubmitSuccess(false);
- window.scrollTo(0, 0);
- }}
- sx={{ mt: 2 }}
- >
- Send Another Message
-
-
- ) : (
-
-
+ Every project is scored on Scope, Documentation, Polish and
+ Security, 1–5 per criterion, 40 points in total. Accessibility
+ is a separate special-category prize.
+
+
+
+
+
>
);
});
-export default JudgeDashboard;
\ No newline at end of file
+export default JudgeDashboard;
diff --git a/src/pages/praise/[id].js b/src/pages/praise/[id].js
index 2a7b92e8..3c243cfa 100644
--- a/src/pages/praise/[id].js
+++ b/src/pages/praise/[id].js
@@ -1,181 +1,188 @@
-import dynamic from 'next/dynamic';
-import { useRouter } from 'next/router';
-import Head from 'next/head';
-import { Box, Container, Button, Typography } from '@mui/material';
-import { ArrowBack } from '@mui/icons-material';
-import Link from 'next/link';
-import { fetchForStaticProps, NOT_FOUND_REVALIDATE } from '../../lib/ssgFetch';
+import dynamic from "next/dynamic";
+import { useRouter } from "next/router";
+import Head from "next/head";
+import { fetchForStaticProps, NOT_FOUND_REVALIDATE } from "../../lib/ssgFetch";
+import {
+ RefinedPage,
+ BackLink,
+ PageMasthead,
+} from "../../components/design/RefinedPage";
-const PraiseCard = dynamic(() => import('../../components/Praise/PraiseCard'), {
- ssr: true
+const PraiseCard = dynamic(() => import("../../components/Praise/PraiseCard"), {
+ ssr: true,
});
export default function PraiseDetailPage({ title, openGraphData, praiseData }) {
- const router = useRouter();
- const { id } = router.query;
-
- // Show loading state for fallback pages
- if (router.isFallback) {
- return (
-