feat(join): cadence selector with annual pricing (monthly×10)

Radio-pair cadence selector (Monthly / Annual) added to the join form,
reusing the existing .circle-radio styling. contributionItems computed
reactively; all tier labels and the left-column price list update on
toggle. cadence submitted with the subscription payload. payment-setup
hardcoded to monthly (annual upgrades go through /join).
This commit is contained in:
Jennie Robinson Faber 2026-04-18 17:59:10 +01:00
parent 0eeed94772
commit cd0d3f7167
2 changed files with 61 additions and 15 deletions

View file

@ -116,19 +116,19 @@
<!-- Left: Monthly Contribution --> <!-- Left: Monthly Contribution -->
<div class="join-col"> <div class="join-col">
<div class="section-label" style="margin-bottom: 12px"> <div class="section-label" style="margin-bottom: 12px">
Monthly Contribution {{ cadence === 'annual' ? 'Annual Contribution' : 'Monthly Contribution' }}
</div> </div>
<h2>Pay what you can</h2> <h2>Pay what you can</h2>
<ul class="tier-list"> <ul class="tier-list">
<li><span class="tier-amt">$0</span> I need support right now</li> <li><span class="tier-amt">$0</span> I need support right now</li>
<li><span class="tier-amt">$5</span> I can contribute</li> <li><span class="tier-amt">{{ cadence === 'annual' ? '$50/yr' : '$5/mo' }}</span> I can contribute</li>
<li> <li>
<span class="tier-amt">$15</span> I can sustain the community <span class="tier-amt">{{ cadence === 'annual' ? '$150/yr' : '$15/mo' }}</span> I can sustain the community
(suggested) (suggested)
</li> </li>
<li><span class="tier-amt">$30</span> I can support others too</li> <li><span class="tier-amt">{{ cadence === 'annual' ? '$300/yr' : '$30/mo' }}</span> I can support others too</li>
<li> <li>
<span class="tier-amt">$50</span> I want to sponsor multiple <span class="tier-amt">{{ cadence === 'annual' ? '$500/yr' : '$50/mo' }}</span> I want to sponsor multiple
members members
</li> </li>
</ul> </ul>
@ -234,9 +234,39 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<label class="form-label">Billing Cadence</label>
<div class="cadence-radios">
<div class="circle-radio">
<input
id="cadence-monthly"
v-model="cadence"
type="radio"
name="cadence"
value="monthly"
>
<label for="cadence-monthly">
<span class="circle-label-name">Monthly</span>
</label>
</div>
<div class="circle-radio">
<input
id="cadence-annual"
v-model="cadence"
type="radio"
name="cadence"
value="annual"
>
<label for="cadence-annual">
<span class="circle-label-name">Annual</span>
<span class="circle-label-desc">2 months free</span>
</label>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="form-label" for="join-contribution" <label class="form-label" for="join-contribution"
>Monthly Contribution</label >{{ cadence === 'annual' ? 'Annual' : 'Monthly' }} Contribution</label
> >
<USelectMenu <USelectMenu
id="join-contribution" id="join-contribution"
@ -393,6 +423,8 @@ import {
getContributionOptions, getContributionOptions,
requiresPayment, requiresPayment,
getContributionTierByValue, getContributionTierByValue,
getTierAmount,
CONTRIBUTION_TIERS,
} from "~/config/contributions"; } from "~/config/contributions";
// Auth state // Auth state
@ -424,6 +456,7 @@ const isSubmitting = ref(false);
const currentStep = ref(1); // 1: Info, 2: Billing (paid only), 3: Payment, 4: Confirmation const currentStep = ref(1); // 1: Info, 2: Billing (paid only), 3: Payment, 4: Confirmation
const errorMessage = ref(""); const errorMessage = ref("");
const successMessage = ref(""); const successMessage = ref("");
const cadence = ref("monthly"); // 'monthly' | 'annual'
// Helcim state // Helcim state
const customerId = ref(null); const customerId = ref(null);
@ -437,14 +470,18 @@ const circleOptions = getCircleOptions();
// Contribution options from central config // Contribution options from central config
const contributionOptions = getContributionOptions(); const contributionOptions = getContributionOptions();
// Minimal labels for the dropdown (tier descriptions live in the left column). // Minimal labels for the dropdown reactive to cadence.
const contributionItems = [ const contributionItems = computed(() => {
{ value: "0", label: "$0/mo" }, const isAnnual = cadence.value === "annual";
{ value: "5", label: "$5/mo" }, return Object.values(CONTRIBUTION_TIERS).map((tier) => {
{ value: "15", label: "$15/mo (suggested)" }, const base = tier.amount;
{ value: "30", label: "$30/mo" }, if (base === 0) return { value: tier.value, label: "$0" };
{ value: "50", label: "$50/mo" }, const amt = isAnnual ? base * 10 : base;
]; const suffix = isAnnual ? "/yr" : "/mo";
const hint = tier.value === "15" && !isAnnual ? " (suggested)" : "";
return { value: tier.value, label: `$${amt}${suffix}${hint}` };
});
});
// Initialize composables // Initialize composables
const { const {
@ -585,6 +622,7 @@ const createSubscription = async (cardToken = null) => {
customerId: customerId.value, customerId: customerId.value,
customerCode: customerCode.value, customerCode: customerCode.value,
contributionTier: form.contributionTier, contributionTier: form.contributionTier,
cadence: cadence.value,
cardToken: cardToken, cardToken: cardToken,
}, },
}); });
@ -863,6 +901,13 @@ onUnmounted(() => {
color: var(--text-faint); color: var(--text-faint);
} }
/* ---- CADENCE RADIOS ---- */
.cadence-radios {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
/* ---- CIRCLE RADIOS ---- */ /* ---- CIRCLE RADIOS ---- */
.circle-radios { .circle-radios {
display: grid; display: grid;

View file

@ -128,7 +128,8 @@ const openModal = async () => {
await $fetch('/api/members/update-contribution', { await $fetch('/api/members/update-contribution', {
method: 'POST', method: 'POST',
body: { contributionTier: targetTier.value }, // cadence: annual upgrades go through /join; this page is monthly-only
body: { contributionTier: targetTier.value, cadence: 'monthly' },
}); });
await checkMemberStatus(); await checkMemberStatus();