From 4d10c4e0a22b033597e58deb001b6386b043fe79 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sun, 19 Apr 2026 18:59:24 +0100 Subject: [PATCH] feat(join): replace tier dropdown with amount input + guidance chips --- app/pages/join.vue | 147 ++++++++++++++++++++++++++++----------------- 1 file changed, 91 insertions(+), 56 deletions(-) diff --git a/app/pages/join.vue b/app/pages/join.vue index 59d943a..24cad1a 100644 --- a/app/pages/join.vue +++ b/app/pages/join.vue @@ -32,7 +32,7 @@
- ${{ memberData?.contributionTier || "0" }} CAD/month + ${{ memberData?.contributionAmount ?? 0 }} CAD/month
@@ -69,14 +69,14 @@

Pay what you can

@@ -207,27 +207,38 @@ >
- - + +
+ $ + +
+
+ +
+

{{ guidanceLabel }}

-
Contribution
{{ selectedTier.label }}
+
Contribution
{{ formatContributionAmount(form.contributionAmount) }}
@@ -409,9 +420,8 @@ import { reactive, ref, computed, onMounted, onUnmounted } from "vue"; import { getCircleOptions } from "~/config/circles"; import { requiresPayment, - getContributionTierByValue, - getTierAmount, - CONTRIBUTION_TIERS, + CONTRIBUTION_PRESETS, + getGuidanceLabel, } from "~/config/contributions"; // Auth state @@ -427,7 +437,7 @@ const form = reactive({ email: "", name: "", circle: "community", - contributionTier: "15", + contributionAmount: 15, agreedToGuidelines: false, billingAddress: { street: "", @@ -461,29 +471,11 @@ const paymentToken = ref(null); // Circle options from central config const circleOptions = getCircleOptions(); -// Minimal labels for the dropdown — reactive to cadence. -// In annual mode, show both monthly and annual price so $50/yr (the $5 tier annual) -// is visually distinct from $500/yr (the $50 tier annual). -const contributionItems = computed(() => { - return Object.values(CONTRIBUTION_TIERS).map((tier) => { - const base = tier.amount; - if (base === 0) return { value: tier.value, label: "$0" }; - const monthlyLabel = `$${base}/mo`; - const priceLabel = - cadence.value === "annual" - ? `${monthlyLabel} → $${getTierAmount(tier, "annual")}/yr` - : monthlyLabel; - const hint = tier.value === "15" ? " (suggested)" : ""; - return { value: tier.value, label: `${priceLabel}${hint}` }; - }); -}); - -const formatTierAmount = (value) => { - const tier = getContributionTierByValue(value); - if (!tier || tier.amount === 0) return "$0"; - const amt = getTierAmount(tier, cadence.value); +const formatContributionAmount = (amount) => { + if (!amount || amount === 0) return "$0"; + const display = cadence.value === "annual" ? amount * 12 : amount; const suffix = cadence.value === "annual" ? "/yr" : "/mo"; - return `$${amt}${suffix}`; + return `$${display}${suffix}`; }; // Initialize composables @@ -499,20 +491,17 @@ const isFormValid = computed(() => { form.name && form.email && form.circle && - form.contributionTier && + Number.isInteger(form.contributionAmount) && form.contributionAmount >= 0 && form.agreedToGuidelines ); }); // Check if payment is required const needsPayment = computed(() => { - return requiresPayment(form.contributionTier); + return requiresPayment(form.contributionAmount); }); -// Get selected tier info -const selectedTier = computed(() => { - return getContributionTierByValue(form.contributionTier); -}); +const guidanceLabel = computed(() => getGuidanceLabel(form.contributionAmount)); const flowStepLabel = computed(() => { switch (flowState.value) { @@ -546,7 +535,7 @@ const handleSubmit = async () => { name: form.name, email: form.email, circle: form.circle, - contributionTier: form.contributionTier, + contributionAmount: form.contributionAmount, agreedToGuidelines: form.agreedToGuidelines, billingAddress: form.billingAddress, }, @@ -617,7 +606,7 @@ const createSubscription = async (cardToken = null) => { body: { customerId: customerId.value, customerCode: customerCode.value, - contributionTier: form.contributionTier, + contributionAmount: form.contributionAmount, cadence: cadence.value, cardToken: cardToken, }, @@ -883,6 +872,52 @@ onUnmounted(() => { gap: 10px; } +/* ---- CONTRIBUTION AMOUNT INPUT + CHIPS ---- */ +.contribution-input-row { + display: flex; + align-items: center; + gap: 0.25rem; +} +.contribution-currency { + font-weight: 600; +} +.contribution-input { + flex: 1; + padding: 0.5rem 0.75rem; + background: var(--input-bg); + border: 1px solid var(--parch); + font-family: 'Commit Mono', monospace; + font-size: 1rem; +} +.contribution-input:focus { + outline: none; + border-color: var(--candle); +} +.contribution-presets { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.5rem; +} +.contribution-preset-chip { + padding: 0.25rem 0.75rem; + background: transparent; + border: 1px dashed var(--parch); + font-family: 'Commit Mono', monospace; + font-size: 0.875rem; + cursor: pointer; +} +.contribution-preset-chip:hover { + border-style: solid; + border-color: var(--candle); +} +.contribution-guidance { + margin-top: 0.5rem; + font-size: 0.875rem; + font-style: italic; + color: var(--ink-soft, currentColor); +} + /* ---- CIRCLE RADIOS ---- */ .circle-radios { display: grid;