feat(account): replace tier control with amount input + guidance chips
This commit is contained in:
parent
4d10c4e0a2
commit
5ef0cc845f
1 changed files with 90 additions and 34 deletions
|
|
@ -236,14 +236,42 @@
|
|||
<PageSection>
|
||||
<div class="section-label">Change Contribution</div>
|
||||
|
||||
<TierPicker v-model="selectedTier" :tiers="tiers" />
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="account-contribution">
|
||||
{{ cadence === 'annual' ? 'Annual' : 'Monthly' }} Contribution
|
||||
</label>
|
||||
<div class="contribution-input-row">
|
||||
<span class="contribution-currency">$</span>
|
||||
<input
|
||||
id="account-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 class="tier-hint">
|
||||
Changes take effect on your next billing cycle
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary btn-section"
|
||||
:disabled="
|
||||
selectedTier === Number(memberData.contributionTier || 0) ||
|
||||
form.contributionAmount === Number(memberData.contributionAmount || 0) ||
|
||||
isUpdating
|
||||
"
|
||||
@click="handleUpdateTier"
|
||||
|
|
@ -277,7 +305,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getTierAmount, getContributionTierByValue } from '~/config/contributions';
|
||||
import { CONTRIBUTION_PRESETS, getGuidanceLabel, requiresPayment } from '~/config/contributions';
|
||||
|
||||
definePageMeta({
|
||||
middleware: "auth",
|
||||
|
|
@ -289,7 +317,7 @@ const { initializeHelcimPay, verifyPayment, cleanup: cleanupHelcimPay } = useHel
|
|||
const toast = useToast();
|
||||
const helcimPortalUrl = useRuntimeConfig().public.helcimPortalUrl || '';
|
||||
|
||||
const selectedTier = ref(0);
|
||||
const form = reactive({ contributionAmount: 0 });
|
||||
const selectedCircle = ref("");
|
||||
const isUpdating = ref(false);
|
||||
const isCancelling = ref(false);
|
||||
|
|
@ -315,37 +343,19 @@ const canChangeCard = computed(() => {
|
|||
if (!m.helcimCustomerId) return false;
|
||||
if (!["active", "pending_payment"].includes(m.status)) return false;
|
||||
// $0 tier has no subscription to attach a card to
|
||||
if (String(m.contributionTier || "0") === "0") return false;
|
||||
if (!requiresPayment(Number(m.contributionAmount || 0))) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const BASE_TIERS = [
|
||||
{ amount: 0, label: "I need support right now" },
|
||||
{ amount: 5, label: "I can contribute" },
|
||||
{ amount: 15, label: "I can sustain the community" },
|
||||
{ amount: 30, label: "I can support others too" },
|
||||
{ amount: 50, label: "I want to sponsor multiple members" },
|
||||
];
|
||||
const cadence = computed(() => memberData.value?.billingCadence || 'monthly');
|
||||
|
||||
const tiers = computed(() => {
|
||||
const cadence = memberData.value?.billingCadence || 'monthly';
|
||||
return BASE_TIERS.map((t) => {
|
||||
if (t.amount === 0) return { ...t, display: '$0' };
|
||||
const suffix = cadence === 'annual' ? '/yr' : '/mo';
|
||||
return {
|
||||
...t,
|
||||
display: `$${getTierAmount(t, cadence)}${suffix}`,
|
||||
subtitle: cadence === 'annual' ? `$${t.amount}/mo tier` : null,
|
||||
};
|
||||
});
|
||||
});
|
||||
const guidanceLabel = computed(() => getGuidanceLabel(form.contributionAmount));
|
||||
|
||||
const currentContributionLabel = computed(() => {
|
||||
const tier = getContributionTierByValue(String(memberData.value?.contributionTier || '0'));
|
||||
const cadence = memberData.value?.billingCadence || 'monthly';
|
||||
if (!tier || tier.amount === 0) return '$0';
|
||||
const amount = getTierAmount(tier, cadence);
|
||||
return cadence === 'annual' ? `$${amount} / year` : `$${amount} / month`;
|
||||
const amount = Number(memberData.value?.contributionAmount || 0);
|
||||
if (!amount) return '$0';
|
||||
const displayAmount = cadence.value === 'annual' ? amount * 12 : amount;
|
||||
return cadence.value === 'annual' ? `$${displayAmount} / year` : `$${displayAmount} / month`;
|
||||
});
|
||||
|
||||
const circleOptions = [
|
||||
|
|
@ -380,7 +390,7 @@ const capitalise = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
|
|||
// Initialize from member data
|
||||
watchEffect(() => {
|
||||
if (memberData.value) {
|
||||
selectedTier.value = Number(memberData.value.contributionTier || 0);
|
||||
form.contributionAmount = Number(memberData.value.contributionAmount || 0);
|
||||
selectedCircle.value = memberData.value.circle || "community";
|
||||
}
|
||||
});
|
||||
|
|
@ -399,8 +409,8 @@ const handleUpdateTier = async () => {
|
|||
await $fetch("/api/members/update-contribution", {
|
||||
method: "POST",
|
||||
body: {
|
||||
contributionTier: String(selectedTier.value),
|
||||
cadence: memberData.value?.billingCadence || 'monthly',
|
||||
contributionAmount: form.contributionAmount,
|
||||
cadence: cadence.value,
|
||||
},
|
||||
});
|
||||
await checkMemberStatus();
|
||||
|
|
@ -409,13 +419,13 @@ const handleUpdateTier = async () => {
|
|||
// Paid upgrade without a saved card — route to payment setup instead of erroring.
|
||||
if (err.data?.data?.requiresPaymentSetup) {
|
||||
await navigateTo(
|
||||
`/member/payment-setup?tier=${selectedTier.value}&circle=${
|
||||
`/member/payment-setup?tier=${form.contributionAmount}&circle=${
|
||||
selectedCircle.value || memberData.value?.circle || 'community'
|
||||
}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
selectedTier.value = Number(memberData.value?.contributionTier || 0);
|
||||
form.contributionAmount = Number(memberData.value?.contributionAmount || 0);
|
||||
toast.add({
|
||||
title: "Update failed",
|
||||
description: err.data?.statusMessage || "Please try again.",
|
||||
|
|
@ -797,6 +807,52 @@ const confirmCancelMembership = async () => {
|
|||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* ---- 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);
|
||||
}
|
||||
|
||||
.btn-section {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue