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.
This commit is contained in:
Jennie Robinson Faber 2026-05-23 15:01:54 +01:00
parent f28558a433
commit 26ee1ca60d

View file

@ -129,7 +129,7 @@
type="text"
placeholder="Your name"
required
/>
>
</div>
<div class="form-group">
<label class="form-label" for="join-email">Email Address</label>
@ -140,7 +140,7 @@
type="email"
placeholder="you@example.com"
required
/>
>
</div>
<div class="form-group">
<label class="form-label">Circle</label>
@ -152,7 +152,7 @@
type="radio"
name="circle"
value="community"
/>
>
<label for="circle-community">
<span
class="circle-label-name"
@ -169,7 +169,7 @@
type="radio"
name="circle"
value="founder"
/>
>
<label for="circle-founder">
<span
class="circle-label-name"
@ -186,7 +186,7 @@
type="radio"
name="circle"
value="practitioner"
/>
>
<label for="circle-practitioner">
<span
class="circle-label-name"
@ -198,90 +198,13 @@
</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">Per Month</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">Per Year</span>
</label>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label" for="join-contribution">
Monthly Contribution
</label>
<div class="contribution-input-row">
<span class="contribution-currency">$</span>
<input
id="join-contribution"
v-model.number="form.contributionAmount"
type="number"
min="0"
step="1"
inputmode="numeric"
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 v-if="form.contributionAmount > 0" class="form-group">
<div class="billing-summary">
<p class="billing-summary-line">
You'll be charged <strong>${{ firstCharge }} today</strong
><span v-if="cadence === 'annual'">
(${{ form.contributionAmount }}/month &times; 12)</span
>.
</p>
<p class="billing-summary-line">
Then
<strong
>${{ firstCharge }} every
{{ cadence === "annual" ? "year" : "month" }}</strong
>, until you cancel.
</p>
</div>
</div>
<ContributionAmountField
v-model="form.contributionAmount"
v-model:cadence="cadence"
/>
<div class="form-group full-width">
<label class="checkbox-label">
<input v-model="form.agreedToGuidelines" type="checkbox" />
<input v-model="form.agreedToGuidelines" type="checkbox" >
<span>
I agree to the Ghost Guild
<NuxtLink to="/community-guidelines" target="_blank"
@ -385,11 +308,7 @@
<script setup>
import { reactive, ref, computed, onMounted, onUnmounted } from "vue";
import { getCircleOptions } from "~/config/circles";
import {
requiresPayment,
CONTRIBUTION_PRESETS,
getGuidanceLabel,
} from "~/config/contributions";
import { requiresPayment } from "~/config/contributions";
useSiteMeta({
title: "Join",
@ -475,13 +394,6 @@ const needsPayment = computed(() => {
return requiresPayment(form.contributionAmount);
});
const guidanceLabel = computed(() => getGuidanceLabel(form.contributionAmount));
const firstCharge = computed(() => {
const amount = form.contributionAmount || 0;
return cadence.value === "annual" ? amount * 12 : amount;
});
const flowSummary = computed(() => ({
name: form.name,
email: form.email,
@ -840,79 +752,6 @@ onUnmounted(() => {
color: var(--text-faint);
}
/* ---- CADENCE RADIOS ---- */
.cadence-radios {
display: grid;
grid-template-columns: repeat(2, 1fr);
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);
}
/* ---- BILLING SUMMARY ---- */
.billing-summary {
padding: 12px 16px;
border: 1px dashed var(--border);
background: var(--surface);
}
.billing-summary-line {
font-size: 13px;
color: var(--text);
line-height: 1.5;
margin: 0;
}
.billing-summary-line + .billing-summary-line {
margin-top: 4px;
}
.billing-summary-line strong {
color: var(--text-bright);
font-weight: 600;
}
/* ---- CIRCLE RADIOS ---- */
.circle-radios {
display: grid;