From 512f96fb2f93cacdf91c322fb17a796762f82f22 Mon Sep 17 00:00:00 2001 From: mohanadft Date: Wed, 30 Sep 2026 15:11:28 +0300 Subject: [PATCH 1/4] test(redirects): assert 301s for removed -new page routes --- src/utils/redirects.test.ts | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/utils/redirects.test.ts diff --git a/src/utils/redirects.test.ts b/src/utils/redirects.test.ts new file mode 100644 index 00000000..3081ff35 --- /dev/null +++ b/src/utils/redirects.test.ts @@ -0,0 +1,48 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const redirects = readFileSync("public/_redirects", "utf8") + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")) + .map((line) => line.split(/\s+/)); + +/** Removed `-new` page routes that must keep a 301. */ +const REMOVED_ROUTES = [ + "/about-new", + "/contact-new", + "/donate-new", + "/donate-2-new", + "/e4p-new", + "/e4p/pledge-new", + "/endorsements-new", + "/events-new", + "/faq-new", + "/get-involved-new", + "/help/hire-new", + "/home-new", + "/ideas-new", + "/incubator-new", + "/legal-new", + "/london-gathering-new", + "/media-new", + "/mentorship-new", + "/privacy-policy-new", + "/projects-new", + "/team-new", + "/terms-new", + "/tools-new", + "/volunteer-new", +]; + +describe("public/_redirects", () => { + for (const route of REMOVED_ROUTES) { + for (const from of [route, `${route}/`]) { + it(`301s ${from}`, () => { + const rule = redirects.find(([source]) => source === from); + expect(rule, `missing redirect for ${from}`).toBeDefined(); + expect(rule?.[2]).toBe("301"); + }); + } + } +}); From 9f082327cc6d18e2fca72c92ed8b5d6b2d8b4e41 Mon Sep 17 00:00:00 2001 From: mohanadft Date: Wed, 30 Sep 2026 15:11:46 +0300 Subject: [PATCH 2/4] refactor(pages): remove unreachable -new pages (301s kept) --- src/pages/about-new.astro | 366 -------- src/pages/contact-new.astro | 237 ------ src/pages/donate-2-new.astro | 254 ------ src/pages/donate-new.astro | 419 ---------- src/pages/e4p-new.astro | 511 ----------- src/pages/e4p/pledge-new.astro | 220 ----- src/pages/endorsements-new.astro | 408 --------- src/pages/events-new.astro | 119 --- src/pages/faq-new.astro | 183 ---- src/pages/get-involved-new.astro | 344 -------- src/pages/help/hire-new.astro | 324 ------- src/pages/home-new.astro | 97 --- src/pages/ideas-new.astro | 203 ----- src/pages/incubator-new.astro | 438 ---------- src/pages/legal-new.astro | 286 ------- src/pages/london-gathering-new.astro | 430 ---------- src/pages/media-new.astro | 613 -------------- src/pages/mentorship-new.astro | 370 -------- src/pages/privacy-policy-new.astro | 990 ---------------------- src/pages/projects-new.astro | 182 ---- src/pages/team-new.astro | 245 ------ src/pages/terms-new.astro | 1163 -------------------------- src/pages/tools-new.astro | 311 ------- src/pages/volunteer-new.astro | 418 --------- 24 files changed, 9131 deletions(-) delete mode 100644 src/pages/about-new.astro delete mode 100644 src/pages/contact-new.astro delete mode 100644 src/pages/donate-2-new.astro delete mode 100644 src/pages/donate-new.astro delete mode 100644 src/pages/e4p-new.astro delete mode 100644 src/pages/e4p/pledge-new.astro delete mode 100644 src/pages/endorsements-new.astro delete mode 100644 src/pages/events-new.astro delete mode 100644 src/pages/faq-new.astro delete mode 100644 src/pages/get-involved-new.astro delete mode 100644 src/pages/help/hire-new.astro delete mode 100644 src/pages/home-new.astro delete mode 100644 src/pages/ideas-new.astro delete mode 100644 src/pages/incubator-new.astro delete mode 100644 src/pages/legal-new.astro delete mode 100644 src/pages/london-gathering-new.astro delete mode 100644 src/pages/media-new.astro delete mode 100644 src/pages/mentorship-new.astro delete mode 100644 src/pages/privacy-policy-new.astro delete mode 100644 src/pages/projects-new.astro delete mode 100644 src/pages/team-new.astro delete mode 100644 src/pages/terms-new.astro delete mode 100644 src/pages/tools-new.astro delete mode 100644 src/pages/volunteer-new.astro diff --git a/src/pages/about-new.astro b/src/pages/about-new.astro deleted file mode 100644 index 2b6424b9..00000000 --- a/src/pages/about-new.astro +++ /dev/null @@ -1,366 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -import Button from "../components/ui/Button.astro"; -import SignUpForm from "../structures/SignUpForm.astro"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -const founderSchema = { - "@context": "https://schema.org", - "@type": "Person", - name: "Paul Biggar", - jobTitle: "Founder", - image: "https://techforpalestine.org/paul-biggar.webp", - worksFor: { - "@type": "NonprofitOrganization", - "@id": "https://techforpalestine.org/#organization", - }, - sameAs: ["https://blog.paulbiggar.com/", "https://circleci.com", "https://darklang.com"], -}; ---- - - - - diff --git a/src/pages/contact-new.astro b/src/pages/contact-new.astro deleted file mode 100644 index 7a110495..00000000 --- a/src/pages/contact-new.astro +++ /dev/null @@ -1,237 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; ---- - - - -
-
-
-
-

Tech for Palestine

-

Contact Us

-

- Have a question, media inquiry, or want to request an endorsement? We'd love to hear - from you. Tech for Palestine is a volunteer-powered 501(c)(3) nonprofit, so most inboxes - below are staffed part-time — we aim to respond to general and media inquiries within - 5-7 business days, and endorsement requests follow the same timeline. If your question - is about how to get involved, donate, or join the Incubator, check our FAQ first — it covers the most common questions we receive. -

-
- -
-
-
- -
-

General Questions

-

- For general inquiries about Tech for Palestine, our Incubator projects, membership, - volunteering, or how to get involved. This is also the right inbox for accessibility - requests or if you're not sure which team to reach. -

- - contact@techforpalestine.org - -
- -
-
- -
-

Media & Press

-

- Journalists and media professionals can find our press kit, past coverage, and - interview contact information on our media page. For time-sensitive press inquiries, - mention your deadline in the subject line. -

- - Visit our Media page → - -
- -
-
- -
-

Endorsements

-

- Seeking a T4P endorsement for your campaign or organization? We review requests within - 5-7 days — learn what we look for and submit a request. -

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

Registered Address

-

- Tech for Palestine is a registered 501(c)(3) nonprofit (EIN 99-3441367). Our full - financial and legal details are on our Legal & Financial Notice page. -

-

- 548 Market St #266950, San Francisco, CA 94104-5401, US -

-
-
- -

- Prefer social media? We're active on Instagram, X/Twitter, and LinkedIn, though email remains the fastest way to reach the right team. -

-
-
-
-
- - - - diff --git a/src/pages/donate-2-new.astro b/src/pages/donate-2-new.astro deleted file mode 100644 index 176cff40..00000000 --- a/src/pages/donate-2-new.astro +++ /dev/null @@ -1,254 +0,0 @@ ---- -// Fallback donation form for donate-new.astro (linked from there as "this form" -// when the primary embed doesn't load) — not an accidental duplicate page. -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; ---- - - - - -
- -
-
- - - - -
-
- - -
-
-
- -
-
-

Make a donation

-
-
-
-
- - -
-
- - 501(c)(3) nonprofit -
- - Tax-deductible in the U.S. - - EIN: 99-3441367 -
-
-
- - -
- -
-

Why your donation matters

-
    -
  • - - Accelerates projects already making impact -
  • -
  • - - Funds critical skills: engineering, design, marketing -
  • -
  • - - Helps launch new initiatives quickly and effectively -
  • -
-
- - -
-

Alternative ways to give

-

- Donate through a Donor-Advised Fund (DAF), family trust, or other structured giving - methods. -

-

- Email donations@techforpalestine.org and we'll assist you. -

-
-
-
-
-
-
-
- - - - - - diff --git a/src/pages/donate-new.astro b/src/pages/donate-new.astro deleted file mode 100644 index 2c0fa219..00000000 --- a/src/pages/donate-new.astro +++ /dev/null @@ -1,419 +0,0 @@ ---- -// This is the primary donate page (see the in-page fallback link to /donate-2). -// donate-2-new.astro is the intentional fallback form, not an accidental duplicate. -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; -const country = Astro.request.headers.get("cf-ipcountry") || "UNKNOWN"; -const isUK = country === "GB"; -// Server-rendered fallback variant (no A/B randomization) so the donation -// form is present in the initial HTML for non-JS clients and crawlers — -// the client-side A/B test below still overrides this for real visitors. -const defaultFormId = isUK ? "form-validaid-link" : "form-qgiv-link"; ---- - - - - -
- -
-
- -
-
- - -
-
-
- -
-
-

How do I donate to Tech for Palestine?

- - - - - - - - - -

- If this page isn't loading, please use this form. -

- - -
-
- - 501(c)(3) nonprofit -
- - Tax-deductible in the U.S. - - EIN: 99-3441367 -
-
-
- - -
- -
-

Why your donation matters

-
    -
  • - - Accelerates projects already making impact -
  • -
  • - - Funds critical skills: engineering, design, marketing -
  • -
  • - - Helps launch new initiatives quickly and effectively -
  • -
-
- - -
-

Other ways to give

-
    -
  • -

    Stock & Cryptocurrency

    -

    - To make a gift via stock or cryptocurrency, visit our form here. -

    -
  • -
  • -

    Corporate Match

    -

    - If your employer uses Benevity to match your contribution, find us on the Benevity Causes Portal. -

    -
  • -
  • -

    Major Gifts

    -

    - For more information about making a major gift or for other questions about - donations, please contact donations@techforpalestine.org. -

    -
  • -
-
-
-
-
-
-
-
- - - - - - diff --git a/src/pages/e4p-new.astro b/src/pages/e4p-new.astro deleted file mode 100644 index 63054506..00000000 --- a/src/pages/e4p-new.astro +++ /dev/null @@ -1,511 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -import Button from "../components/ui/Button.astro"; -import SignUpForm from "../structures/SignUpForm.astro"; -import Map from "../components/Map"; -import { Icon } from "astro-icon/components"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -const benefits = [ - { - name: "Community", - description: "A supportive community of peers who share the same values", - icon: "mdi:account-group", - }, - { - name: "New Business & Networking", - description: "Opportunities to generate new business and build your professional network", - icon: "mdi:handshake", - }, - { - name: "In-Person Meetups", - description: "Invitations to in-person meetups around the world with local E4P members", - icon: "mdi:calendar-star", - }, - { - name: "Funding Introductions", - description: - "Introductions to values-aligned VCs, angel investors and other sources of funding", - icon: "mdi:cash-multiple", - }, - { - name: "Hiring Support", - description: - "Support for enriching your hiring pipeline with Palestinian and pro-Palestine talent", - icon: "mdi:account-search", - }, - { - name: "Boycott Alternatives", - description: "Lists of companies and tech products to boycott and alternatives", - icon: "mdi:swap-horizontal-circle", - }, - { - name: "PR Assistance", - description: "PR assistance by amplifying your posts and showcasing your business", - icon: "mdi:bullhorn", - }, - { - name: "Mentorship & Coaching", - description: "Mentorship and personal coaching by other E4P members", - icon: "mdi:school", - }, - { - name: "Legal Assistance", - description: "Pro bono or discounted legal assistance in case of any legal issues", - icon: "mdi:gavel", - }, -]; - -const testimonials = [ - { - name: "Chris Musei-Sequeira", - title: "Founder, CJSC — USA", - image: "/images/chris-linkedin.jpg", - linkedin: "https://www.linkedin.com/in/cjsequeira/", - quote: - "I'm so glad to be a member of Entrepreneurs for Palestine, where I know everyone stands for basic human decency. We are so many different people from so many parts of the world, but we all agree on this most fundamental value.", - }, - { - name: "Sami Abu Heiba", - title: "Founder, GIL — Palestine", - image: "/images/sami-linkedin.jpg", - linkedin: "https://www.linkedin.com/in/samiabuheiba/", - quote: - "Joining E4P has been an incredibly rich and inspiring experience. It connected me with a diverse community of purpose-driven entrepreneurs and gave me access to valuable tools, insights, and opportunities for growth. Being part of this network reminds me daily of the power of collaboration and shared impact.", - }, - { - name: "Ariana Alexander-Sefre", - title: "Founder, SPOKE — UK", - image: "/images/ariana-linkedin.jpg", - linkedin: "https://www.linkedin.com/in/arianasefre/", - quote: - "I joined E4P because I started to ask myself who I can truly trust. I don't want to work with, or take investment from, people who aren't on the right side of history. From now on, I believe it's essential to align with people and brands who will stand up to injustice when they see it. That's why E4P is a great community; because everyone here shares those values.", - }, -]; - -const founders = [ - { - name: "Aline Sara", - title: "Co-Founder, Entrepreneurs for Palestine", - image: "/images/founders/aline-sara.png", - linkedin: "https://www.linkedin.com/in/alinesara/", - }, - { - name: "Iva Gumnishka", - title: "Co-Founder", - image: "/images/founders/iva-gumnishka.png", - linkedin: "https://www.linkedin.com/in/ivagumnishka/", - }, - { - name: "Paul Biggar", - title: "Founder, Tech for Palestine", - image: "/images/founders/paul-biggar.png", - linkedin: "https://www.linkedin.com/in/paulbiggar/", - }, - { - name: "Miguel de Icaza", - title: "CEO, Xibbon", - image: "/images/founders/miguel-de-icaza.png", - linkedin: "https://www.linkedin.com/in/migueld1/", - }, - { - name: "Fadi Ghandour", - title: "Executive Chairman, Wamda Group", - image: "/images/founders/fadi-ghandour.png", - linkedin: "https://www.linkedin.com/in/fadi-ghandour-52353b/", - }, - { - name: "Amir Nathoo", - title: "Co-Founder and CEO, Outschool", - image: "/images/founders/amir-nathoo.png", - linkedin: "https://www.linkedin.com/in/amirnathoo/", - }, - { - name: "Stephanie Nadi Olson", - title: "Founder and Executive Chair of the Board, We Are Rosie", - image: "/images/founders/stephanie-nadi-olson.png", - linkedin: "https://www.linkedin.com/in/snadi/", - }, - { - name: "Reem Qawasmi", - title: "Venture Partner, Ibtikar Fund", - image: "/images/founders/reem-qawasmi.png", - linkedin: "https://www.linkedin.com/in/reemqawasmi/", - }, - { - name: "Essma Bengabsia", - title: "Founder, Bengabsia Consulting", - image: "/images/founders/essma-bengabsia.png", - linkedin: "https://www.linkedin.com/in/essmabengabsia/", - }, - { - name: "Arshan Ahmad", - title: "Co-Founder, Friday Ventures", - image: "/images/founders/arshan-ahmad.png", - linkedin: "https://www.linkedin.com/in/arshanahmad/", - }, -]; -const peopleSchema = [...testimonials, ...founders].map((person) => ({ - "@context": "https://schema.org", - "@type": "Person", - name: person.name, - jobTitle: person.title, - sameAs: [person.linkedin], -})); ---- - - - - { - peopleSchema.map((schema) => ( - diff --git a/src/pages/e4p/pledge-new.astro b/src/pages/e4p/pledge-new.astro deleted file mode 100644 index ad8da5fa..00000000 --- a/src/pages/e4p/pledge-new.astro +++ /dev/null @@ -1,220 +0,0 @@ ---- -import { getEnv } from "../../utils/getEnv"; -import { fetchE4PSignatories } from "../../store/notionClient"; -import HomeLayout from "../../layouts/HomeLayout.astro"; -import HomeNavbar from "../../components/home/HomeNavbar.astro"; -import Button from "../../components/ui/Button.astro"; -import SignatoriesNew from "../../components/SignatoriesNew.tsx"; -import PledgeForm from "../../components/PledgeForm.tsx"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; -const signatories = await fetchE4PSignatories(Astro.locals).catch(() => []); - -const breadcrumbSchema = { - "@context": "https://schema.org", - "@type": "BreadcrumbList", - itemListElement: [ - { - "@type": "ListItem", - position: 1, - name: "Entrepreneurs for Palestine", - item: "https://techforpalestine.org/e4p-new", - }, - { - "@type": "ListItem", - position: 2, - name: "E4P Pledge", - item: "https://techforpalestine.org/e4p/pledge-new", - }, - ], -}; ---- - - - - diff --git a/src/pages/endorsements-new.astro b/src/pages/endorsements-new.astro deleted file mode 100644 index 7fd92dba..00000000 --- a/src/pages/endorsements-new.astro +++ /dev/null @@ -1,408 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -import EndorsementForm from "../components/EndorsementForm.tsx"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -const endorsementFaqs = [ - { - question: "What does a T4P endorsement mean?", - answer: - "A T4P endorsement is a public statement of support for a specific campaign or initiative. It signals to our community of thousands of tech workers that the campaign aligns with our values. It does not imply financial support or ongoing partnership. Note that we don't fully vet every endorsement — our review is based on available information and community knowledge, not exhaustive due diligence.", - }, - { - question: "How long does the review process take?", - answer: - "We aim to review all requests within 5-7 days. We'll reach out if we need additional information.", - }, - { - question: "Can T4P projects apply for endorsements?", - answer: - 'Yes. If you\'re already a T4P incubator project and are launching a specific campaign, you can request an endorsement. Check the "T4P project" box in the form and our review will reflect that context.', - }, - { - question: "What happens if our request is declined?", - answer: - "We'll let you know and, where possible, share the reason. Declinations are not permanent. If your campaign evolves or new information is available, you're welcome to reapply.", - }, -]; - -const faqSchema = { - "@context": "https://schema.org", - "@type": "FAQPage", - mainEntity: endorsementFaqs.map((faq) => ({ - "@type": "Question", - name: faq.question, - acceptedAnswer: { - "@type": "Answer", - text: faq.answer, - }, - })), -}; ---- - - - - diff --git a/src/pages/events-new.astro b/src/pages/events-new.astro deleted file mode 100644 index f9f0f6cf..00000000 --- a/src/pages/events-new.astro +++ /dev/null @@ -1,119 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import { fetchEvents, type EventItem } from "../store/eventsClient"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -import EventsNewComponent from "../components/events/EventsNew"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -const events: EventItem[] = await fetchEvents(Astro.locals).catch(() => []); - -// Pick and describe the next event by its instant, not by the organizer-zone -// `date` string — that one is a day off for part of the world. -// `fetchEvents` doesn't return the feed in date order, so pick the soonest -// upcoming event explicitly — and compare instants rather than the -// organizer-zone `date` string, which is a day off for part of the world. -const now = Date.now(); -const nextEvent = events - .filter((e) => e.dateUtcIso && new Date(e.dateUtcIso).getTime() >= now) - .sort((a, b) => new Date(a.dateUtcIso!).getTime() - new Date(b.dateUtcIso!).getTime())[0]; - -const eventSchema = nextEvent && { - "@context": "https://schema.org", - "@type": "Event", - name: nextEvent.title, - startDate: nextEvent.dateUtcIso, - eventStatus: "https://schema.org/EventScheduled", - image: [nextEvent.image || "https://techforpalestine.org/t4p-social-logo.png"], - location: nextEvent.location - ? { "@type": "Place", name: nextEvent.location } - : { - "@type": "VirtualLocation", - url: nextEvent.link || "https://techforpalestine.org/events-new", - }, - description: nextEvent.description, - organizer: { - "@type": "NonprofitOrganization", - "@id": "https://techforpalestine.org/#organization", - }, -}; ---- - - - - {eventSchema && diff --git a/src/pages/faq-new.astro b/src/pages/faq-new.astro deleted file mode 100644 index 3c817481..00000000 --- a/src/pages/faq-new.astro +++ /dev/null @@ -1,183 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import { fetchNotionFAQ } from "../store/notionClient"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -import FAQSection from "../components/FAQSection.tsx"; -import SignUpForm from "../structures/SignUpForm.astro"; -import type { RichTextSegment } from "../types/richText"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -interface FAQItem { - id: string; - question: string; - answer: string | RichTextSegment[]; -} - -const fallbackFaqs: FAQItem[] = [ - { - id: "fallback-mission", - question: "What does Tech for Palestine do?", - answer: - "Tech for Palestine is a 501(c)(3) nonprofit that incubates and scales tech and advocacy projects for Palestinian liberation.", - }, - { - id: "fallback-get-involved", - question: "How can I get involved with Tech for Palestine?", - answer: - "You can volunteer your skills, mentor an Incubator project, or join the T4P Incubator to build your own project. Visit our Get Involved page for the full list of ways to contribute.", - }, - { - id: "fallback-incubator", - question: "What is the T4P Incubator?", - answer: - "The T4P Incubator helps advocates build, grow, and scale technical projects that support Palestinian freedom, providing mentorship, community, and resources to project leaders.", - }, - { - id: "fallback-donate", - question: "How can I donate to Tech for Palestine?", - answer: - "You can donate directly on our Donate page. Tech for Palestine is a registered 501(c)(3) nonprofit (EIN 99-3441367), so US donations are generally tax-deductible.", - }, - { - id: "fallback-membership", - question: "How much are Tech for Palestine membership dues?", - answer: - "We suggest monthly dues equal to one hour's salary, though you can contribute any amount. Fee waivers are available — see the Membership page for eligibility.", - }, - { - id: "fallback-mentor", - question: "How do I become a mentor?", - answer: - "Submit a Mentor Application. If your profile fits, you'll be invited to interview with the Tech for Palestine team, then matched with an Incubator project that needs your expertise.", - }, -]; - -const notionFaqs: FAQItem[] = await fetchNotionFAQ(false, Astro.locals).catch(() => []); -const faqs: FAQItem[] = notionFaqs.length > 0 ? notionFaqs : fallbackFaqs; - -const answerToPlainText = (answer: FAQItem["answer"]): string => - typeof answer === "string" ? answer : answer.map((segment) => segment.plain_text).join(""); - -const faqSchema = { - "@context": "https://schema.org", - "@type": "FAQPage", - mainEntity: faqs.map((faq) => ({ - "@type": "Question", - name: faq.question, - acceptedAnswer: { - "@type": "Answer", - text: answerToPlainText(faq.answer), - }, - })), -}; ---- - - - - diff --git a/src/pages/get-involved-new.astro b/src/pages/get-involved-new.astro deleted file mode 100644 index fa1fc4bc..00000000 --- a/src/pages/get-involved-new.astro +++ /dev/null @@ -1,344 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -import Button from "../components/ui/Button.astro"; -import SignUpForm from "../structures/SignUpForm.astro"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -const webPageSchema = { - "@context": "https://schema.org", - "@type": "WebPage", - "@id": "https://techforpalestine.org/get-involved-new", - url: "https://techforpalestine.org/get-involved-new", - name: "Get Involved with Tech for Palestine | Volunteer & More", - description: - "Join our mission-driven community building tools and projects for Palestinian liberation. Volunteer, mentor, contribute your skills, or help lead a project.", - isPartOf: { - "@type": "NonprofitOrganization", - "@id": "https://techforpalestine.org/#organization", - }, -}; ---- - - - - diff --git a/src/pages/help/hire-new.astro b/src/pages/help/hire-new.astro deleted file mode 100644 index 87a0a1c7..00000000 --- a/src/pages/help/hire-new.astro +++ /dev/null @@ -1,324 +0,0 @@ ---- -import { getEnv } from "../../utils/getEnv"; -import HomeLayout from "../../layouts/HomeLayout.astro"; -import HomeNavbar from "../../components/home/HomeNavbar.astro"; -import SignUpForm from "../../structures/SignUpForm.astro"; -import { Icon } from "astro-icon/components"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -const contacts = [ - { - name: "Instagram", - url: "https://www.instagram.com/techforpalestine", - icon: "mdi:instagram", - }, - { - name: "Twitter", - url: "https://twitter.com/tech4palestine", - icon: "mdi:twitter", - }, - { - name: "GitHub", - url: "https://github.com/TechForPalestine/", - icon: "mdi:github", - }, - { - name: "Discord", - url: "https://techforpalestine.org/discord-invite", - icon: "ic:baseline-discord", - }, -]; - -function generateLabel(name: string): string { - const map: Record = { - Upwork: "Hire on Upwork", - Dribble: "View on Dribble", - TAP: "View TAP Program", - "Manara.tech": "Hire via Manara", - "Rising from Ashes": "Hire via Rising Bees", - Youmna: "Meet Your Assistant", - }; - const key = Object.keys(map).find((k) => name.includes(k)); - return key ? map[key] : `Explore ${name}`; -} - -const resourceSections = [ - { - name: "Recruitment Platforms", - overline: "Hire from Palestine", - bg: "bg-sand" as const, - cardBg: "bg-page" as const, - resources: [ - { - name: "Apricot", - url: "https://apricotinternational.org/", - logo: "/apricot.jpg", - description: `Apricot connects engineering talent in Palestine with international teams.`, - }, - { - name: "MENA Alliances", - url: "https://menaalliances.com/jobsforpalestine/", - logo: "/mena-alliances-logo.webp", - description: `MENA Alliance sources tech talent from Palestine for global companies.`, - }, - { - name: "Gaza Sky Geeks", - url: "https://gazaskygeeks.com/", - logo: "/gaza-sky-geeks-logo.png", - description: `A leading tech hub in Gaza and the West Bank, fostering future tech leaders since 2011.`, - }, - { - name: "Manara.tech", - url: "https://manara.tech/tech-jobs-for-palestine", - logo: "/manara.png", - description: `Connecting top tech talent in the MENA region to global jobs.`, - }, - { - name: "Rising from Ashes", - url: "https://bees.to/hire-ps/", - logo: "/bees-logo-horizontal.webp", - description: `Rising Bees connects Palestinians with remote IT jobs.`, - }, - { - name: "TAP", - url: "https://www.tapcareers.io/", - logo: "/tap-logo.svg", - description: `TAP offers career acceleration in Palestine and Jordan with remote-ready talent.`, - }, - { - name: "Techtative", - url: "https://techtative.io/", - logo: "/techtative-logo.png", - description: `Premier EOR in Palestine, helping global orgs tap into local expertise.`, - }, - { - name: "Olives and Heather", - url: "https://www.olivesandheather.com/", - logo: "/olives-and-heather.png", - description: `Empowering Palestinian marketers through global marketing excellence.`, - }, - { - name: "Gaza Talents", - url: "https://gazatalents.com/en", - logo: "/gazatalents.png", - description: `Quick access to skilled professionals from Gaza.`, - }, - { - name: "Youmna", - url: "https://www.withyoumna.com/", - logo: "/youmna.png", - description: `Remote Executive Assistants from Palestine — your "right hand".`, - }, - { - name: "BrightGaza", - url: "https://brightgaza.com/", - logo: "/brightgaza-logo.svg", - description: `BrightGaza connects talented freelancers in Gaza with real remote work opportunities around the world, helping them grow professionally, earn income, and build sustainable careers.`, - }, - ], - }, - { - name: "Freelance Networks", - overline: "Freelancers", - bg: "bg-page" as const, - cardBg: "bg-sand" as const, - resources: [ - { - name: "Dribble", - url: "https://dribbble.com/freelance-ux-designers-for-hire-gaza-ps", - logo: "/dribbble-logo.png", - description: `UX designers from Gaza offering freelance services.`, - }, - { - name: "Upwork", - url: "https://www.upwork.com/hire/ps/", - logo: "/upwork.png", - description: `Find Palestinian freelancers for your projects on Upwork.`, - }, - ], - }, -]; -const hireItemListSchema = { - "@context": "https://schema.org", - "@type": "ItemList", - itemListElement: resourceSections - .flatMap((section) => section.resources) - .map((res, index) => ({ - "@type": "ListItem", - position: index + 1, - name: res.name, - url: res.url, - })), -}; - -const breadcrumbSchema = { - "@context": "https://schema.org", - "@type": "BreadcrumbList", - itemListElement: [ - { - "@type": "ListItem", - position: 1, - name: "Tech for Palestine", - item: "https://techforpalestine.org/", - }, - { - "@type": "ListItem", - position: 2, - name: "Jobs for Palestinians", - item: "https://techforpalestine.org/help/hire-new", - }, - ], -}; ---- - - - - diff --git a/src/pages/home-new.astro b/src/pages/home-new.astro deleted file mode 100644 index 091ed6e1..00000000 --- a/src/pages/home-new.astro +++ /dev/null @@ -1,97 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HeroSection from "../components/home/HeroSection.astro"; -import ManifestoSection from "../components/home/ManifestoSection.astro"; -import WhySection from "../components/home/WhySection.astro"; -import PortfolioSection from "../components/home/PortfolioSection.astro"; -import SupportSection from "../components/home/SupportSection.astro"; -import TestimonialsSection from "../components/home/TestimonialsSection.astro"; -import OpeningsSection from "../components/home/OpeningsSection.astro"; -import PressSection from "../components/home/PressSection.astro"; -import CtaSection from "../components/home/CtaSection.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -import SignUpFormHomeNew from "../structures/SignUpFormHomeNew.astro"; -import CommunityCallBanner from "../components/CommunityCallBanner.astro"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -let openRoles: Array<{ - id: string; - title: string; - description: string | null; - skillCategories: string[]; - timeCommitment: string | null; - type: "team" | "project"; - team: { name: string } | null; - project: { id: string; name: string } | null; -}> = []; - -try { - // This blocks SSR on every request (open-roles has averaged ~0.7-0.8s - // round-trip in production). The openings section is below the fold and - // already renders fine with zero roles, so cap the wait tightly rather - // than at 3s — a slow/hanging upstream should cost this page far less - // than that off its TTFB/LCP. - const res = await fetch("https://hub.techforpalestine.org/api/public/open-roles", { - signal: AbortSignal.timeout(1200), - }); - if (res.ok) { - openRoles = await res.json(); - } -} catch {} - -const openRolesSchema = openRoles.length > 0 && { - "@context": "https://schema.org", - "@type": "ItemList", - itemListElement: openRoles.map((role, i) => ({ - "@type": "ListItem", - position: i + 1, - name: role.title, - description: role.description || undefined, - })), -}; ---- - - - { - openRolesSchema && ( - diff --git a/src/pages/incubator-new.astro b/src/pages/incubator-new.astro deleted file mode 100644 index 24763873..00000000 --- a/src/pages/incubator-new.astro +++ /dev/null @@ -1,438 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import { Icon } from "astro-icon/components"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -import Button from "../components/ui/Button.astro"; -import SignUpForm from "../structures/SignUpForm.astro"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; ---- - - - -
- -
-
-
-

T4P Incubator

-

- Turn your idea for Palestine into something that scales -

-

- The Tech for Palestine incubator supports initiatives for Palestine, by getting them off - the ground and helping them scale up. Our goal is to create a broad and diverse set of - initiatives for Palestine that are able to educate the entire world about Palestine, and - to address Hasbara and Zionism wherever it takes hold. -

-

- We take on a limited number of projects to support with our expertise and resources. -

-
- - -
-
-
-
- - -
-
-
-

Why we exist

-

Our theory of change

-

- Thousands of high-impact, pro-Palestinian advocacy projects are needed to counter the - massive flood of anti-Palestinian propaganda, especially in the west. We started a - project incubator because we saw highly motivated people with great ideas struggling to - bring them to life. Our purpose is to provide the support, expertise, and connections - that turn good ideas into projects with real reach. -

-
-
-
- - -
-
-
-

What you get

-

What the incubator provides

-
    -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
-
-
-
- - -
-
-
-

Who it's for

-

Who is the T4P Incubator for?

-

- Any early-stage project that directly or indirectly advocates for Palestine, where we - believe you can have impact and scale. Our projects often work on scaling boycotts, protest tech, tech industry divestment, media bias, reaching new audiences, Silicon Valley partnerships, AI bias, and exposing hasbara. -

-

- Whether you're just getting started or have an experienced team looking for connections - and strategic advice, the incubator is built to meet you where you are. -

-
-
-
- - -
-
-
-

How we help

-

How the Incubator can help you

-
-

- Many projects for Palestine are started by advocates who have a good idea, but don't - yet know how to execute on it. We help support you on your journey, using our - knowledge of building scalable projects, working with volunteers, the pro-Palestine - ecosystem, and using our platform and network to support your work. -

-

- More experienced teams may find our connections and platform valuable, as well as our - strategic advice on how to tailor what you're building for the right niche to start, - and our knowledge of what exists already in the ecosystem, and how to partner with - them. -

-
-
-
-
- - -
-
- Palestinian children smiling together -
-
- - -
-
-
-

Our expectations

-

What we expect

-
    -
  • - - Focus on impact, such as reaching 10 - million users -
  • -
  • - - Commitment to keep working on your project - and driving it forward -
  • -
  • - - Communication through office hours (weekly - for the first 6 weeks, monthly after) and regular updates -
  • -
  • - - Flexible participation: if the incubator - no longer fits your needs, membership may be removed without hard feelings -
  • -
-
-

What we don't require or expect

-
    -
  • -

    No Daily Meetings

    -

    - We don't require daily or recurring meetings. Engage through office hours on your - own schedule. -

    -
  • -
  • -

    No T4P Ownership

    -

    - Your project remains yours. We don't take ownership of your work or require T4P - branding. -

    -
  • -
  • -

    Flexible Tools

    -

    - Use whatever tools work best for your project. We don't require specific platforms - or workflows. -

    -
  • -
-
-
-
-
- - -
-
-
-

The process

-

How do I apply to the T4P Incubator?

-
    -
  1. - 1 - Application form: fill out the - application (takes 10 to 30 minutes) -
  2. -
  3. - 2 - Review: we review your application and - invite selected projects for an interview. We may reach out for more information. -
  4. -
  5. - 3 - Interview: a 30-minute call focusing on - the impact you intend to have and how you plan to get there. No slides needed. - Interviews are very direct and aim to push you on your understanding of the problem - and the impact you might have. -
  6. -
  7. - 4 - Decision: we aim to notify all applicants - within 10 days of application. Interviews and decisions happen twice weekly. In some - cases we may ask more questions after the interview. -
  8. -
  9. - 5 - Onboarding: if admitted, you receive next - steps and access to all Tech for Palestine resources. -
  10. -
-
-
-
- - -
-
-
-

Ready to get started?

-
-
-

Have an idea and ready to build?

- -
-
-

- Want to lead a project but need inspiration? -

- -
-
-

- Want to contribute your skills to existing projects? -

- -
-
-

- If you have already applied, we'll reach out to you soon. -

-
-
-
- - -
-
- -
-
-
-
- - - - diff --git a/src/pages/legal-new.astro b/src/pages/legal-new.astro deleted file mode 100644 index 0d2629fa..00000000 --- a/src/pages/legal-new.astro +++ /dev/null @@ -1,286 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -const pageTitle = "Financial & Legal Notice"; -const pageDescription = - "Tech for Palestine's legal structure, financial transparency, and compliance information."; -const webPageSchema = { - "@context": "https://schema.org", - "@type": "WebPage", - "@id": "https://techforpalestine.org/legal-new", - url: "https://techforpalestine.org/legal-new", - name: pageTitle, - description: pageDescription, - dateModified: "2025-08-05", - publisher: { - "@type": "NonprofitOrganization", - "@id": "https://techforpalestine.org/#organization", - }, -}; ---- - - - - diff --git a/src/pages/london-gathering-new.astro b/src/pages/london-gathering-new.astro deleted file mode 100644 index 4613a874..00000000 --- a/src/pages/london-gathering-new.astro +++ /dev/null @@ -1,430 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -import Button from "../components/ui/Button.astro"; -import SignUpForm from "../structures/SignUpForm.astro"; -import { Icon } from "astro-icon/components"; -import { fetchNotionAgenda } from "../store/notionClient"; -import SpeakersSection from "../components/london-gathering/SpeakersSection"; -import AgendaSection from "../components/london-gathering/AgendaSection"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -let agendaData: any = { agendaItems: [], speakers: [] }; -try { - agendaData = await fetchNotionAgenda(Astro.locals); -} catch (error) { - console.error("Error fetching agenda data:", error); -} - -const themes = [ - { name: "Scaling Up Boycotts", icon: "mdi:cart-off" }, - { name: "Media Bias and Hasbara", icon: "mdi:bullhorn" }, - { name: "Protests and Activism", icon: "mdi:account-group" }, - { name: "Palestinian Tech Sector", icon: "mdi:domain" }, - { name: "MedTech for Palestine", icon: "mdi:medical-bag" }, - { name: "Hiring Palestinians", icon: "mdi:briefcase-search" }, - { name: "Social Media Censorship", icon: "mdi:cancel" }, - { name: "Investors for Palestine", icon: "mdi:trending-up" }, -]; - -const stats = [ - { - number: "70+", - label: "Incubator", - description: "Impactful advocacy projects", - href: "/projects-new", - }, - { number: "9K+", label: "Community", description: "Tech workers worldwide", href: "/about-new" }, - { - number: "1K+", - label: "Volunteers", - description: "Donate skills to projects you care about", - href: "/volunteer-new", - }, - { - number: "250+", - label: "Entrepreneurs", - description: "Pro-Palestine entrepreneurs", - href: "/e4p-new", - }, -]; - -const eventSchema = { - "@context": "https://schema.org", - "@type": "Event", - name: "Tech for Palestine Gathering — London", - startDate: "2025-11-08T09:00:00Z", - endDate: "2025-11-08T16:00:00Z", - eventStatus: "https://schema.org/EventCompleted", - eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode", - image: ["https://techforpalestine.org/t4p-social-logo.png"], - location: { - "@type": "Place", - name: "Palestine House", - address: { - "@type": "PostalAddress", - streetAddress: "113 High Holborn", - addressLocality: "London", - postalCode: "WC1V 6JQ", - addressCountry: "GB", - }, - }, - organizer: { - "@type": "NonprofitOrganization", - "@id": "https://techforpalestine.org/#organization", - }, - description: - "A full day event for T4P community members with workshops and networking opportunities.", -}; ---- - - - - diff --git a/src/pages/media-new.astro b/src/pages/media-new.astro deleted file mode 100644 index 63306541..00000000 --- a/src/pages/media-new.astro +++ /dev/null @@ -1,613 +0,0 @@ ---- -import { getEnv } from "../utils/getEnv"; -import HomeLayout from "../layouts/HomeLayout.astro"; -import HomeNavbar from "../components/home/HomeNavbar.astro"; -import Button from "../components/ui/Button.astro"; - -const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; - -const logos = [ - { - src: "https://upload.wikimedia.org/wikipedia/commons/7/75/The_Guardian_2018.svg", - alt: "The Guardian", - }, - { - src: "https://e7.pngegg.com/pngimages/669/74/png-clipart-tc-techcrunch-logo-illustration-techcrunch-logo-icons-logos-emojis-iconic-brands.png", - alt: "TechCrunch", - }, - { - src: "https://upload.wikimedia.org/wikipedia/en/thumb/8/8f/Al_Jazeera_Media_Network_Logo.svg/1280px-Al_Jazeera_Media_Network_Logo.svg.png", - alt: "Al Jazeera", - }, - { - src: "https://upload.wikimedia.org/wikipedia/commons/d/d6/Inc._%28business_magazine%29_logo.svg", - alt: "Inc.", - }, - { - src: "https://ssir.org/images/resources/SSIR_Logo.RGB_Black_984x234.png", - alt: "Stanford Social Innovation Review", - }, - { - src: "https://upload.wikimedia.org/wikipedia/commons/4/4f/TRT_World_logosu.png", - alt: "TRT World", - }, -]; - -const MONTHS: Record = { - Jan: "01", - Feb: "02", - Mar: "03", - Apr: "04", - May: "05", - Jun: "06", - Jul: "07", - Aug: "08", - Sep: "09", - Oct: "10", - Nov: "11", - Dec: "12", -}; - -// Converts a display date like "Jan 2024" or "2024" into an ISO 8601 date -// (year-month or year precision — we only ever know month/year for these). -const toIsoDate = (displayDate: string): string => { - const [month, year] = displayDate.split(" "); - if (year && MONTHS[month]) return `${year}-${MONTHS[month]}`; - return displayDate; -}; - -const leadArticle = { - publication: "Stanford Social Innovation Review", - date: "2024", - headline: "Tech for Palestine Business Incubator Supports Entrepreneurs Protecting Human Rights", - standfirst: - "A Stanford Social Innovation Review deep-dive into how Tech for Palestine's incubator model is backing founders building tools for human rights and Palestinian liberation.", - url: "https://ssir.org/articles/entry/tech-against-genocide", -}; - -const articles = [ - { - publication: "TechCrunch", - date: "Jan 2024", - headline: "Tech for Palestine launches to provide tools to help support Palestinians", - description: - "Highlighting the formation of a global coalition of technologists for Palestinian advocacy.", - url: "https://techcrunch.com/2024/01/02/tech-for-palestine-launches-to-provide-tools-and-projects-to-help-advocate-for-palestinians/", - }, - { - publication: "Al Jazeera", - date: "Jan 2026", - headline: "What's UpScrolled, the app gaining popularity after TikTok's US takeover?", - url: "https://www.aljazeera.com/news/2026/1/29/whats-upscrolled-the-app-gaining-popularity-after-tiktoks-us-takeover", - }, - { - publication: "The Guardian", - date: "Nov 2025", - headline: "Fundraisers warn of 'catastrophic' drop in donations to Gaza since ceasefire", - url: "https://www.theguardian.com/global-development/2025/nov/21/fundraisers-warn-catastrophic-drop-donations-aid-palestinians-gaza-israel-ceasefire", - }, - { - publication: "TRT World", - date: "2024", - headline: - "Tech for Palestine initiative supports advocacy and accountability amid Israel's use of AI", - url: "https://www.trtworld.com/video/18268907", - }, - { - publication: "Palestine Chronicle", - date: "Jan 2024", - headline: "'Tech for Palestine' Aims to Boost Global Palestinian Advocacy", - description: "Focus on how the initiative empowers tech workers to speak up for Palestine.", - url: "https://www.palestinechronicle.com/tech-for-palestine-aims-to-boost-global-palestinian-advocacy/", - }, - { - publication: "The Daily Star", - date: "Jan 2024", - headline: "'Tech for Palestine' Initiative Launched to Support Palestinians", - description: "Reporting on the launch and global reach of the Tech for Palestine movement.", - url: "https://www.thedailystar.net/tech-startup/news/tech-palestine-initiative-launched-support-palestinians-3510791", - }, - { - publication: "Muslim Tech Wire", - date: "Jan 2024", - headline: "Tech for Palestine — A Global Call", - description: - "Coverage of the initiative's mission to support Palestine through open-source tools.", - url: "https://muslimtechwire.com/tech-for-palestine-launches", - }, - { - publication: "Counter Points", - date: "2024", - headline: "Paul on Counter Points, Tech for Palestine", - url: "https://www.youtube.com/watch?v=UldcFPEZBM4&t=4388s", - }, -]; - -const interviews = [ - { - id: "SZYxUoHYPIg", - title: "Gaza Diaries: Paul Biggar & Dr. Omar Suleiman", - description: - "Founder Paul Biggar joins Dr. Omar Suleiman to discuss the role of tech in the Palestinian liberation movement.", - }, - { - id: "LzXASFo-cf0", - title: "How Big Tech Enables Israel's Crimes", - description: - "A deep conversation on the relationship between the tech industry and Israeli military operations.", - }, - { - id: "lQ5DLKLqUtg", - title: "Voices of Impact: Tech for Palestine's Journey", - description: - "An inside look at how Tech for Palestine grew from a single blog post to a global coalition of advocates.", - }, - { - id: "8mdYIozAgBw", - title: "TRT World Interview", - description: - "TRT World sits down with Tech for Palestine on tech accountability and advocacy amid the ongoing genocide.", - }, -]; - -const lightAssets = [ - { label: "Logo (PNG)", file: "/logos/Logo.png" }, - { label: "Logo (SVG)", file: "/logos/Logo.svg" }, - { label: "Logo Transparent", file: "/logos/Logo---Transparent.png" }, - { label: "Mark (PNG)", file: "/logos/Mark.png" }, - { label: "Mark Transparent", file: "/logos/Mark---Transparent.png" }, - { label: "Brand Colors", file: "/logos/Colors.png" }, -]; - -const darkAssets = [ - { label: "Logo Dark", file: "/logos/Logo-dark.png" }, - { label: "Mark Dark", file: "/logos/Mark-dark.png" }, -]; - -// Mentions rather than NewsArticle: these articles are hosted on third-party -// publications' own domains, not this page, so NewsArticle (which expects an -// `author` and describes content published at this URL) doesn't apply. -const pressMentions = [leadArticle, ...articles].map((a) => ({ - "@type": "CreativeWork", - headline: a.headline, - url: a.url, - datePublished: toIsoDate(a.date), - publisher: { - "@type": "Organization", - name: a.publication, - }, -})); ---- - - - - -
- -
-
- -
-

Media & Press

-

- Interviews, news coverage, and thought leadership about Tech for Palestine. -

-
-
- - -
-

Media inquiries

- - media@techforpalestine.org - -

- For press inquiries, interviews, or media requests, please contact: -

-
- - -
-

For media

-

Press kit

-

- Tech for Palestine is a 501(c)(3) nonprofit that incubates and scales tech and advocacy - projects for Palestinian liberation. Our community spans thousands of volunteers, - engineers, designers, and advocates worldwide. Use the assets below in coverage, - attribution, and partner materials. -

-

- Tech for Palestine ("T4P") is a coalition of thousands of founders, engineers, product - marketers, investors and other professionals who are working in support of Palestinian - liberation. The T4P Incubator helps pro-Palestine advocates build, grow, and scale their - work towards a Free Palestine. We support projects — whether collections of individuals, - registered non-profits, or even companies — whose mission helps Palestine, especially - advocacy groups building technical products or in the tech space. -

-
- - -
-
- - -
-

Official logos

- - - - - -
-

Dark variants · for use on dark backgrounds

- -
-
- { - [ - { href: "/logos/Logo.ai", label: "Logo (Adobe Illustrator)" }, - { href: "/logos/Logo.pdf", label: "Logo (PDF)" }, - { href: "/logos/Font-20240919T051150Z-001.zip", label: "Fonts (ZIP)" }, - ].map(({ href, label }) => ( - - - {label} - - )) - } -
-
-
-
- - -
- -
- - -
-
-
-

On camera

-

Interviews

-
- -
- { - interviews.map((interview) => ( -
-
- -
-
- - -
-
-

- "Tens of thousands of well-funded hasbara projects exist. We're building - tens of thousands of liberation initiatives to match them." -

-
-
- Paul Biggar, Founder of Tech for Palestine -
-

Paul Biggar

-

Founder, Tech for Palestine

-
-
-
-
-
-
- - - - diff --git a/src/components/hook-form/form-provider.tsx b/src/components/hook-form/form-provider.tsx deleted file mode 100644 index e7912e5e..00000000 --- a/src/components/hook-form/form-provider.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { FormProvider as Form } from "react-hook-form"; -import type { UseFormReturn, SubmitHandler } from "react-hook-form"; - -// ---------------------------------------------------------------------- - -type Props = { - children: React.ReactNode; - methods: UseFormReturn; - onSubmit?: SubmitHandler; -}; - -export default function FormProvider({ children, onSubmit, methods }: Props) { - return ( -
- { - console.log("🔥 Form submit event triggered!"); - methods.handleSubmit( - onSubmit ?? (() => {}), - (errors) => console.log("❌ Validation Errors:", errors) // Log errors - )(e); - }} - > - {children} -
- - ); -} diff --git a/src/components/hook-form/index.ts b/src/components/hook-form/index.ts deleted file mode 100644 index 47e1f67d..00000000 --- a/src/components/hook-form/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from "./rhf-select"; -export * from "./rhf-checkbox"; - -export { default } from "./form-provider"; -export { default as RHFTextField } from "./rhf-text-field"; -export { default as RHFSkillSelector } from "./rhf-check-table"; -export { default as RHFFileUpload } from "./rhf-file-upload"; -export { default as RHFRadioGroup } from "./rhf-radio-group"; -export { default as RHFTableSection } from "./rhf-table-section"; diff --git a/src/components/hook-form/rhf-check-table.tsx b/src/components/hook-form/rhf-check-table.tsx deleted file mode 100644 index e8727076..00000000 --- a/src/components/hook-form/rhf-check-table.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Controller, useFormContext } from "react-hook-form"; -import { - Radio, - RadioGroup, - FormControlLabel, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, - FormHelperText, -} from "@mui/material"; - -type SkillOption = { - skill: string; // Changed from 'skill_name' to 'skill' - label: string; -}; - -type Props = { - name: string; - skills: SkillOption[]; - skillLevels: string[]; // Dynamic columns (e.g., Beginner, Intermediate, Advanced) -}; - -export default function RHFSkillSelector({ name, skills = [], skillLevels = [] }: Props) { - const { - control, - setValue, - formState: { errors }, - watch, - } = useFormContext(); - - // Handle radio change to update the selected skill - const handleRadioChange = (skill: string, skill_level: string) => { - console.log(skill, "skill"); - // Update the form value to store the skill level for the corresponding skill - setValue(name, { - ...watch(name), - [skill]: skill_level, // Store skill level for each skill - }); - }; - - return ( - - - - - - Skill - - {skillLevels.map((level) => ( - - {level} - - ))} - - - - {skills.map(({ skill, label }) => { - // Changed 'skill_name' to 'skill' - // Fetch the selected skill level for the current skill - const selectedSkillLevel = watch(name)?.[skill]; // Changed 'skill_name' to 'skill' - - // Get the validation error for the current skill - const skillError = (errors as any)[name]?.[skill]; // Changed 'skill_name' to 'skill' - - return ( - - {" "} - {/* Changed 'skill_name' to 'skill' */} - - {label} - {/* Displaying error below the skill name */} - {skillError && {skillError.message}} - - {skillLevels.map((level) => ( - - ( - <> - handleRadioChange(skill, e.target.value)} // Changed 'skill_name' to 'skill' - > - } label={level} /> - - - )} - /> - - ))} - - ); - })} - -
-
- ); -} diff --git a/src/components/hook-form/rhf-checkbox.tsx b/src/components/hook-form/rhf-checkbox.tsx deleted file mode 100644 index 44cf5ef8..00000000 --- a/src/components/hook-form/rhf-checkbox.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Controller, useFormContext } from "react-hook-form"; - -import { FormControl, FormLabel, FormGroup, Checkbox, FormHelperText } from "@mui/material"; -import { FormControlLabel } from "@mui/material"; -import type { FormControlLabelProps } from "@mui/material/FormControlLabel"; -import { formControlLabelClasses } from "@mui/material/FormControlLabel"; -// ---------------------------------------------------------------------- - -interface RHFCheckboxProps extends Omit { - name: string; - helperText?: React.ReactNode; -} - -export function RHFCheckbox({ name, helperText, ...other }: RHFCheckboxProps) { - const { control } = useFormContext(); - - return ( - ( -
- } {...other} /> - - {(!!error || helperText) && ( - {error ? error?.message : helperText} - )} -
- )} - /> - ); -} - -// ---------------------------------------------------------------------- - -interface RHFMultiCheckboxProps extends Omit { - name: string; - options: { label: string; value: any }[]; - row?: boolean; - label?: string; - spacing?: number; - helperText?: React.ReactNode; -} - -export function RHFMultiCheckbox({ - row, - name, - label, - options, - spacing, - helperText, - sx, - ...other -}: RHFMultiCheckboxProps) { - const { control } = useFormContext(); - - const getSelected = (selectedItems: string[], item: string) => - selectedItems.includes(item) - ? selectedItems.filter((value) => value !== item) - : [...selectedItems, item]; - - return ( - ( - - {label && ( - - {label} - - )} - - - {options.map((option) => ( - field.onChange(getSelected(field.value, option.value))} - /> - } - label={option.label} - {...other} - /> - ))} - - - {(!!error || helperText) && ( - - {error ? error?.message : helperText} - - )} - - )} - /> - ); -} diff --git a/src/components/hook-form/rhf-file-upload.tsx b/src/components/hook-form/rhf-file-upload.tsx deleted file mode 100644 index 737341f5..00000000 --- a/src/components/hook-form/rhf-file-upload.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type { ChangeEvent } from "react"; -import { Controller, useFormContext } from "react-hook-form"; -import { Box, Button, Typography } from "@mui/material"; -import CloudUploadIcon from "@mui/icons-material/CloudUpload"; - -// ---------------------------------------------------------------------- - -type Props = { - name: string; - label?: string; -}; - -export default function RHFFileUpload({ name, label }: Props) { - const { control, setValue, watch } = useFormContext(); - const file = watch(name); // Watch the file value to update UI correctly - - const handleFileChange = (event: ChangeEvent) => { - const selectedFile = event.target.files?.[0] || null; - setValue(name, selectedFile, { shouldValidate: true }); // Ensure validation updates - }; - - return ( - ( - - - - - {error && ( - - {error.message} - - )} - - )} - /> - ); -} diff --git a/src/components/hook-form/rhf-radio-group.tsx b/src/components/hook-form/rhf-radio-group.tsx deleted file mode 100644 index 822a0983..00000000 --- a/src/components/hook-form/rhf-radio-group.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Controller, useFormContext } from "react-hook-form"; - -import Radio from "@mui/material/Radio"; -import FormLabel from "@mui/material/FormLabel"; -import FormControl from "@mui/material/FormControl"; -import FormHelperText from "@mui/material/FormHelperText"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import RadioGroup from "@mui/material/RadioGroup"; -import type { RadioGroupProps } from "@mui/material"; - -// ---------------------------------------------------------------------- - -type Props = RadioGroupProps & { - name: string; - options: { label: string; value: any }[]; - label?: string; - spacing?: number; - helperText?: React.ReactNode; -}; - -export default function RHFRadioGroup({ - row, - name, - label, - options, - spacing, - helperText, - ...other -}: Props) { - const { control } = useFormContext(); - - const labelledby = label ? `${name}-${label}` : ""; - - return ( - ( - - {label && ( - - {label} - - )} - - - {options.map((option) => ( - } - label={option.label} - sx={{ - "&:not(:last-of-type)": { - mb: spacing || 0, - }, - ...(row && { - mr: 0, - "&:not(:last-of-type)": { - mr: spacing || 2, - }, - }), - }} - /> - ))} - - - {(!!error || helperText) && ( - - {error ? error?.message : helperText} - - )} - - )} - /> - ); -} diff --git a/src/components/hook-form/rhf-select.tsx b/src/components/hook-form/rhf-select.tsx deleted file mode 100644 index 25fa7f14..00000000 --- a/src/components/hook-form/rhf-select.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { Controller, useFormContext } from "react-hook-form"; -import Box from "@mui/material/Box"; -import Chip from "@mui/material/Chip"; -import Select from "@mui/material/Select"; -import MenuItem from "@mui/material/MenuItem"; -import Checkbox from "@mui/material/Checkbox"; -import InputLabel from "@mui/material/InputLabel"; -import type { SxProps, Theme } from "@mui/material/styles"; -import FormHelperText from "@mui/material/FormHelperText"; -import TextField from "@mui/material/TextField"; -import type { TextFieldProps } from "@mui/material"; -import FormControl from "@mui/material/FormControl"; -import type { FormControlProps } from "@mui/material/FormControl"; - -// ---------------------------------------------------------------------- - -type RHFSelectProps = TextFieldProps & { - name: string; - native?: boolean; - maxHeight?: boolean | number; - children: React.ReactNode; - PaperPropsSx?: SxProps; - rules?: any; - label?: React.ReactNode; -}; - -export function RHFSelect({ - name, - native, - maxHeight = 220, - helperText, - children, - PaperPropsSx, - rules, - label, - ...other -}: RHFSelectProps) { - const { control } = useFormContext(); - - return ( - ( - - {label} * - - ) : ( - label - ) - } - slotProps={{ - select: { - native, - MenuProps: { - slotProps: { - paper: { - sx: { - ...(!native && { - maxHeight: typeof maxHeight === "number" ? maxHeight : "unset", - }), - ...PaperPropsSx, - }, - }, - }, - }, - sx: { textTransform: "capitalize" }, - }, - }} - error={!!error} - helperText={error ? error?.message : helperText} - sx={{ - backgroundColor: "white", // ✅ Set white background - borderRadius: "8px", // ✅ Optional: Rounded corners - "& .MuiInputBase-input": { - backgroundColor: "white", // ✅ Ensure input itself is white - }, - }} - {...other} - > - {children} - - )} - /> - ); -} - -// ---------------------------------------------------------------------- - -type RHFMultiSelectProps = FormControlProps & { - name: string; - label?: string; - chip?: boolean; - checkbox?: boolean; - placeholder?: string; - helperText?: React.ReactNode; - rules?: any; - options: { - title: string; - value: string; - name: string; - }[]; -}; - -export function RHFMultiSelect({ - name, - chip, - label, - options, - checkbox, - placeholder, - helperText, - rules, - ...other -}: RHFMultiSelectProps) { - const { control } = useFormContext(); - - const renderValues = (selectedIds: string[] = []) => { - const selectedItems = options.filter((item) => selectedIds.includes(item.name)); - - if (!selectedItems.length && placeholder) { - return {placeholder}; - } - - if (chip) { - return ( - - {selectedItems.map((item) => ( - - ))} - - ); - } - - return selectedItems.map((item) => item.title).join(", "); - }; - - return ( - ( - - {label && ( - - {rules?.required ? ( - - {label} * - - ) : ( - label - )} - - )} - - - {(!!error || helperText) && ( - {error ? error?.message : helperText} - )} - - )} - /> - ); -} diff --git a/src/components/hook-form/rhf-table-section.tsx b/src/components/hook-form/rhf-table-section.tsx deleted file mode 100644 index 6217aa9c..00000000 --- a/src/components/hook-form/rhf-table-section.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useFieldArray, useFormContext, Controller } from "react-hook-form"; -import { - Box, - Button, - IconButton, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, - Typography, -} from "@mui/material"; -import Add from "@mui/icons-material/Add"; -import Settings from "@mui/icons-material/Settings"; -import Delete from "@mui/icons-material/Delete"; -import { RenderFormFields } from "../InputsMapping"; - -type RHFTableSectionProps = { - name: string; - label?: string; - columns?: { fieldname: string; label: string; reqd?: number }[]; // Added reqd field -}; - -export default function RHFTableSection({ name, label, columns = [] }: RHFTableSectionProps) { - const { control } = useFormContext(); - const { fields, append, remove } = useFieldArray({ control, name }); - - const onAdd = () => { - append(Object.fromEntries(columns.map((col) => [col.fieldname, ""]))); - }; - - return ( - - {/* Label & Add Button */} - - {label && ( - - {label} - - )} - - - - {/* Table Container */} - - - {/* Table Header */} - - - {columns.map((col) => ( - - {col.label} {col.reqd === 1 && *} - - ))} - {/* Replace "Actions" with a Settings Icon */} - - - - - - - {/* Table Body */} - - {fields.length > 0 ? ( - fields.map((row, index) => ( - - {columns.map((col) => ( - - ( - - )} - /> - - ))} - - {/* Delete Button */} - - remove(index)} - sx={{ - "&:hover": { backgroundColor: "#f8d7da" }, - transition: "0.3s ease", - }} - > - - - - - )) - ) : ( - /* Show "No Data" when the table is empty */ - - - - No Data - - - - )} - -
-
-
- ); -} diff --git a/src/components/hook-form/rhf-text-field.tsx b/src/components/hook-form/rhf-text-field.tsx deleted file mode 100644 index 477ac743..00000000 --- a/src/components/hook-form/rhf-text-field.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Controller, useFormContext } from "react-hook-form"; -import TextField from "@mui/material/TextField"; -import type { TextFieldProps } from "@mui/material"; - -// ---------------------------------------------------------------------- - -type Props = TextFieldProps & { - name: string; - rules?: { required?: boolean | string }; -}; - -export default function RHFTextField({ name, label, helperText, type, rules, ...other }: Props) { - const { control } = useFormContext(); - - return ( - ( - - {label} * - - ) : ( - label - ) - } - fullWidth - type={type} - value={field.value ?? ""} // Default to empty string if value is null or undefined - onChange={(event) => { - if (type === "number") { - field.onChange(Number(event.target.value)); - } else { - field.onChange(event.target.value); - } - }} - error={!!error} - helperText={error ? error?.message : helperText} - sx={{ - backgroundColor: "white", // ✅ Set background to white - borderRadius: "8px", // Optional: Rounded corners - "& .MuiInputBase-input": { - backgroundColor: "white", // Ensure input itself is white - }, - }} - {...other} - /> - )} - /> - ); -} diff --git a/src/components/london-gathering/AgendaSection.tsx b/src/components/london-gathering/AgendaSection.tsx deleted file mode 100644 index 8df9338e..00000000 --- a/src/components/london-gathering/AgendaSection.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import { useState, useEffect } from "react"; - -interface Moderator { - id: string; - name: string; - title: string; - bio: string; - photo: string; -} - -interface AgendaItem { - id: string; - title: string; - description: string; - time: string; - moderator: Moderator | null; -} - -function sortAgendaItems(items: AgendaItem[]): AgendaItem[] { - const getBreakoutNumber = (title: string) => { - const match = title.match(/Breakout (\d+)/); - return match ? parseInt(match[1]) : 0; - }; - - const session1Breakouts = items - .filter((item) => item.title.includes("Breakout") && item.time === "10:30 - 12:00") - .sort((a, b) => getBreakoutNumber(a.title) - getBreakoutNumber(b.title)); - - const session2Breakouts = items - .filter((item) => item.title.includes("Breakout") && item.time === "13:30 - 15:00") - .sort((a, b) => getBreakoutNumber(a.title) - getBreakoutNumber(b.title)); - - const otherItems = items.filter((item) => !item.title.includes("Breakout")); - - const sorted = otherItems.sort((a, b) => { - const getStartMinutes = (timeStr: string) => { - if (!timeStr) return 0; - const startTime = timeStr.split(" - ")[0]; - const match = startTime.match(/^(\d{1,2}):(\d{2})/); - if (!match) return 0; - return parseInt(match[1], 10) * 60 + parseInt(match[2], 10); - }; - return getStartMinutes(a.time) - getStartMinutes(b.time); - }); - - const result: AgendaItem[] = []; - let session1Added = false; - sorted.forEach((item) => { - result.push(item); - if (!session1Added && item.title.toLowerCase().includes("keynote")) { - result.push({ - id: "session1-header", - title: "Workshop Session 1", - description: "", - time: "10:30 - 12:00", - moderator: null, - }); - result.push(...session1Breakouts); - session1Added = true; - } else if (item.title.toLowerCase().includes("lunch")) { - result.push({ - id: "session2-header", - title: "Workshop Session 2", - description: "", - time: "13:30 - 15:00", - moderator: null, - }); - result.push(...session2Breakouts); - } - }); - - return result; -} - -function AgendaIcon({ title }: { title: string }) { - const t = title.toLowerCase(); - if ( - t.includes("coffee") || - t.includes("welcome") || - t.includes("goodbye") || - t.includes("closing") - ) { - return ( - - - - ); - } - if (t.includes("lunch")) { - return ( - - - - ); - } - if (t.includes("breakout") || t.includes("workshop")) { - return ( - - - - ); - } - return ( - - - - ); -} - -function getIconBg(title: string): string { - const t = title.toLowerCase(); - if ( - t.includes("coffee") || - t.includes("welcome") || - t.includes("goodbye") || - t.includes("closing") - ) - return "bg-brand"; - if (t.includes("lunch")) return "bg-[#d97706]"; - if (t.includes("breakout") || t.includes("workshop")) return "bg-ink-dark"; - return "bg-[#168039]"; -} - -interface AgendaSectionProps { - initialAgendaItems?: AgendaItem[]; -} - -export default function AgendaSection({ initialAgendaItems = [] }: AgendaSectionProps) { - const [agendaItems, setAgendaItems] = useState( - initialAgendaItems.length > 0 ? sortAgendaItems(initialAgendaItems) : [] - ); - const [loading, setLoading] = useState(initialAgendaItems.length === 0); - - useEffect(() => { - if (initialAgendaItems.length === 0) { - fetch("/api/speakers") - .then((r) => r.json()) - .then((data) => setAgendaItems(sortAgendaItems(data.agendaItems || []))) - .catch((err) => console.error("Error fetching agenda:", err)) - .finally(() => setLoading(false)); - } - }, []); - - if (loading) { - return ( -
-
-

Loading agenda...

-
-
- ); - } - - return ( -
-
-

Schedule

-

Agenda

- -
- {agendaItems.map((item) => { - const isWorkshopHeader = item.title.includes("Workshop Session"); - const isBreakout = item.title.includes("Breakout"); - - if (isWorkshopHeader) { - return ( -
-
-
- -
-
-

{item.time}

-

- {item.title} -

-
-
-
- ); - } - - if (isBreakout) { - return ( -
-
-

- {item.title} - {item.moderator && ( - - {" "} - (moderated by {item.moderator.name}) - - )} -

-
-
- ); - } - - return ( -
-
-
- -
-
-

{item.time}

-

{item.title}

-
-
-
- ); - })} -
-
-
- ); -} diff --git a/src/components/london-gathering/SpeakersSection.tsx b/src/components/london-gathering/SpeakersSection.tsx deleted file mode 100644 index 7f457af5..00000000 --- a/src/components/london-gathering/SpeakersSection.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from "react"; - -interface Speaker { - id: string; - name: string; - title: string; - bio: string; - photo: string; -} - -interface SpeakersSectionProps { - initialSpeakers?: Speaker[]; -} - -export default function SpeakersSection({ initialSpeakers = [] }: SpeakersSectionProps) { - const [speakers, setSpeakers] = useState(initialSpeakers); - const [loading, setLoading] = useState(initialSpeakers.length === 0); - const [selectedSpeaker, setSelectedSpeaker] = useState(null); - const dialogRef = useRef(null); - - useEffect(() => { - if (initialSpeakers.length === 0) { - fetch("/api/speakers") - .then((r) => r.json()) - .then((data) => setSpeakers(data.speakers || [])) - .catch((err) => console.error("Error fetching speakers:", err)) - .finally(() => setLoading(false)); - } - }, []); - - const openModal = useCallback((speaker: Speaker) => { - setSelectedSpeaker(speaker); - dialogRef.current?.showModal(); - }, []); - - const closeModal = useCallback(() => { - dialogRef.current?.close(); - }, []); - - if (loading) { - return ( -
-
-

Loading speakers...

-
-
- ); - } - - return ( -
-
-

Who spoke

-

Speakers

- -
- {speakers.map((speaker) => ( - - ))} -
-
- - { - if (e.target === dialogRef.current) closeModal(); - }} - > - {selectedSpeaker && ( -
-
- {selectedSpeaker.name} -
-

{selectedSpeaker.name}

-

{selectedSpeaker.title}

-
- -
-
-

- {selectedSpeaker.bio || "Bio coming soon..."} -

-
-
- )} -
-
- ); -} diff --git a/src/components/membership/LegacyJoinSection.tsx b/src/components/membership/LegacyJoinSection.tsx deleted file mode 100644 index de51e188..00000000 --- a/src/components/membership/LegacyJoinSection.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { useEffect, useState } from "react"; -import { Box, Typography, Button } from "@mui/material"; -import MembershipCalculator from "../MembershipCalculator"; -import QgivJoin from "./QgivJoin"; -import AboutYouStep, { type AboutYouData } from "./AboutYouStep"; -import type { MembershipTier, QgivPrefill } from "./qgiv"; - -interface LegacyJoinSectionProps { - tier: MembershipTier; - /** Optional section heading. Omitted on the membership pages, which follow - * the brief's headingless prose structure. */ - heading?: string; -} - -/** Hides the "Become a member" CTA anchors once the join form is revealed — - * clicking them again is pointless once the form is already showing. These - * anchors live outside this component's tree (plain HTML in - * supporting-member.astro), so a DOM query is the simplest - * way to reach all of them uniformly. The reveal is one-directional (there is - * no "un-reveal" path), so there is no corresponding un-hide routine. */ -function hideJoinCtas(): void { - document.querySelectorAll("[data-membership-cta]").forEach((el) => { - el.style.display = "none"; - }); -} - -/** Shared minimum height for the About You / payment panel, matching QgivJoin's - * own minimum, so short content (the loading spinner, the About You form) - * doesn't look collapsed. The panel is allowed to grow taller than this when - * the Qgiv payment form needs more room, rather than clipping it. */ -const JOIN_PANEL_HEIGHT = 640; - -/** How long the "Next" button shows its own spinner before the payment step - * is revealed. Without this floor the swap is instant and the button's - * loading state never becomes visible — the visitor would see the button - * vanish and a second, differently-positioned spinner (QgivJoin's) appear in - * its place instead of one continuous loading experience anchored to their - * click. Mirrors the MIN_LOADING_DISPLAY_MS floor QgivJoin already uses. */ -const NEXT_BUTTON_LOADING_MS = 400; - -/** - * Payment surface for the legacy (green/grey) design system: the Qgiv embed - * and the dues calculator. - * - * The design-system equivalent is `MembershipDues.tsx`. The two are kept apart - * because `Layout.astro` does not load `design-system.css`, so the `ts-*` - * typography scale the new component relies on is unavailable here. - */ -export default function LegacyJoinSection({ tier, heading }: LegacyJoinSectionProps) { - const [step, setStep] = useState<"about-you" | "payment">("about-you"); - const [aboutYou, setAboutYou] = useState(null); - const [revealed, setRevealed] = useState(false); - const [advancingToPayment, setAdvancingToPayment] = useState(false); - - function handleAboutYouContinue(data: AboutYouData) { - setAboutYou(data); - setAdvancingToPayment(true); - window.setTimeout(() => { - setStep("payment"); - setAdvancingToPayment(false); - }, NEXT_BUTTON_LOADING_MS); - } - - function splitName(name: string): { firstName: string; lastName: string } { - const [firstName, ...rest] = name.trim().split(/\s+/); - return { firstName: firstName ?? "", lastName: rest.join(" ") }; - } - - const prefill: QgivPrefill | undefined = aboutYou - ? { ...splitName(aboutYou.name), email: aboutYou.email } - : undefined; - - useEffect(() => { - // The CTA that dispatches "membership:reveal-join" may fire before this - // island hydrates and registers its listener (e.g. the plain CTAs on - // /supporting-member, tracked by a vanilla - - diff --git a/src/styles/ideas.css b/src/styles/ideas.css deleted file mode 100644 index 9d2f121d..00000000 --- a/src/styles/ideas.css +++ /dev/null @@ -1,50 +0,0 @@ -/* ideas.css */ -.idea { - background: linear-gradient(to top left, #f0fdf4, #dcfce7); - border: 1px solid #bbf7d0; - border-radius: 1rem; - padding: 1.75rem; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); - transition: - transform 0.3s ease, - box-shadow 0.3s ease; - display: flex; - flex-direction: column; - justify-content: space-between; - position: relative; -} - -.idea:hover { - transform: translateY(-6px); - box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1); -} - -.idea h2 { - font-size: 1.375rem; - font-weight: 700; - color: #14532d; - margin-bottom: 0.75rem; - line-height: 1.3; -} - -.idea p { - color: #334155; - line-height: 1.7; - font-size: 0.95rem; -} - -.tags { - margin-top: 0.75rem; - display: flex; - flex-wrap: wrap; - gap: 0.5rem; -} - -.tag { - background-color: #bbf7d0; - color: #065f46; - font-size: 0.75rem; - padding: 0.25rem 0.5rem; - border-radius: 9999px; - font-weight: 500; -}