feat(join): replace tier dropdown with amount input + guidance chips

This commit is contained in:
Jennie Robinson Faber 2026-04-19 18:59:24 +01:00
parent 50a1ffe735
commit 4d10c4e0a2

View file

@ -32,7 +32,7 @@
<DashedBox :hoverable="false"> <DashedBox :hoverable="false">
<div class="section-label">Contribution</div> <div class="section-label">Contribution</div>
<div class="info-value"> <div class="info-value">
${{ memberData?.contributionTier || "0" }} CAD/month ${{ memberData?.contributionAmount ?? 0 }} CAD/month
</div> </div>
</DashedBox> </DashedBox>
</div> </div>
@ -69,14 +69,14 @@
<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">{{ formatTierAmount('5') }}</span> I can contribute</li> <li><span class="tier-amt">{{ formatContributionAmount(5) }}</span> I can contribute</li>
<li> <li>
<span class="tier-amt">{{ formatTierAmount('15') }}</span> I can sustain the community <span class="tier-amt">{{ formatContributionAmount(15) }}</span> I can sustain the community
(suggested) (suggested)
</li> </li>
<li><span class="tier-amt">{{ formatTierAmount('30') }}</span> I can support others too</li> <li><span class="tier-amt">{{ formatContributionAmount(30) }}</span> I can support others too</li>
<li> <li>
<span class="tier-amt">{{ formatTierAmount('50') }}</span> I want to sponsor multiple <span class="tier-amt">{{ formatContributionAmount(50) }}</span> I want to sponsor multiple
members members
</li> </li>
</ul> </ul>
@ -207,27 +207,38 @@
> >
<label for="cadence-annual"> <label for="cadence-annual">
<span class="circle-label-name">Annual</span> <span class="circle-label-name">Annual</span>
<span class="circle-label-desc">2 months free</span>
</label> </label>
</div> </div>
</div> </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">
>{{ cadence === 'annual' ? 'Annual' : 'Monthly' }} Contribution</label {{ cadence === 'annual' ? 'Annual' : 'Monthly' }} Contribution
> </label>
<USelectMenu <div class="contribution-input-row">
id="join-contribution" <span class="contribution-currency">$</span>
v-model="form.contributionTier" <input
:items="contributionItems" id="join-contribution"
value-key="value" v-model.number="form.contributionAmount"
:search-input="false" type="number"
class="zine-select" min="0"
:ui="{ step="1"
content: 'tz-content', inputmode="numeric"
item: 'tz-item', class="contribution-input"
}" >
/> </div>
<div class="contribution-presets" role="group" aria-label="Suggested amounts">
<button
v-for="preset in CONTRIBUTION_PRESETS"
:key="preset.amount"
type="button"
class="contribution-preset-chip"
@click="form.contributionAmount = preset.amount"
>
${{ preset.amount }}
</button>
</div>
<p v-if="guidanceLabel" class="contribution-guidance">{{ guidanceLabel }}</p>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label class="checkbox-label"> <label class="checkbox-label">
@ -371,7 +382,7 @@
<dt>Circle</dt><dd class="capitalize">{{ form.circle }}</dd> <dt>Circle</dt><dd class="capitalize">{{ form.circle }}</dd>
</div> </div>
<div class="details-row"> <div class="details-row">
<dt>Contribution</dt><dd>{{ selectedTier.label }}</dd> <dt>Contribution</dt><dd>{{ formatContributionAmount(form.contributionAmount) }}</dd>
</div> </div>
</dl> </dl>
</DashedBox> </DashedBox>
@ -409,9 +420,8 @@ import { reactive, ref, computed, onMounted, onUnmounted } from "vue";
import { getCircleOptions } from "~/config/circles"; import { getCircleOptions } from "~/config/circles";
import { import {
requiresPayment, requiresPayment,
getContributionTierByValue, CONTRIBUTION_PRESETS,
getTierAmount, getGuidanceLabel,
CONTRIBUTION_TIERS,
} from "~/config/contributions"; } from "~/config/contributions";
// Auth state // Auth state
@ -427,7 +437,7 @@ const form = reactive({
email: "", email: "",
name: "", name: "",
circle: "community", circle: "community",
contributionTier: "15", contributionAmount: 15,
agreedToGuidelines: false, agreedToGuidelines: false,
billingAddress: { billingAddress: {
street: "", street: "",
@ -461,29 +471,11 @@ const paymentToken = ref(null);
// Circle options from central config // Circle options from central config
const circleOptions = getCircleOptions(); const circleOptions = getCircleOptions();
// Minimal labels for the dropdown reactive to cadence. const formatContributionAmount = (amount) => {
// In annual mode, show both monthly and annual price so $50/yr (the $5 tier annual) if (!amount || amount === 0) return "$0";
// is visually distinct from $500/yr (the $50 tier annual). const display = cadence.value === "annual" ? amount * 12 : amount;
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 suffix = cadence.value === "annual" ? "/yr" : "/mo"; const suffix = cadence.value === "annual" ? "/yr" : "/mo";
return `$${amt}${suffix}`; return `$${display}${suffix}`;
}; };
// Initialize composables // Initialize composables
@ -499,20 +491,17 @@ const isFormValid = computed(() => {
form.name && form.name &&
form.email && form.email &&
form.circle && form.circle &&
form.contributionTier && Number.isInteger(form.contributionAmount) && form.contributionAmount >= 0 &&
form.agreedToGuidelines form.agreedToGuidelines
); );
}); });
// Check if payment is required // Check if payment is required
const needsPayment = computed(() => { const needsPayment = computed(() => {
return requiresPayment(form.contributionTier); return requiresPayment(form.contributionAmount);
}); });
// Get selected tier info const guidanceLabel = computed(() => getGuidanceLabel(form.contributionAmount));
const selectedTier = computed(() => {
return getContributionTierByValue(form.contributionTier);
});
const flowStepLabel = computed(() => { const flowStepLabel = computed(() => {
switch (flowState.value) { switch (flowState.value) {
@ -546,7 +535,7 @@ const handleSubmit = async () => {
name: form.name, name: form.name,
email: form.email, email: form.email,
circle: form.circle, circle: form.circle,
contributionTier: form.contributionTier, contributionAmount: form.contributionAmount,
agreedToGuidelines: form.agreedToGuidelines, agreedToGuidelines: form.agreedToGuidelines,
billingAddress: form.billingAddress, billingAddress: form.billingAddress,
}, },
@ -617,7 +606,7 @@ const createSubscription = async (cardToken = null) => {
body: { body: {
customerId: customerId.value, customerId: customerId.value,
customerCode: customerCode.value, customerCode: customerCode.value,
contributionTier: form.contributionTier, contributionAmount: form.contributionAmount,
cadence: cadence.value, cadence: cadence.value,
cardToken: cardToken, cardToken: cardToken,
}, },
@ -883,6 +872,52 @@ onUnmounted(() => {
gap: 10px; 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 ---- */
.circle-radios { .circle-radios {
display: grid; display: grid;