From 110560706c9af5e2ecb47a74be394e8962ba2159 Mon Sep 17 00:00:00 2001 From: mohanadft Date: Thu, 5 Nov 2026 14:08:18 +0200 Subject: [PATCH 1/9] feat(design): redesign Team, Media, FAQ and Contact on the live design system - Team: editorial board rows with full bios, dense staff directory, membership CTA - Media: press release inline with progressive disclosure (replaces modal, keeps hash deep link and Plausible event), dated coverage list, accessible logo downloads - FAQ: native details accordion, MUI removed, loading/error/empty states, deep links now open the targeted question - Contact: routing list to the right channel per need, no-JS email fallback - All four: First Light bands, one Nightfall terminal CTA, Grove tokens, gray/green-800/blue/gradient drift removed Co-Authored-By: Claude Opus 5.5 --- src/components/faq/FAQAccordion.tsx | 61 +- src/components/faq/FAQList.tsx | 174 ++++-- src/components/media/PressRelease.astro | 196 +++++++ src/components/media/PressReleaseDialog.astro | 183 ------ src/pages/contact.astro | 316 +++++----- src/pages/faq.astro | 80 ++- src/pages/media.astro | 541 ++++++++++-------- src/pages/team.astro | 483 +++++++--------- 8 files changed, 1130 insertions(+), 904 deletions(-) create mode 100644 src/components/media/PressRelease.astro delete mode 100644 src/components/media/PressReleaseDialog.astro diff --git a/src/components/faq/FAQAccordion.tsx b/src/components/faq/FAQAccordion.tsx index 64ae142b..9166a460 100644 --- a/src/components/faq/FAQAccordion.tsx +++ b/src/components/faq/FAQAccordion.tsx @@ -1,6 +1,4 @@ import * as React from "react"; -import { Accordion, AccordionSummary, AccordionDetails, Typography } from "@mui/material"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import RichTextRenderer from "../shared/RichTextRenderer"; import type { RichTextSegment } from "../../types/richText"; @@ -9,31 +7,60 @@ type FAQAccordionProps = { answer: React.ReactNode | string | RichTextSegment[]; }; +// Stable anchor for a question. Kept identical to the id the old MUI +// accordion put on its summary, so any `/faq#faq-...` link already shared +// keeps resolving. +export function faqAnchorId(question: string): string { + return `faq-${question.replaceAll(" ", "-")}`; +} + +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-grove"; +const answerText = "max-w-[65ch] text-[17px] leading-[1.7] text-ink-secondary min-[810px]:text-lg"; +const answerLink = `font-bold text-grove underline decoration-grove/40 underline-offset-4 transition-colors hover:decoration-grove motion-reduce:transition-none ${focusRing}`; + +// One question, as a native
: keyboard operable (Enter/Space on the +// summary), announced as expandable by screen readers, and searchable with +// find-in-page, with no script and no MUI. Rendered inside FAQList's
    , +// whose hairline dividers separate the items. export default function FAQAccordion({ question, answer }: FAQAccordionProps) { return ( - - } - aria-controls="faq-content" - id={`faq-${question.replaceAll(" ", "-")}`} +
    + - {question} - - + {question} + + +
    {typeof answer === "string" ? ( - {answer} +

    {answer}

    ) : Array.isArray(answer) ? ( ) : ( answer )} - - +
    +
    ); } diff --git a/src/components/faq/FAQList.tsx b/src/components/faq/FAQList.tsx index a5b2112c..293c2678 100644 --- a/src/components/faq/FAQList.tsx +++ b/src/components/faq/FAQList.tsx @@ -1,6 +1,5 @@ -import React, { useState, useEffect } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import FAQAccordion from "./FAQAccordion"; -import { CircularProgress, Typography, Box } from "@mui/material"; import type { RichTextSegment } from "../../types/richText"; interface FAQItem { @@ -14,87 +13,148 @@ interface FAQListProps { loading?: boolean; } +type LoadStatus = "loading" | "ready" | "error"; + +// Varied widths so the placeholder reads as questions of different lengths +// rather than a uniform stack of bars. +const SKELETON_WIDTHS = ["w-4/5", "w-3/5", "w-11/12", "w-2/3", "w-3/4", "w-1/2"]; + +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-grove"; +const inlineLink = `font-bold text-grove underline decoration-grove/40 underline-offset-4 transition-colors hover:decoration-grove motion-reduce:transition-none ${focusRing}`; +const ghostButton = `inline-flex min-h-[44px] items-center justify-center rounded-pill border border-ink px-5 py-3 text-[13px] font-bold text-ink transition-colors hover:border-grove hover:text-grove motion-reduce:transition-none ${focusRing}`; +const stateText = "max-w-[52ch] text-[17px] leading-[1.7] text-ink-secondary"; + +// `?showAll=yes` asks the API for hidden (non-Visible) Notion entries too, for +// editors previewing drafts. +function faqEndpoint(): string { + const showAll = new URLSearchParams(window.location.search).get("showAll") === "yes"; + return showAll ? `/api/faq?showAll=${encodeURIComponent("yes")}` : "/api/faq"; +} + +// The list mounts client-side, after the browser has already tried (and +// failed) to honour a `#faq-...` fragment. Once the questions exist, open the +// targeted one and bring it into view. +function openHashTarget(): void { + const hash = window.location.hash.slice(1); + if (!hash) return; + const target = document.getElementById(decodeURIComponent(hash)); + if (!(target instanceof HTMLDetailsElement)) return; + target.open = true; + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + target.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "start" }); + target.querySelector("summary")?.focus({ preventScroll: true }); +} + +function LoadingState() { + return ( +
    + Loading questions + +
    + ); +} + +function ErrorState({ onRetry }: { onRetry: () => void }) { + return ( +
    +

    The questions didn't load.

    +

    + This is on our side, not yours. Try again, or{" "} + + send us your question + {" "} + and we'll answer it directly. +

    + +
    + ); +} + +function EmptyState() { + return ( +
    +

    + No questions are published right now. +

    +

    + We're updating this page.{" "} + + Ask us directly + {" "} + in the meantime. +

    +
    + ); +} + export default function FAQList({ faqs: initialFaqs = [], loading: initialLoading = false, }: FAQListProps) { const [faqs, setFaqs] = useState(initialFaqs); - const [loading, setLoading] = useState(initialLoading); - - const urlSearchParams = new URLSearchParams(window.location.search); - const params = Object.fromEntries(urlSearchParams.entries()); - const showAll = params?.showAll === "yes"; + const [status, setStatus] = useState( + initialFaqs.length === 0 || initialLoading ? "loading" : "ready" + ); - const fetchFreshFAQs = async () => { - setLoading(true); + const fetchFreshFAQs = useCallback(async () => { + setStatus("loading"); try { - console.log("Fetching fresh FAQs from Notion API..."); - // Encode the showAll parameter - const encodedShowAll = encodeURIComponent(showAll ? "yes" : "no"); - const response = await fetch(showAll ? `/api/faq?showAll=${encodedShowAll}` : "/api/faq", { + const response = await fetch(faqEndpoint(), { cache: "no-cache", headers: { "Cache-Control": "no-cache", Pragma: "no-cache", }, }); - - if (response.ok) { - const newFaqs = await response.json(); - console.log(`Refreshed: Loaded ${newFaqs.length} FAQs from Notion`); - setFaqs(newFaqs); - } else { + if (!response.ok) { console.error("Failed to fetch FAQs:", response.status); + setStatus("error"); + return; } + const newFaqs: FAQItem[] = await response.json(); + setFaqs(newFaqs); + setStatus("ready"); } catch (error) { - console.error("Failed to fetch fresh FAQs:", error); - } finally { - setLoading(false); + console.error("Failed to fetch FAQs:", error); + setStatus("error"); } - }; + }, []); useEffect(() => { - console.log("FAQList component mounted:", { - initialFaqsLength: initialFaqs.length, - initialLoading, - currentLoadingState: loading, - }); - if (initialFaqs.length === 0) { - console.log("No initial FAQs, fetching from API..."); fetchFreshFAQs(); - } else { - console.log(`Using ${initialFaqs.length} initial FAQs from SSR, setting loading to false`); - setLoading(false); } - }, []); + }, [fetchFreshFAQs, initialFaqs.length]); - if (loading) { - return ( - - - - Loading FAQs... - - - ); - } - - if (faqs.length === 0) { - return ( - - - No FAQs available at the moment. - - - ); - } + useEffect(() => { + if (status === "ready" && faqs.length > 0) { + openHashTarget(); + } + }, [status, faqs]); + + if (status === "loading") return ; + if (status === "error") return ; + if (faqs.length === 0) return ; return ( -
    +
      {faqs.map((faq, index) => ( - +
    • + +
    • ))} -
    +
); } diff --git a/src/components/media/PressRelease.astro b/src/components/media/PressRelease.astro new file mode 100644 index 00000000..2c3746c1 --- /dev/null +++ b/src/components/media/PressRelease.astro @@ -0,0 +1,196 @@ +--- +// The Nov 2026 member-driven press release, shown inline on /media as a band +// with progressive disclosure (it replaced the old PressReleaseDialog modal). +// The shareable link is still /media#member-driven-press-release: the section +// carries that id, so the browser scrolls to it, and the script below expands +// the full text. Keep PRESS_RELEASE_ID in sync with the hero link in media.astro. +const PRESS_RELEASE_ID = "member-driven-press-release"; + +const container = "mx-auto max-w-[1280px] px-6"; +const overline = "text-xs font-bold uppercase tracking-[0.14em] text-grove-deep"; +const body = "text-[19px] leading-relaxed min-[810px]:text-[21px]"; +const subhead = "mt-10 text-[22px] font-extrabold leading-snug text-ink min-[810px]:text-[24px]"; +const para = "mt-4 text-lg leading-relaxed text-ink-secondary"; +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-grove"; +const inlineLink = `font-bold text-grove underline decoration-grove/40 underline-offset-4 transition-colors hover:decoration-grove motion-reduce:transition-none ${focusRing}`; +--- + +
+
+ +
+

Press release

+

+ +

+
+
Media contact
+
Afzal Mangal
+
+ afzal@techforpalestine.org +
+
+
+ +
+

+ Tech for Palestine, Behind Apps With More Than 10 Million Users, Becomes a Member-Driven + Organization +

+ +

+ After three years as an incubator, Tech for Palestine is ready for more: more projects, + built faster, with more impact. That is why it now puts members at the center. +

+ + Tech for Palestine conference in Barcelona, 2026 + +

+ Tech for Palestine (T4P), whose projects have reached more than 10 million users in less + than three years, today opened its membership to the public. From November 2, anyone can + become a member and build advocacy projects under the T4P banner. Members get a team to work + with and the backing of the organization behind Big Tech alternatives such as UpScrolled and + Thaura, and advocacy tools such as Boycat and NewsCord. +

+ +
+ + Read the full release + + + + +
+

From incubator to member-driven organization

+

+ For the past three years, the T4P incubator helped the people behind more than 90 + initiatives turn their ideas into advocacy tools. Those teams received mentorship, + connections and support, but the building itself happened in their own organizations, + outside T4P. +

+

+ T4P is now bringing the building inside. The support it offered external teams is + available to every member. New projects carry the T4P name, and members build them + together. +

+
+

+ Our incubator proved that small + teams with the right skills can build tools used by millions. Now we build with many + more people, and with far less distance between an idea and the team that ships it. +

+
+ +

How membership works

+

+ Members join with the skills they have: engineering, design, research, writing or + organizing. Some join an existing project that needs their expertise. Others bring their + own idea and start a new team, which T4P mentors help scope, staff with fellow members + and take to a first public version. +

+

+ The model can move fast. When Macklemore was dropped from Ed Sheeran’s US tour after + saying “Free Palestine” on stage, he explained that Sheeran could not get past his + public stance of “I don’t take sides.” T4P members picked up the moment and built Ed + Takes No Sides, a directory where artists who do take a stand for Palestine can create a + profile and be found. +

+ +

25+ projects from a 200-member private beta

+

+ Before the public launch, T4P tested membership for several months with 200 members. In + that time, members built more than 25 new advocacy projects, including: +

+
    +
  • Israel Bonds Tracker, a platform that raises awareness about Israeli bonds.
  • +
  • + Sponsored internships connecting students from Gaza with internships in the UK and + Ireland. +
  • +
  • + Profile Pic Maker, revived by members: a tool that adds a Palestinian-flag frame to + social media profile pictures. +
  • +
  • + A research team investigating bias against Palestinians in frontier large language + models. +
  • +
+ +

Two ways to join

+

+ Members contribute time and skills. They can + join an existing team or form a new one with members who complement their skills. Sign-up + is at techforpalestine.org/membership, and + the T4P team helps each new member find a first project. +

+

+ Supporting members back the work financially + and leave the building to others. Their dues fund the marketing, engineering, mentorship, + software access, partnerships and grants that T4P provides to its projects. Details are at + techforpalestine.org/supporting-member. +

+
+
+
+
+
+ + diff --git a/src/components/media/PressReleaseDialog.astro b/src/components/media/PressReleaseDialog.astro deleted file mode 100644 index 9655e0a9..00000000 --- a/src/components/media/PressReleaseDialog.astro +++ /dev/null @@ -1,183 +0,0 @@ ---- -// The Nov 2026 member-driven press release, shown as a modal on /media. -// It has no page of its own: the shareable link is /media#, -// which opens the dialog on load (see the script below). Keep the id in sync -// with the press release entry in src/pages/media.astro. -const PRESS_RELEASE_ID = "member-driven-press-release"; ---- - - -
-

Press release

- -
- -
-

November 2, 2026

- -

- Tech for Palestine, Behind Apps With More Than 10 Million Users, Becomes a Member-Driven - Organization -

- -

- After three years as an incubator, Tech for Palestine is ready for more: more projects, built - faster, with more impact. That is why it now puts members at the center. -

- -

- Tech for Palestine (T4P), whose projects have reached more than 10 million users in less than - three years, today opened its membership to the public. From November 2, anyone can become a - member and build advocacy projects under the T4P banner. Members get a team to work with and - the backing of the organization behind Big Tech alternatives such as UpScrolled and Thaura, - and advocacy tools such as Boycat and NewsCord. -

- -

- From incubator to member-driven organization -

-

- For the past three years, the T4P incubator helped the people behind more than 90 initiatives - turn their ideas into advocacy tools. Those teams received mentorship, connections and - support, but the building itself happened in their own organizations, outside T4P. -

-

- T4P is now bringing the building inside. The support it offered external teams is available to - every member. New projects carry the T4P name, and members build them together. -

-
- “Our incubator proved that small teams with the right skills can build tools used by millions. - Now we build with many more people, and with far less distance between an idea and the team - that ships it.” -
- -

How membership works

-

- Members join with the skills they have: engineering, design, research, writing or organizing. - Some join an existing project that needs their expertise. Others bring their own idea and - start a new team, which T4P mentors help scope, staff with fellow members and take to a first - public version. -

-

- The model can move fast. When Macklemore was dropped from Ed Sheeran’s US tour after saying - “Free Palestine” on stage, he explained that Sheeran could not get past his public stance of - “I don’t take sides.” T4P members picked up the moment and built Ed Takes No Sides, a - directory where artists who do take a stand for Palestine can create a profile and be found. -

- -

- 25+ projects from a 200-member private beta -

-

- Before the public launch, T4P tested membership for several months with 200 members. In that - time, members built more than 25 new advocacy projects, including: -

-
    -
  • Israel Bonds Tracker, a platform that raises awareness about Israeli bonds.
  • -
  • - Sponsored internships connecting students from Gaza with internships in the UK and Ireland. -
  • -
  • - Profile Pic Maker, revived by members: a tool that adds a Palestinian-flag frame to social - media profile pictures. -
  • -
  • - A research team investigating bias against Palestinians in frontier large language models. -
  • -
- -

Two ways to join

-

- Members contribute time and skills. They can join an existing team or form a new - one with members who complement their skills. Sign-up is at - techforpalestine.org/membership, and the T4P team helps each new member find a first project. -

-

- Supporting members back the work financially and leave the building to others. - Their dues fund the marketing, engineering, mentorship, software access, partnerships and grants - that T4P provides to its projects. Details are at - techforpalestine.org/supporting-member. -

- -

Media contact

-

- Afzal Mangal - - afzal@techforpalestine.org -

-
-
- - - - diff --git a/src/pages/contact.astro b/src/pages/contact.astro index 40f737c5..dc1d4e31 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -1,159 +1,199 @@ --- import Layout from "../layouts/Layout.astro"; import "../styles/base.css"; + +// Shared class strings: the live system's type scale (DESIGN.md §3) in plain +// Tailwind, since Layout.astro does not load design-system.css. Mirrors +// about.astro so the two pages read as one system. +const container = "mx-auto max-w-[1280px] px-6"; +const headline = "text-[28px] font-extrabold leading-[1.15] text-ink min-[810px]:text-[38px]"; +const overline = "text-xs font-bold uppercase tracking-[0.14em] text-grove-deep"; +const body = "text-[19px] leading-relaxed min-[810px]:text-[21px]"; +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-grove"; +const pill = `inline-flex min-h-[44px] items-center justify-center gap-2 rounded-pill px-5 py-3 text-[13px] font-bold transition-colors motion-reduce:transition-none ${focusRing}`; +const inverseButton = `${pill} bg-white text-nightfall hover:bg-first-light`; +const inlineLink = `font-bold text-grove underline decoration-grove/40 underline-offset-4 transition-colors hover:decoration-grove motion-reduce:transition-none ${focusRing}`; +const routeLink = `inline-flex min-h-[44px] items-center gap-2 font-bold text-grove transition-colors hover:text-grove-hover motion-reduce:transition-none ${focusRing}`; + +// The routing table. Each row answers "who are you, and where do you go?" +// `email` is shown as visible text beside the page link when a direct +// address exists for that need. +const routes = [ + { + id: "press", + need: "Press and media", + detail: + "Journalists and media professionals: press releases, coverage and the media contact live on our Media page.", + href: "/media", + action: "Visit the Media page", + email: "media@techforpalestine.org", + }, + { + id: "endorsements", + need: "Endorsements", + detail: + "Seeking a T4P endorsement for your campaign or organization? See what we look for, then submit a request.", + href: "/endorsements", + action: "Request an endorsement", + }, + { + id: "incubator", + need: "Building a project", + detail: + "Launching or scaling a project for Palestinian liberation? The incubator offers mentorship, volunteers and connections.", + href: "/incubator", + action: "Apply to the incubator", + }, + { + id: "volunteer", + need: "Volunteering", + detail: "Put your skills to work on projects that need them.", + href: "/volunteer", + action: "Find volunteer roles", + }, +]; ---
-
-
- - +

- - - - - + Contact Tech for Palestine +

+

+ Questions, press, endorsements or a project for Palestinian liberation: find the right + channel below and reach the people who can help. +

+
-
-
- -
-

- Tech for Palestine -

-

- Contact Us -

-

- Have a question, media inquiry, or want to request an endorsement? We'd love to hear - from you. -

-
+
+

General questions

+

+ For inquiries about Tech for Palestine, our projects, or how to get involved. +

+ contact [at] techforpalestine [dot] org + +
+
+ - -
- -
-
- - - -
-

General Questions

-

- For general inquiries about Tech for Palestine, our projects, or how to get - involved. -

- - -
+ +
+
+

Something specific?

+

+ Go straight to the right place +

- -
-
- + { + routes.map((route) => ( +
  • +

    - - -

  • -

    Media & Press

    -

    - Journalists and media professionals can find press resources and contact information - on our media page. -

    - - Visit our Media page → - -
    + {route.need} + +
    +

    + {route.detail} +

    + {route.email && ( +

    + Or email{" "} + + {route.email} + +

    + )} +
    + + {route.action} + + + + )) + } + +
    +
    - -
    -
    - - - -
    -

    Endorsements

    -

    - Seeking a T4P endorsement for your campaign or organization? Learn what we look for - and submit a request. -

    - - Request an endorsement → - -
    + +
    +
    +
    +
    +

    + Want to do more than get in touch? +

    +

    + Members fund the work and join the teams building tech for Palestinian liberation. +

    + + Become a member + +
    -
    +
    diff --git a/src/pages/faq.astro b/src/pages/faq.astro index 05f8a240..e29ed55e 100644 --- a/src/pages/faq.astro +++ b/src/pages/faq.astro @@ -2,6 +2,19 @@ import Layout from "../layouts/Layout.astro"; import FAQList from "../components/faq/FAQList.tsx"; import "../styles/base.css"; + +// Shared class strings: the live system's type scale (DESIGN.md §3) in plain +// Tailwind, since Layout.astro does not load design-system.css. Mirrors +// about.astro so the two pages read as one system. +const container = "mx-auto max-w-[1280px] px-6"; +const headline = "text-[28px] font-extrabold leading-[1.15] text-ink min-[810px]:text-[38px]"; +const overline = "text-xs font-bold uppercase tracking-[0.14em] text-grove-deep"; +const body = "text-[19px] leading-relaxed min-[810px]:text-[21px]"; +const focusRingOnDark = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"; +const pillOnDark = `inline-flex min-h-[44px] items-center justify-center gap-2 rounded-pill px-5 py-3 text-[13px] font-bold transition-colors motion-reduce:transition-none ${focusRingOnDark}`; +const inverseButton = `${pillOnDark} bg-white text-nightfall hover:bg-first-light`; +const ghostOnDarkButton = `${pillOnDark} border border-white/45 text-white hover:border-white hover:bg-white hover:text-nightfall`; ---
    -
    -
    -
    - -

    +
    +
    +

    Frequently asked questions

    +

    + Straight answers about working for Palestinian liberation +

    +

    + What Tech for Palestine does, how projects get support, and where your skills fit in. Open + any question below; every answer comes from the people who run T4P. +

    +
    +
    + + +
    +
    +
    +

    + What people ask us +

    +
    +
    + +
    +
    +
    + + +
    +
    +
    +

    - Frequently Asked Questions -

    + Still have a question? Ask a person. +

    +

    + Write to the team and someone will get back to you. Members can also ask in the vetted + member chat, where the people building the projects answer directly. +

    +
    -
    +
    - - -
    - -
    diff --git a/src/pages/media.astro b/src/pages/media.astro index 05bdef92..e5263f30 100644 --- a/src/pages/media.astro +++ b/src/pages/media.astro @@ -1,58 +1,59 @@ --- import Layout from "../layouts/Layout.astro"; import { Icon } from "astro-icon/components"; -import PressReleaseDialog from "../components/media/PressReleaseDialog.astro"; +import PressRelease from "../components/media/PressRelease.astro"; import "../styles/base.css"; -// Entries with `popup: true` link to an in-page dialog (url is its #hash), -// rather than to an external site. +// Must match the section id in PressRelease.astro: the hero links to it, and +// /media#member-driven-press-release is the release's shareable URL. +const PRESS_RELEASE_HASH = "#member-driven-press-release"; +const PRESS_EMAIL = "media@techforpalestine.org"; + +// External coverage, newest first. `datetime` feeds