diff --git a/public/images/hackathon-2026/hackathon-hero-small.webp b/public/images/hackathon-2026/hackathon-hero-small.webp deleted file mode 100644 index 18f46e0e..00000000 Binary files a/public/images/hackathon-2026/hackathon-hero-small.webp and /dev/null differ diff --git a/public/images/hackathon-2026/hackathon-hero.webp b/public/images/hackathon-2026/hackathon-hero.webp deleted file mode 100644 index 3222ef89..00000000 Binary files a/public/images/hackathon-2026/hackathon-hero.webp and /dev/null differ diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 4a03178b..75b21c02 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -20,7 +20,7 @@ const footerNavigation: Record = new Map([ submenu: [ ["View Projects", "/projects"], ["Start a project", "/incubator"], - ["October Challenge", "/monthly-challenge"], + ["November Challenge", "/monthly-challenge"], ["Project Opportunities", "/ideas"], ["Tools", "/tools"], ] as [string, string][], diff --git a/src/components/challenge/ChallengeProjectCard.astro b/src/components/challenge/ChallengeProjectCard.astro new file mode 100644 index 00000000..f3a2f058 --- /dev/null +++ b/src/components/challenge/ChallengeProjectCard.astro @@ -0,0 +1,70 @@ +--- +// A supported-project card for the monthly challenge pages: logo (initials +// underneath as the fallback), name, pitch, and a link to the project's own +// site when ProjectHub has one, else its /projects profile. Sits inside a +// First Light band, so the resting border is First Light Edge. +import type { ProjectItem } from "../../types/projects"; +import { projectPath } from "../../utils/projectSlug"; +import { getInitials, getProjectText, resolveLogoSrc } from "../projects/projectData"; +import { focusRing } from "../../utils/designClasses"; + +interface Props { + project: ProjectItem; +} + +const { project } = Astro.props; +const logo = resolveLogoSrc(project.logoUrl); +const website = project.websiteUrl; +--- + + +
+ + + { + logo && ( + + ) + } + +

{project.name}

+
+

+ {project.elevatorPitch || getProjectText(project)} +

+ + + + {website ? "Visit website" : "View project"} + {" "} + + {website && (opens in a new tab)} + + +
+ + diff --git a/src/components/challenge/CorrectedHeadline.astro b/src/components/challenge/CorrectedHeadline.astro new file mode 100644 index 00000000..6e33f7c5 --- /dev/null +++ b/src/components/challenge/CorrectedHeadline.astro @@ -0,0 +1,188 @@ +--- +// A news-site article header from a fictional outlet ("The Consent", after +// manufacturing consent), its headline corrected in red the way readers fix +// real ones: what was left out written in above a caret, the framing struck +// through, the truth written in after it. Used on the /monthly-challenge hero +// and the homepage popup. Everything is sized in em off the root, so `size` +// scales the whole graphic. The drawing is aria-hidden; screen readers get the +// original and corrected headlines as text. +import { correctedHeadline } from "../../data/challenges/november-2026"; + +interface Props { + size?: "lg" | "sm"; + /** "green" when placed on the Grove Green hero: the caption turns white. */ + tone?: "paper" | "green"; +} + +const { size = "lg", tone = "paper" } = Astro.props; +const { outlet, original, corrected } = correctedHeadline; +--- + +
+ +
+ + Illustrative headline from a fictional outlet: “{original}”. Corrected: “{corrected}”. + + +
+
+ + diff --git a/src/components/ChallengeQuestion.astro b/src/components/challenge/OctoberQuestion.astro similarity index 94% rename from src/components/ChallengeQuestion.astro rename to src/components/challenge/OctoberQuestion.astro index 735fa35d..2ddabab3 100644 --- a/src/components/ChallengeQuestion.astro +++ b/src/components/challenge/OctoberQuestion.astro @@ -1,5 +1,5 @@ --- -import { questionParts } from "../data/challenge"; +import { questionParts } from "../../data/challenges/october-2026"; // Inline content for the challenge heading: the parent supplies the h1/h2. // The marks are decoration only, so screen readers hear the plain question. diff --git a/src/components/home/ChallengePopup.astro b/src/components/home/ChallengePopup.astro new file mode 100644 index 00000000..6eae98bf --- /dev/null +++ b/src/components/home/ChallengePopup.astro @@ -0,0 +1,416 @@ +--- +// Homepage announcement for the November 2026 Incubator challenge. A compact +// card: the question on Grove Green (the same surface as the challenge page +// hero, so the click-through feels continuous), the actions on paper below. +// Centred on desktop, a bottom sheet on phones. Shown once per visitor, after +// 8s or a quarter of the page scrolled, and the newsletter popup stands down +// for that page load. Renders nothing after November; delete it then. +import { CHALLENGE_PATH } from "../../data/challenges"; +import { APPLY_URL, brainstorm, challenge } from "../../data/challenges/november-2026"; +import { isBrainstormOpen, isCampaignLive } from "../../utils/challengePopup"; +import CorrectedHeadline from "../challenge/CorrectedHeadline.astro"; + +const now = Date.now(); +const live = isCampaignLive(now); +const brainstormOpen = isBrainstormOpen(now); +--- + +{ + live && ( + +
+
+ +

New Incubator Challenge · {challenge.month}

+

+ {challenge.question} +

+
+ +
+ {/* The page hero's corrected headline in miniature, so the + click-through lands somewhere familiar. */} +
+ +
+ {brainstormOpen ? ( +

+ Build the tools that call out framing like this. Apply to the Incubator, or pitch your + idea first at our open brainstorming session. +

+ ) : ( +

+ Build the tools that call out framing like this. Apply to the Incubator and join this + month's cohort. +

+ )} + + {brainstormOpen && ( +

+ +

+ )} + + + + +
+
+
+ ) +} + + + + diff --git a/src/components/home/NewsletterPopup.tsx b/src/components/home/NewsletterPopup.tsx index 1725545f..e53c2237 100644 --- a/src/components/home/NewsletterPopup.tsx +++ b/src/components/home/NewsletterPopup.tsx @@ -17,6 +17,10 @@ import { safeLocalStorage, writePopupState, } from "../../utils/newsletterPopup"; +import { + SHOWN_ATTRIBUTE as CHALLENGE_SHOWN_ATTRIBUTE, + SHOWN_EVENT as CHALLENGE_SHOWN_EVENT, +} from "../../utils/challengePopup"; // Homepage mailing-list popup. Spec: docs/superpowers/specs/2026-09-23-newsletter-popup-design.md @@ -95,6 +99,17 @@ function Popup() { const bottomFormInView = useInView(FOOTER_SECTION_ID); const isDesktop = useIsDesktop(); const [closed, setClosed] = useState(false); + // The homepage challenge popup wins the page load: once it has opened, this + // popup stays hidden until the next page load. Not a dismissal, so nothing + // is written to storage. + const [challengeShown, setChallengeShown] = useState(() => + document.documentElement.hasAttribute(CHALLENGE_SHOWN_ATTRIBUTE) + ); + useEffect(() => { + const onChallengeShown = () => setChallengeShown(true); + window.addEventListener(CHALLENGE_SHOWN_EVENT, onChallengeShown); + return () => window.removeEventListener(CHALLENGE_SHOWN_EVENT, onChallengeShown); + }, []); // Desktop opens straight to the card; mobile starts collapsed to a slim bar // (see the render branch below — the same collapsed/expanded split now also // drives the 30s idle auto-collapse on both). @@ -104,7 +119,7 @@ function Popup() { const shownRef = useRef(false); const resetIdleTimerRef = useRef(() => {}); - const mounted = enabled && eligible && triggered && !closed; + const mounted = enabled && eligible && triggered && !closed && !challengeShown; const visible = mounted && !bottomFormInView; useEffect(() => { diff --git a/src/data/challenges/index.ts b/src/data/challenges/index.ts new file mode 100644 index 00000000..ce1803fd --- /dev/null +++ b/src/data/challenges/index.ts @@ -0,0 +1,27 @@ +/** Shared copy for the monthly Incubator challenge pages, plus the archive of + * past months listed at the foot of /monthly-challenge. */ + +import { challenge as october2026 } from "./october-2026"; + +export const CHALLENGE_PATH = "/monthly-challenge"; + +export interface PreviousChallenge { + month: string; + question: string; + href: string; +} + +/** Newest first. Each archived month keeps its own page under /monthly-challenge/. */ +export const previousChallenges: PreviousChallenge[] = [ + { + month: october2026.month, + question: october2026.question, + href: `${CHALLENGE_PATH}/october-2026`, + }, +]; + +export const whatAreChallenges: string[] = [ + "To date, the Tech for Palestine Incubator has supported more than 100 projects working on different aspects of Palestinian liberation. These projects span themes like media bias, protest tech, divestment, exposing complicit companies, and much more.", + "Applications for the Incubator are open on a rolling basis for any relevant idea which furthers the Palestinian cause. On top of that, each month we pose one open challenge to the community, to direct efforts towards priorities we consider urgent.", + "The projects which apply for the challenge and are approved to join the Incubator form a small cohort, with tailored capacity building and opportunities for close collaboration with the other projects in the cohort.", +]; diff --git a/src/data/challenges/november-2026.ts b/src/data/challenges/november-2026.ts new file mode 100644 index 00000000..37d1d69c --- /dev/null +++ b/src/data/challenges/november-2026.ts @@ -0,0 +1,115 @@ +/** Copy for /monthly-challenge: the November 2026 Incubator challenge on + * mainstream media bias. When December's challenge lands, move this month into + * `previousChallenges` (./index.ts) the way October was archived. */ + +import type { WantedProject } from "../../utils/challengeProjects"; + +/** Where "Apply for this challenge" goes, per the brief. */ +export const APPLY_URL = "https://projecthub.techforpalestine.org/apply"; + +export const challenge = { + month: "November 2026", + question: + "How do we expose and combat hasbara, pro-Israel bias and Zionist ownership and influence of mainstream news media?", + summary: + "This month we're forming a cohort of projects that expose how the news is shaped, and build ways to get it out that can't be bought.", +}; + +/** The brainstorming session on Zoom. Times are CET (UTC+1 in November). */ +export const brainstorm = { + url: "https://us06web.zoom.us/meeting/register/VcASwZtkTsmH3poubwJcOw", + startsAt: "2026-11-20T20:30:00+01:00", + /** When the session is assumed over; the promo hides after this. */ + endsAt: "2026-11-20T22:00:00+01:00", + weekday: "Friday", + day: "20", + monthShort: "Nov", + time: "8:30pm CET", + description: + "Tech for Palestine is hosting an open brainstorming session for anyone who is interested in this challenge and wants to pitch an idea. You will be able to learn more about the T4P Incubator, get feedback on your idea, and find team members.", +}; + +/** The corrected-headline graphic (CorrectedHeadline.astro) on the page hero + * and homepage popup. A fictional outlet and an illustrative headline built + * from the patterns the brief names; no real outlet is quoted. These two lines + * are what screen readers get in place of the drawn corrections. */ +export const correctedHeadline = { + outlet: "The Consent", + original: "38 killed in Gaza strike as tensions rise, health ministry says", + corrected: "38 Palestinians killed in Gaza strike by Israel, genocide continues", +}; + +export const aboutLead = + "The mainstream media narrative on Palestine and the Gaza genocide is flooded with hasbara propaganda and consistently framed around Israel's talking points. It manufactures consent for, normalizes and downplays the atrocities Israel commits, through a playbook that is easy to spot once named."; + +export interface PlaybookMove { + name: string; + detail: string; + /** 1-based numbers of the questions below that take this move on. */ + questions: number[]; +} + +/** The brief's account of how coverage is bent, one move per line. */ +export const playbook: PlaybookMove[] = [ + { + name: "Passive voice", + detail: "Atrocities are reported without naming who committed them.", + questions: [3, 4], + }, + { + name: "Unequal coverage", + detail: "Israeli deaths and casualties get far more coverage than Palestinian ones.", + questions: [1, 3], + }, + { + name: "Selective doubt", + detail: + "Fatality figures from Palestinian institutions are questioned, while Israeli claims are cited without question.", + questions: [5], + }, + { + name: "Silenced newsrooms", + detail: + "Journalists worldwide are penalized for positions sympathetic to Palestine, and newsroom guidelines restrict reporting, even simply using the word \u201cPalestine\u201d.", + questions: [6, 9], + }, + { + name: "Pro-Israel owners", + detail: + "With Paramount's takeover of Warner Bros, some of the dominant US news operations are now owned by the Ellison family, known for its support for Israel and its million-dollar donations to Friends of the IDF.", + questions: [7, 8], + }, +]; + +export const aboutNeed = + "We urgently need independent media which can report on Palestine and Israel objectively, as well as transparency and public awareness of the positionality of different news sources."; + +export const ask = + "This challenge asks you to find ways to combat the different aspects of large-scale, systemic pro-Israel influence over mainstream media: monitoring and exposing bias, raising awareness of hasbara and encouraging critical thinking, and limiting the influence of biased outlets over the public narrative."; + +export const especially = + "We're especially interested in sovereign, independent ways to publish and disseminate news, including non-traditional channels that are harder to influence."; + +export const questions: string[] = [ + "How do we ensure that the Palestinian struggle is top of mind for the general audience even if mainstream media tries to bury it?", + "How do we develop alternative ways of sharing news using analog media, local distribution, peer-to-peer sharing and other channels which are resistant to outside control and influence?", + "How do we detect and raise awareness about pro-Israel and/or anti-Palestine bias and framings in mainstream media?", + "How do we encourage critical thinking among readers and viewers and ensure they are aware of the positionality of the news sources they are following?", + "How do we fact-check claims in mainstream media and trace their origin to state PR messaging when they are the product of hasbara?", + "How do we expose the unofficial editorial guidelines which media outlets use in their reporting on Palestine and detect regurgitations of Israeli talking points?", + "How do we build a public ownership atlas of news media, showing who owns which outlet and their documented political donations and advocacy funding?", + "How do we track the changes in coverage of Palestine and Israel by news outlets like CNN and CBS before and after the takeover by Paramount?", + "How do we document and support journalists penalized for pro-Palestine reporting and encourage reporters to maintain journalistic integrity?", +]; + +/** Incubator projects already working on this challenge, in display order. + * Matched against ProjectHub by name; a project only shows once it is + * published there. Add an alias if a card goes missing. */ +export const supportedProjects: WantedProject[] = [ + { name: "NewsCord" }, + { name: "Accountable Media" }, + { name: "Media Bias Meter", aliases: ["Media-Bias-Meter"] }, + { name: "MediaBiasWatch" }, + { name: "Bias-Proof" }, + { name: "Bulletin" }, +]; diff --git a/src/data/challenge.ts b/src/data/challenges/october-2026.ts similarity index 72% rename from src/data/challenge.ts rename to src/data/challenges/october-2026.ts index e69fbb93..1a2be52c 100644 --- a/src/data/challenge.ts +++ b/src/data/challenges/october-2026.ts @@ -1,9 +1,9 @@ -/** Copy for /monthly-challenge: the monthly Incubator challenge. Swap this file's - * contents when the next month's challenge is announced. */ +/** Copy for the archived October 2026 challenge at /monthly-challenge/october-2026. + * Kept as published; only the hackathon promo (over on Oct 31) was removed. */ -import type { WantedProject } from "../utils/challengeProjects"; +import type { WantedProject } from "../../utils/challengeProjects"; -/** The question, split so the hero and homepage popup can strike through what +/** The question, split so the hero can strike through what * people leave and highlight where they go. */ export const questionParts: { text: string; mark?: "strike" | "highlight" }[] = [ { text: "How do we make it easier for users to migrate from " }, @@ -95,16 +95,3 @@ export const supportedProjects: WantedProject[] = [ { name: "Boycott Shopify" }, { name: "Migrate Off Vercel", aliases: ["Migrate from Vercel"] }, ]; - -export const hackathon = { - url: "https://hackathon2026.techforpalestine.org/", - title: "Join our upcoming hackathon", - description: - "As part of Mozilla Festival, Tech for Palestine will hold a hackathon in Barcelona, Spain on October 31st on this same challenge. Join us in-person at Canodrom in Barcelona for a full day of hands-on work and collaboration, powered by Thaura.ai!", -}; - -export const whatAreChallenges: string[] = [ - "To date, the Tech for Palestine Incubator has supported more than 90 projects working on different aspects of Palestinian liberation. These projects span themes like media bias, protest tech, divestment, exposing complicit companies, and much more.", - "Applications for the Incubator are open on a rolling basis for any relevant idea which furthers the Palestinian cause. On top of that, each month we pose one open challenge to the community, to direct efforts towards priorities we consider urgent.", - "The projects which apply for the challenge and are approved to join the Incubator form a small cohort, with tailored capacity building and opportunities for close collaboration with the other projects in the cohort.", -]; diff --git a/src/pages/index.astro b/src/pages/index.astro index f330be6c..2927ea68 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,6 +4,7 @@ import SignUpForm from "../structures/SignUpForm.astro"; import CommunityCallBanner from "../components/CommunityCallBanner.astro"; import MembershipLaunchBanner from "../components/home/MembershipLaunchBanner.astro"; import NewsletterPopup from "../components/home/NewsletterPopup"; +import ChallengePopup from "../components/home/ChallengePopup.astro"; import "../styles/base.css"; const stats = [ @@ -488,6 +489,7 @@ const cardBase = `group relative flex flex-col rounded-lg border border-ink-divi + + diff --git a/src/pages/monthly-challenge.astro b/src/pages/monthly-challenge/october-2026.astro similarity index 63% rename from src/pages/monthly-challenge.astro rename to src/pages/monthly-challenge/october-2026.astro index 688c4dba..113d40f9 100644 --- a/src/pages/monthly-challenge.astro +++ b/src/pages/monthly-challenge/october-2026.astro @@ -1,25 +1,26 @@ --- -import Layout from "../layouts/Layout.astro"; -import ChallengeQuestion from "../components/ChallengeQuestion.astro"; -import { APPLY_URL } from "../data/incubator"; +// Archived October 2026 challenge, kept as published apart from the hackathon +// promo (the event was Oct 31). Listed under "Previous challenges" on +// /monthly-challenge. +import Layout from "../../layouts/Layout.astro"; +import ChallengeQuestion from "../../components/challenge/OctoberQuestion.astro"; +import { APPLY_URL } from "../../data/incubator"; import { about, approach, migrations, challenge, exampleTargets, - hackathon, otherTargets, supportedProjects, - whatAreChallenges, -} from "../data/challenge"; -import { fetchProjectsData } from "../store/projectsClient"; -import type { ProjectItem } from "../types/projects"; -import { pickChallengeProjects } from "../utils/challengeProjects"; -import { projectPath } from "../utils/projectSlug"; -import { getInitials, getProjectText, resolveLogoSrc } from "../components/projects/projectData"; -import { isCampaignLive } from "../utils/hackathonPopup"; -import "../styles/base.css"; +} from "../../data/challenges/october-2026"; +import { CHALLENGE_PATH, whatAreChallenges } from "../../data/challenges"; +import { challenge as currentChallenge } from "../../data/challenges/november-2026"; +import { fetchProjectsData } from "../../store/projectsClient"; +import type { ProjectItem } from "../../types/projects"; +import { pickChallengeProjects } from "../../utils/challengeProjects"; +import ChallengeProjectCard from "../../components/challenge/ChallengeProjectCard.astro"; +import "../../styles/base.css"; const container = "mx-auto max-w-[1280px] px-6 sm:px-8"; const h2 = @@ -44,15 +45,25 @@ try { // Don't let the middleware cache the degraded page past the outage. Astro.response.headers.set("Cache-Control", "no-store"); } - -const showHackathon = isCampaignLive(Date.now()); ---
+ +
+

+ Archived challenge, {challenge.month}. + + See the {currentChallenge.month} challenge + +

+
+
Apply for this challenge - { - showHackathon && ( - - Or join the hackathon, Oct 31 - - ) - }
@@ -193,53 +197,11 @@ const showHackathon = isCampaignLive(Date.now()); { projects && projects.length > 0 ? ( ) : (

@@ -254,50 +216,8 @@ const showHackathon = isCampaignLive(Date.now());

- - { - showHackathon && ( -
-
- Hackathon participants laughing and debating ideas around a long workshop table -
-

Mozilla Festival · Barcelona · October 31

-

- {hackathon.title} -

-

{hackathon.description}

- - Join the hackathon - (opens in a new tab) - -
-
-
- ) - } - -
+