diff --git a/src/components/volunteer/OpenRoleCard.tsx b/src/components/volunteer/OpenRoleCard.tsx index 73559494..0331e2f2 100644 --- a/src/components/volunteer/OpenRoleCard.tsx +++ b/src/components/volunteer/OpenRoleCard.tsx @@ -1,6 +1,6 @@ import React, { useId, useMemo, useState } from "react"; import { parseDescriptionHtml, type DescriptionNode, type DescriptionTag } from "./descriptionHtml"; -import { getApplyTarget, type OpenRole } from "./openRoleData"; +import { MEMBERSHIP_URL, type OpenRole } from "./openRoleData"; import type { SkinTokens } from "./openRoleSkins"; /** Descriptions shorter than this read fine in full, so they get no toggle. */ @@ -89,7 +89,6 @@ interface OpenRoleCardProps { export default function OpenRoleCard({ role, tokens }: OpenRoleCardProps) { const [expanded, setExpanded] = useState(false); const descriptionId = useId(); - const apply = getApplyTarget(role); const isTeamRole = role.type === "team"; const canCollapse = role.description.length > CLAMP_THRESHOLD; const isCollapsed = canCollapse && !expanded; @@ -118,12 +117,9 @@ export default function OpenRoleCard({ role, tokens }: OpenRoleCardProps) {
We couldn’t load open roles just now. Try again, or{" "} - - apply to volunteer + + become a member {" "} and we’ll match you to a role.
@@ -126,8 +126,8 @@ export default function OpenRoles({ skin = "classic" }: OpenRolesProps) { return (No roles are open right now.{" "} - - Apply anyway + + Become a member {" "} and we’ll get in touch when one opens up.
diff --git a/src/components/volunteer/openRoleData.test.ts b/src/components/volunteer/openRoleData.test.ts index e9c6c6c1..4c37819e 100644 --- a/src/components/volunteer/openRoleData.test.ts +++ b/src/components/volunteer/openRoleData.test.ts @@ -2,13 +2,9 @@ import { describe, expect, it } from "vitest"; import { attachProjectLinks, clampPage, - getApplyTarget, getPageCount, getPageMarkers, - getRoleApplicationUrl, - MEMBERSHIP_URL, normalizeOpenRoles, - VOLUNTEER_SURVEY_URL, type OpenRole, } from "./openRoleData"; @@ -26,36 +22,6 @@ const projectRole: OpenRole = { createdAt: "2026-10-14T18:35:10.338Z", }; -describe("getRoleApplicationUrl", () => { - it("passes the role id, title and project name as encoded survey params", () => { - const url = new URL(getRoleApplicationUrl(projectRole)); - - expect(`${url.origin}${url.pathname}`).toBe(VOLUNTEER_SURVEY_URL); - expect(url.searchParams.get("role_id")).toBe(projectRole.id); - expect(url.searchParams.get("role_title")).toBe("Data Entry & Research"); - expect(url.searchParams.get("org_name")).toBe("Demilitarise Education"); - }); - - it("omits org_name when the role has no project or team name", () => { - const url = new URL(getRoleApplicationUrl({ ...projectRole, orgName: "" })); - - expect(url.searchParams.has("org_name")).toBe(false); - }); -}); - -describe("getApplyTarget", () => { - it("sends project roles to the volunteer survey with the role attached", () => { - const target = getApplyTarget(projectRole); - - expect(target.label).toBe("Apply to volunteer"); - expect(target.href).toBe(getRoleApplicationUrl(projectRole)); - }); - - it("sends team roles to membership", () => { - expect(getApplyTarget({ ...projectRole, type: "team" }).href).toBe(MEMBERSHIP_URL); - }); -}); - describe("normalizeOpenRoles", () => { const hubRow = { id: "14a4bd4a-a8ab-4dec-9c05-730f062a56dc", diff --git a/src/components/volunteer/openRoleData.ts b/src/components/volunteer/openRoleData.ts index 4767451c..b10da72c 100644 --- a/src/components/volunteer/openRoleData.ts +++ b/src/components/volunteer/openRoleData.ts @@ -36,23 +36,9 @@ export interface OpenRole { createdAt: string; } -export const VOLUNTEER_FORM_URL = "https://techforpalestine.org/volunteer-form"; +/** Every role, project or team, is applied for by becoming a member. */ export const MEMBERSHIP_URL = "/membership"; -/** - * The Formbricks survey `/volunteer-form` redirects to. Role links point here - * directly because that Cloudflare redirect drops the query string, and the - * survey reads `role_id`, `role_title` and `org_name` as passed-in hidden fields. - */ -export const VOLUNTEER_SURVEY_URL = "https://app.formbricks.com/s/clzavvkp80002yqhejhqgoa3a"; - -/** Volunteer survey link carrying the role, so the response records what was applied for. */ -export function getRoleApplicationUrl(role: OpenRole): string { - const params = new URLSearchParams({ role_id: role.id, role_title: role.title }); - if (role.orgName) params.set("org_name", role.orgName); - return `${VOLUNTEER_SURVEY_URL}?${params.toString()}`; -} - /** Upstream descriptions are free text; cap them so one bad row can't bloat the page. */ const MAX_DESCRIPTION_LENGTH = 4000; /** Markup roughly doubles a description's length, so its cap is looser. */ @@ -147,13 +133,6 @@ export function attachProjectLinks( })); } -/** Where a role's apply button points, per how the role is staffed. */ -export function getApplyTarget(role: OpenRole): { href: string; label: string } { - return role.type === "team" - ? { href: MEMBERSHIP_URL, label: "Become a member" } - : { href: getRoleApplicationUrl(role), label: "Apply to volunteer" }; -} - export interface SkillFilter { name: string; count: number; diff --git a/src/pages/index.astro b/src/pages/index.astro index c62ca0ff..7a2dd25a 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,6 +4,7 @@ import SignUpForm from "../structures/SignUpForm.astro"; import CommunityCallBanner from "../components/CommunityCallBanner.astro"; import NewsletterPopup from "../components/home/NewsletterPopup"; import HackathonPopup from "../components/home/HackathonPopup.astro"; +import { HOMEPAGE_EXPERIMENT, pickHomepageVariant } from "../utils/homepageExperiment"; import "../styles/base.css"; const stats = [ @@ -153,22 +154,16 @@ const press = [ const focusRing = "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-grove"; const pillPrimary = `inline-flex items-center justify-center rounded-pill bg-grove px-7 py-3.5 text-[15px] font-bold text-white transition-colors duration-150 hover:bg-grove-hover ${focusRing}`; -// The hero's two paths: each card is one link, a sentence then the action. -const heroPaths = [ - { - href: "/membership", - body: "Join the community, build projects, and start your own through T4P:", - action: "Become a Member", - tone: "border-grove bg-grove text-white hover:border-grove-hover hover:bg-grove-hover", - }, - { - href: "/incubator", - body: "If you run your own pro-Palestine project, and want help scaling:", - action: "Apply to the Incubator", - tone: "border-ink-divider bg-paper text-ink hover:border-grove hover:shadow-sm", - }, -]; -const heroCard = `flex flex-col rounded-lg border p-6 transition-[background-color,border-color,box-shadow] duration-150 ${focusRing}`; +// Hero buttons carry a sentence then the action, so they wrap as running text, not flex items. +const heroButton = `inline-block rounded-pill px-7 py-3.5 text-left text-[15px] leading-[1.45] transition-colors duration-150 ${focusRing}`; +const heroPrimary = `${heroButton} bg-grove text-white hover:bg-grove-hover`; +const heroGhost = `${heroButton} border border-ink-divider text-ink hover:border-grove hover:text-grove`; + +// Homepage description test. The variant is chosen per request and rendered into the HTML, +// so there is no flicker and nothing is stored. The page must not be cached or every visitor +// in the cache window would get the same variant. +const variant = pickHomepageVariant(); +Astro.response.headers.set("Cache-Control", "no-store"); const cardBase = `group relative flex flex-col rounded-lg border border-ink-divider bg-paper transition-[border-color,box-shadow] duration-150 hover:border-grove hover:shadow-sm ${focusRing}`; --- @@ -197,33 +192,60 @@ const cardBase = `group relative flex flex-col rounded-lg border border-ink-divi Join the tech movement behind 90+ projects for Palestine -- Tech for Palestine is a 501(c)(3) nonprofit starting and scaling initiatives for - Palestinian liberation. -
-- We support anyone with a meaningful idea, whether it is working against media censorship, - using tech to aid protests, scaling boycotts, pushing for tech divestment, supporting - Palestinian entrepreneurs, or anything else that contributes to a free Palestine. -
+ { + variant === "A" ? ( + <> ++ Tech for Palestine is a 501(c)(3) nonprofit starting and scaling initiatives for + Palestinian liberation. +
++ We support anyone with a meaningful idea, whether it is working against media + censorship, using tech to aid protests, scaling boycotts, pushing for tech + divestment, supporting Palestinian entrepreneurs, or anything else that contributes + to a free Palestine. +
+ > + ) : ( + <> ++ Tech for Palestine is a member-driven organization that brings together people, + skills and resources. Through technology and collective action, our members build + tools and solutions that support the movement for Palestine and help people take + action against Israel's occupation, apartheid and genocide. +
++ These projects include: apps to organize protests, tools that make boycotting + companies easier, support for Palestinian students and businesses, alternatives to + mainstream tech platforms, and many more. +
++ Whether you're a tech worker, founder, marketer, activist - there's a place for you + to contribute at T4P. +
++ Our Vision: A free Palestine, a tech + industry that actively participates in liberation, and a world where all people + enjoy equal dignity, justice, and human rights. +
+ > + ) + } -