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>
|
<PageSection>
|
||||||
<div class="section-label">Change Contribution</div>
|
<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">
|
<div class="tier-hint">
|
||||||
Changes take effect on your next billing cycle
|
Changes take effect on your next billing cycle
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-section"
|
class="btn btn-primary btn-section"
|
||||||
:disabled="
|
:disabled="
|
||||||
selectedTier === Number(memberData.contributionTier || 0) ||
|
form.contributionAmount === Number(memberData.contributionAmount || 0) ||
|
||||||
isUpdating
|
isUpdating
|
||||||
"
|
"
|
||||||
@click="handleUpdateTier"
|
@click="handleUpdateTier"
|
||||||
|
|
@ -277,7 +305,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { getTierAmount, getContributionTierByValue } from '~/config/contributions';
|
import { CONTRIBUTION_PRESETS, getGuidanceLabel, requiresPayment } from '~/config/contributions';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: "auth",
|
middleware: "auth",
|
||||||
|
|
@ -289,7 +317,7 @@ const { initializeHelcimPay, verifyPayment, cleanup: cleanupHelcimPay } = useHel
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const helcimPortalUrl = useRuntimeConfig().public.helcimPortalUrl || '';
|
const helcimPortalUrl = useRuntimeConfig().public.helcimPortalUrl || '';
|
||||||
|
|
||||||
const selectedTier = ref(0);
|
const form = reactive({ contributionAmount: 0 });
|
||||||
const selectedCircle = ref("");
|
const selectedCircle = ref("");
|
||||||
const isUpdating = ref(false);
|
const isUpdating = ref(false);
|
||||||
const isCancelling = ref(false);
|
const isCancelling = ref(false);
|
||||||
|
|
@ -315,37 +343,19 @@ const canChangeCard = computed(() => {
|
||||||
if (!m.helcimCustomerId) return false;
|
if (!m.helcimCustomerId) return false;
|
||||||
if (!["active", "pending_payment"].includes(m.status)) return false;
|
if (!["active", "pending_payment"].includes(m.status)) return false;
|
||||||
// $0 tier has no subscription to attach a card to
|
// $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;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const BASE_TIERS = [
|
const cadence = computed(() => memberData.value?.billingCadence || 'monthly');
|
||||||
{ 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 tiers = computed(() => {
|
const guidanceLabel = computed(() => getGuidanceLabel(form.contributionAmount));
|
||||||
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 currentContributionLabel = computed(() => {
|
const currentContributionLabel = computed(() => {
|
||||||
const tier = getContributionTierByValue(String(memberData.value?.contributionTier || '0'));
|
const amount = Number(memberData.value?.contributionAmount || 0);
|
||||||
const cadence = memberData.value?.billingCadence || 'monthly';
|
if (!amount) return '$0';
|
||||||
if (!tier || tier.amount === 0) return '$0';
|
const displayAmount = cadence.value === 'annual' ? amount * 12 : amount;
|
||||||
const amount = getTierAmount(tier, cadence);
|
return cadence.value === 'annual' ? `$${displayAmount} / year` : `$${displayAmount} / month`;
|
||||||
return cadence === 'annual' ? `$${amount} / year` : `$${amount} / month`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const circleOptions = [
|
const circleOptions = [
|
||||||
|
|
@ -380,7 +390,7 @@ const capitalise = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
|
||||||
// Initialize from member data
|
// Initialize from member data
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (memberData.value) {
|
if (memberData.value) {
|
||||||
selectedTier.value = Number(memberData.value.contributionTier || 0);
|
form.contributionAmount = Number(memberData.value.contributionAmount || 0);
|
||||||
selectedCircle.value = memberData.value.circle || "community";
|
selectedCircle.value = memberData.value.circle || "community";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -399,8 +409,8 @@ const handleUpdateTier = async () => {
|
||||||
await $fetch("/api/members/update-contribution", {
|
await $fetch("/api/members/update-contribution", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
contributionTier: String(selectedTier.value),
|
contributionAmount: form.contributionAmount,
|
||||||
cadence: memberData.value?.billingCadence || 'monthly',
|
cadence: cadence.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await checkMemberStatus();
|
await checkMemberStatus();
|
||||||
|
|
@ -409,13 +419,13 @@ const handleUpdateTier = async () => {
|
||||||
// Paid upgrade without a saved card — route to payment setup instead of erroring.
|
// Paid upgrade without a saved card — route to payment setup instead of erroring.
|
||||||
if (err.data?.data?.requiresPaymentSetup) {
|
if (err.data?.data?.requiresPaymentSetup) {
|
||||||
await navigateTo(
|
await navigateTo(
|
||||||
`/member/payment-setup?tier=${selectedTier.value}&circle=${
|
`/member/payment-setup?tier=${form.contributionAmount}&circle=${
|
||||||
selectedCircle.value || memberData.value?.circle || 'community'
|
selectedCircle.value || memberData.value?.circle || 'community'
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectedTier.value = Number(memberData.value?.contributionTier || 0);
|
form.contributionAmount = Number(memberData.value?.contributionAmount || 0);
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Update failed",
|
title: "Update failed",
|
||||||
description: err.data?.statusMessage || "Please try again.",
|
description: err.data?.statusMessage || "Please try again.",
|
||||||
|
|
@ -797,6 +807,52 @@ const confirmCancelMembership = async () => {
|
||||||
margin-bottom: 12px;
|
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 {
|
.btn-section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue