Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/MembershipPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default function MembershipPage() {
href="#join"
onClick={handleJoinClick("secondary_cta")}
data-membership-cta="true"
className="inline-flex min-h-[44px] items-center justify-center rounded-full bg-[#168039] px-6 py-3 font-semibold text-white shadow-sm transition-all duration-150 hover:bg-[#116b2f] hover:shadow-md active:bg-[#116b2f] active:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#168039] focus-visible:ring-offset-2"
className="inline-flex min-h-[88px] items-center justify-center rounded-full bg-[#168039] px-12 py-6 text-2xl font-semibold text-white shadow-sm transition-all duration-150 hover:bg-[#116b2f] hover:shadow-md active:bg-[#116b2f] active:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#168039] focus-visible:ring-offset-2"
>
Become a member
</a>
Expand Down
5 changes: 4 additions & 1 deletion src/components/TestimonialsMarquee.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ interface Props {
membershipLive?: boolean;
heading?: string;
subheading?: string;
background?: "default" | "green";
}

const {
membershipLive = false,
heading = "What it's like to work with T4P",
subheading = "Project leaders, mentors, and members on their time inside the coalition.",
background = "default",
} = Astro.props;

const testimonials = getTestimonials(membershipLive);
const sectionBgClass = background === "green" ? "border-green-100 bg-green-50" : "border-zinc-200 bg-zinc-50";
---

<section class="border-t border-zinc-200 bg-zinc-50 py-16">
<section class:list={["border-t py-16", sectionBgClass]}>
<div class="mx-auto max-w-6xl px-6 sm:px-8">
<h2 class="text-3xl font-extrabold tracking-tight text-zinc-900">{heading}</h2>
<p class="mt-3 max-w-2xl text-lg text-zinc-600">{subheading}</p>
Expand Down
5 changes: 4 additions & 1 deletion src/components/home/TestimonialsSection.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface Props {
accent?: string;
/** Right-hand supporting line, beside the heading on wide viewports. */
subheading?: string;
background?: "default" | "green";
}

const {
Expand All @@ -18,12 +19,14 @@ const {
heading = "What it's like to",
accent = "work with T4P",
subheading = "Project leaders, mentors, and members on their time inside the coalition.",
background = "default",
} = Astro.props;

const testimonials = getTestimonials(membershipLive);
const sectionBgClass = background === "green" ? "bg-positive-tint" : "bg-page";
---

<section class="bg-page pb-14 pt-14 min-[810px]:pb-20 min-[810px]:pt-20">
<section class:list={[sectionBgClass, "pb-14 pt-14 min-[810px]:pb-20 min-[810px]:pt-20"]}>
<div class="mx-auto max-w-[1400px] px-6 min-[810px]:px-10">
<div
class="testimonials-header mb-12 grid grid-cols-1 gap-6 min-[810px]:mb-16 min-[810px]:grid-cols-[1fr_1fr] min-[810px]:items-end"
Expand Down
55 changes: 6 additions & 49 deletions src/components/membership/LegacyJoinSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,18 @@ const NEXT_BUTTON_LOADING_MS = 400;

/**
* Payment surface for the legacy (green/grey) design system: the Qgiv embed
* and the dues calculator A/B test.
* 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 [showCalculator, setShowCalculator] = useState(false);
const [variant, setVariant] = useState<string | undefined>(undefined);
const [step, setStep] = useState<"about-you" | "payment">("about-you");
const [aboutYou, setAboutYou] = useState<AboutYouData | null>(null);
const [revealed, setRevealed] = useState(false);
const [advancingToPayment, setAdvancingToPayment] = useState(false);

const runsCalculatorTest = tier === "member";

function handleAboutYouContinue(data: AboutYouData) {
setAboutYou(data);
setAdvancingToPayment(true);
Expand All @@ -74,40 +70,6 @@ export default function LegacyJoinSection({ tier, heading }: LegacyJoinSectionPr
? { ...splitName(aboutYou.name), email: aboutYou.email }
: undefined;

useEffect(() => {
if (!runsCalculatorTest) return;

const urlParams = new URLSearchParams(window.location.search);
const urlParam = urlParams.get("calculator");

let assigned: boolean;
if (urlParam === "yes") {
assigned = true;
} else if (urlParam === "no") {
assigned = false;
} else {
const stored = localStorage.getItem("membership_ab_variant");
if (stored === "Calculator") {
assigned = true;
} else if (stored === "No Calculator") {
assigned = false;
} else {
assigned = Math.random() < 0.5;
localStorage.setItem("membership_ab_variant", assigned ? "Calculator" : "No Calculator");
}
}

const assignedVariant = assigned ? "Calculator" : "No Calculator";
setShowCalculator(assigned);
setVariant(assignedVariant);

if (typeof window.plausible !== "undefined") {
window.plausible("Membership Page", {
props: { membership_variant: assignedVariant },
});
}
}, [runsCalculatorTest]);

useEffect(() => {
// The CTA that dispatches "membership:reveal-join" may fire before this
// island hydrates and registers its listener (e.g. the plain <a> CTAs on
Expand All @@ -129,17 +91,12 @@ export default function LegacyJoinSection({ tier, heading }: LegacyJoinSectionPr
return () => window.removeEventListener("membership:reveal-join", handleReveal);
}, []);

// The supporting tier always gets the calculator; the member tier only gets
// it in the "Calculator" A/B arm.
const calculatorVisible = step === "payment" && (tier === "supporting" || showCalculator);
const calculatorVisible = step === "payment";

const intro = calculatorVisible
? tier === "supporting"
const intro =
tier === "supporting"
? "Contribute any amount for supporting membership dues. We suggest monthly dues equal to one hour's salary, which you can calculate below:"
: "Contribute any amount for membership dues. We suggest monthly dues equal to one hour's salary, which you can calculate below:"
: tier === "supporting"
? "Contribute any amount for supporting membership dues. We suggest monthly dues equal to one hour's salary."
: "Contribute any amount for membership dues. We suggest monthly dues equal to one hour's salary.";
: "Contribute any amount for membership dues. We suggest monthly dues equal to one hour's salary, which you can calculate below:";

return (
<Box id="join" sx={{ scrollMarginTop: "80px" }}>
Expand Down Expand Up @@ -217,7 +174,7 @@ export default function LegacyJoinSection({ tier, heading }: LegacyJoinSectionPr
</Box>
{aboutYou && (
<Box sx={{ display: step === "payment" ? "block" : "none" }}>
<QgivJoin tier={tier} variant={variant} prefill={prefill} />
<QgivJoin tier={tier} prefill={prefill} />
</Box>
)}
</Box>
Expand Down
55 changes: 5 additions & 50 deletions src/components/membership/MembershipDues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,11 @@ const JOIN_PANEL_HEIGHT = 640;
const NEXT_BUTTON_LOADING_MS = 400;

export default function MembershipDues({ tier = "member" }: MembershipDuesProps) {
// Default to the no-calculator variant so this renders identically during
// SSR (no window/localStorage access) before the A/B assignment runs below.
const [showCalculator, setShowCalculator] = useState(false);
const [variant, setVariant] = useState<string | undefined>(undefined);
const [step, setStep] = useState<"about-you" | "payment">("about-you");
const [aboutYou, setAboutYou] = useState<AboutYouData | null>(null);
const [revealed, setRevealed] = useState(false);
const [advancingToPayment, setAdvancingToPayment] = useState(false);

const runsCalculatorTest = tier === "member";

function handleAboutYouContinue(data: AboutYouData) {
setAboutYou(data);
setAdvancingToPayment(true);
Expand All @@ -64,40 +58,6 @@ export default function MembershipDues({ tier = "member" }: MembershipDuesProps)
? { ...splitName(aboutYou.name), email: aboutYou.email }
: undefined;

useEffect(() => {
if (!runsCalculatorTest) return;

const urlParams = new URLSearchParams(window.location.search);
const urlParam = urlParams.get("calculator");

let assigned: boolean;
if (urlParam === "yes") {
assigned = true;
} else if (urlParam === "no") {
assigned = false;
} else {
const stored = localStorage.getItem("membership_ab_variant");
if (stored === "Calculator") {
assigned = true;
} else if (stored === "No Calculator") {
assigned = false;
} else {
assigned = Math.random() < 0.5;
localStorage.setItem("membership_ab_variant", assigned ? "Calculator" : "No Calculator");
}
}

const assignedVariant = assigned ? "Calculator" : "No Calculator";
setShowCalculator(assigned);
setVariant(assignedVariant);

if (typeof window.plausible !== "undefined") {
window.plausible("Membership Page", {
props: { membership_variant: assignedVariant },
});
}
}, [runsCalculatorTest]);

useEffect(() => {
// The CTA that dispatches "membership:reveal-join" may fire before this
// island hydrates and registers its listener (e.g. plain <a> CTAs tracked
Expand All @@ -119,17 +79,12 @@ export default function MembershipDues({ tier = "member" }: MembershipDuesProps)
return () => window.removeEventListener("membership:reveal-join", handleReveal);
}, []);

// The supporting tier always gets the calculator; the member tier only gets
// it in the "Calculator" A/B arm.
const calculatorVisible = step === "payment" && (tier === "supporting" || showCalculator);
const calculatorVisible = step === "payment";

const intro = calculatorVisible
? tier === "supporting"
const intro =
tier === "supporting"
? "Contribute any amount for supporting membership dues. We suggest monthly dues equal to one hour's salary, which you can calculate below:"
: "Contribute any amount for membership dues. We suggest monthly dues equal to one hour's salary, which you can calculate below:"
: tier === "supporting"
? "Contribute any amount for supporting membership dues. We suggest monthly dues equal to one hour's salary."
: "Contribute any amount for membership dues. We suggest monthly dues equal to one hour's salary.";
: "Contribute any amount for membership dues. We suggest monthly dues equal to one hour's salary, which you can calculate below:";

return (
<div>
Expand Down Expand Up @@ -199,7 +154,7 @@ export default function MembershipDues({ tier = "member" }: MembershipDuesProps)
</div>
{aboutYou && (
<div style={{ display: step === "payment" ? "block" : "none" }}>
<QgivJoin tier={tier} variant={variant} prefill={prefill} />
<QgivJoin tier={tier} prefill={prefill} />
</div>
)}
</div>
Expand Down
10 changes: 2 additions & 8 deletions src/components/membership/QgivJoin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ function prefillKey(prefill?: QgivPrefill): string {

interface QgivJoinProps {
tier: MembershipTier;
/**
* Plausible prop recording which membership-page A/B variant the visitor saw.
* Only meaningful for the `member` tier.
*/
variant?: string;
className?: string;
prefill?: QgivPrefill;
}
Expand All @@ -46,7 +41,7 @@ interface QgivTransactionDetail {
* embed and the listener — keeping them apart is what caused /membership-new to
* silently stop sending Hub invites and EmailOctopus tags.
*/
export default function QgivJoin({ tier, variant, className, prefill }: QgivJoinProps) {
export default function QgivJoin({ tier, className, prefill }: QgivJoinProps) {
const form = QGIV_FORMS[tier];
const scriptLoadedRef = useRef(false);
const containerRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -183,7 +178,6 @@ export default function QgivJoin({ tier, variant, className, prefill }: QgivJoin
props: {
amount: transaction.total != null ? String(transaction.total) : "",
membership_tier: tier,
...(variant ? { membership_variant: variant } : {}),
},
});
}
Expand All @@ -205,7 +199,7 @@ export default function QgivJoin({ tier, variant, className, prefill }: QgivJoin

document.addEventListener("QGIV.donationComplete", handleDonationComplete);
return () => document.removeEventListener("QGIV.donationComplete", handleDonationComplete);
}, [tier, variant]);
}, [tier]);

// Safety net for a tier whose embed ID has been cleared: link out to the
// hosted form so the page still converts, at the cost of losing completion
Expand Down
16 changes: 13 additions & 3 deletions src/components/ui/Button.astro
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
interface Props {
variant?: "primary" | "ghost" | "text";
size?: "md" | "lg";
size?: "md" | "lg" | "xl";
href?: string;
arrow?: boolean;
class?: string;
Expand All @@ -22,6 +22,7 @@ const base =
const sizes = {
md: "px-5 py-3.5",
lg: "px-10 py-5 min-[810px]:px-11",
xl: "btn-xl px-10 py-7 min-[810px]:px-14 min-[810px]:py-8",
};

const variants = {
Expand All @@ -41,8 +42,8 @@ const Tag = href ? "a" : "button";
{
arrow && (
<svg
width="16"
height="16"
width={size === "xl" ? "32" : "16"}
height={size === "xl" ? "32" : "16"}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
Expand All @@ -54,3 +55,12 @@ const Tag = href ? "a" : "button";
)
}
</Tag>

<style>
/* Doubles the visual size of the "lg" primary CTA — bigger than the
shared ts-label scale allows without touching that scale's font-size
everywhere else it's used. */
.btn-xl {
font-size: 32px;
}
</style>
4 changes: 2 additions & 2 deletions src/pages/membership-new.astro
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true";
<Button
href="#join"
variant="primary"
size="md"
size="xl"
arrow
data-track-join-click="secondary_cta"
data-membership-cta="true"
Expand All @@ -145,7 +145,7 @@ const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true";
</div>
</section>

<TestimonialsSection membershipLive={membershipLive} />
<TestimonialsSection membershipLive={membershipLive} background="green" />
</main>
</HomeLayout>

Expand Down
2 changes: 1 addition & 1 deletion src/pages/membership.astro
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ import "../styles/base.css";
</div>
</div>

<TestimonialsMarquee />
<TestimonialsMarquee background="green" />
</main>
</Layout>
9 changes: 7 additions & 2 deletions src/pages/supporting-member-new.astro
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true";
<Button
href="#join"
variant="primary"
size="md"
size="xl"
arrow
data-track-join-click="secondary_cta"
data-membership-cta="true"
Expand All @@ -126,7 +126,12 @@ const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true";
</div>
</section>

<TestimonialsSection membershipLive={membershipLive} heading="Your support in" accent="action." />
<TestimonialsSection
membershipLive={membershipLive}
heading="Your support in"
accent="action."
background="green"
/>
</main>
</HomeLayout>

Expand Down
4 changes: 2 additions & 2 deletions src/pages/supporting-member.astro
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,14 @@ const logoWallLabel = `Projects supported by Tech for Palestine: ${showcaseProje
href="#join"
data-track-join-click="secondary_cta"
data-membership-cta="true"
class="inline-flex min-h-[44px] items-center justify-center rounded-full bg-[#168039] px-6 py-3 font-semibold text-white shadow-sm transition-all duration-150 hover:bg-[#116b2f] hover:shadow-md active:bg-[#116b2f] active:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#168039] focus-visible:ring-offset-2"
class="inline-flex min-h-[88px] items-center justify-center rounded-full bg-[#168039] px-12 py-6 text-2xl font-semibold text-white shadow-sm transition-all duration-150 hover:bg-[#116b2f] hover:shadow-md active:bg-[#116b2f] active:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#168039] focus-visible:ring-offset-2"
>
Become a Supporting Member
</a>
</div>
</div>

<TestimonialsMarquee heading="Your support in action." />
<TestimonialsMarquee heading="Your support in action." background="green" />
</main>

<script>
Expand Down
Loading