From 81783866d1326b1862fd5e4a68d396a2222af050 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sat, 23 May 2026 14:53:47 +0100 Subject: [PATCH 01/21] feat(contribution): extract ContributionAmountField component --- app/components/ContributionAmountField.vue | 297 +++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 app/components/ContributionAmountField.vue diff --git a/app/components/ContributionAmountField.vue b/app/components/ContributionAmountField.vue new file mode 100644 index 0000000..64485d6 --- /dev/null +++ b/app/components/ContributionAmountField.vue @@ -0,0 +1,297 @@ + + + + + From f28558a4335f474473110e457c3120807479ba1a Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sat, 23 May 2026 14:58:39 +0100 Subject: [PATCH 02/21] fix(contribution): sanitize amount input and a11y polish --- app/components/ContributionAmountField.vue | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/components/ContributionAmountField.vue b/app/components/ContributionAmountField.vue index 64485d6..2f564f1 100644 --- a/app/components/ContributionAmountField.vue +++ b/app/components/ContributionAmountField.vue @@ -58,6 +58,7 @@ type="button" class="contribution-preset-chip" :class="{ active: numericAmount === preset.amount }" + :aria-pressed="numericAmount === preset.amount" @click="selectPreset(preset.amount)" > ${{ preset.amount }} @@ -97,7 +98,11 @@ import { const props = defineProps({ modelValue: { type: Number, required: true }, - cadence: { type: String, required: true }, + cadence: { + type: String, + required: true, + validator: (v) => v === 'monthly' || v === 'annual', + }, allowCadenceChange: { type: Boolean, default: true }, showSummary: { type: Boolean, default: true }, summaryNote: { type: String, default: '' }, @@ -134,15 +139,14 @@ const onAmountInput = (event) => { return } const n = Number(raw) - emit('update:modelValue', Number.isFinite(n) ? n : 0) + const sanitized = Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 0 + emit('update:modelValue', sanitized) } const selectPreset = (amount) => { emit('update:modelValue', amount) } -// Annual→monthly snap: floor(annual/12)*12 keeps the amount cleanly divisible -// before we divide, so toggling cadences never leaves fractional dollars. const onCadenceChange = (newCadence) => { if (newCadence === props.cadence) return const current = numericAmount.value From 26ee1ca60d73ffd78d55642edb268758ce300d4a Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sat, 23 May 2026 15:01:54 +0100 Subject: [PATCH 03/21] feat(contribution): port join.vue to ContributionAmountField Replace inline cadence radios, contribution input + presets, guidance label, and billing summary with the shared ContributionAmountField component. Removes duplicated state (guidanceLabel, firstCharge), unused imports (CONTRIBUTION_PRESETS, getGuidanceLabel), and the matching CSS rules. The parent retains the cadence ref because formatContributionAmount (left-column tier list) reads it. --- app/pages/join.vue | 183 +++------------------------------------------ 1 file changed, 11 insertions(+), 172 deletions(-) diff --git a/app/pages/join.vue b/app/pages/join.vue index 26a2621..cae35fe 100644 --- a/app/pages/join.vue +++ b/app/pages/join.vue @@ -129,7 +129,7 @@ type="text" placeholder="Your name" required - /> + >
@@ -140,7 +140,7 @@ type="email" placeholder="you@example.com" required - /> + >
@@ -152,7 +152,7 @@ type="radio" name="circle" value="community" - /> + >
-
- -
-
- - -
-
- - -
-
-
-
- -
- $ - -
-
- -
-

- {{ guidanceLabel }} -

-
-
-
-

- You'll be charged ${{ firstCharge }} today - (${{ form.contributionAmount }}/month × 12). -

-

- Then - ${{ firstCharge }} every - {{ cadence === "annual" ? "year" : "month" }}, until you cancel. -

-
-
+
@@ -25,7 +25,7 @@ :checked="cadence === 'annual'" @change="onCadenceChange('annual')" > -