feat(member): account/profile polish + tier upgrade flow
- Timezone: curated USelectMenu dropdown (app/config/timezones.js), preserves unknown saved values
- Profile save now uses useToast() for success/error; remove inline save banner
- Nav onboarding dot nudged down 1px for optical alignment with lowercase text
- Onboarding: skip a suggestion with POST /api/onboarding/track {skip}; member.onboarding.skipped map; does not affect graduation
- CirclePicker takes :saved-value so 'Current' badge stays until save completes
- PrivacyToggle is binary (USwitch labeled Private); member schema enum reduced to ['members','private']; zod coerces legacy 'public'
- New /member/payment-setup page: HelcimPay $0 verify + update-contribution, wired from account.vue via requiresPaymentSetup redirect
- Helcim portal: NUXT_PUBLIC_HELCIM_PORTAL_URL env + account.vue 'Manage billing in Helcim' link
- Migration script: scripts/migrate-privacy-public-to-members.js
This commit is contained in:
parent
08fc3884da
commit
7292b11c0b
18 changed files with 604 additions and 122 deletions
|
|
@ -339,5 +339,6 @@ const exploreItems = [
|
|||
background: var(--candle);
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,16 @@
|
|||
v-for="circle in circles"
|
||||
:key="circle.value"
|
||||
class="circle-option"
|
||||
:class="{ current: modelValue === circle.value }"
|
||||
:class="{
|
||||
selected: modelValue === circle.value,
|
||||
current: savedValue === circle.value,
|
||||
}"
|
||||
@click="$emit('update:modelValue', circle.value)"
|
||||
>
|
||||
<span class="circle-name">{{ circle.label }}</span>
|
||||
<span class="circle-desc">{{ circle.description }}</span>
|
||||
<span
|
||||
v-if="modelValue === circle.value"
|
||||
v-if="savedValue === circle.value"
|
||||
class="circle-tag"
|
||||
:style="{ color: `var(--c-${circle.value})`, borderColor: `var(--c-${circle.value})` }"
|
||||
>Current</span>
|
||||
|
|
@ -21,6 +24,7 @@
|
|||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
savedValue: { type: String, default: '' },
|
||||
circles: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
|
|
@ -54,7 +58,7 @@ defineEmits(['update:modelValue'])
|
|||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.circle-option.current {
|
||||
.circle-option.selected {
|
||||
border-color: var(--candle);
|
||||
border-style: solid;
|
||||
background: var(--surface);
|
||||
|
|
@ -67,7 +71,7 @@ defineEmits(['update:modelValue'])
|
|||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.circle-option.current .circle-name {
|
||||
.circle-option.selected .circle-name {
|
||||
color: var(--candle);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@
|
|||
<div class="ow-progress">
|
||||
<span class="ow-bar"><span class="ow-bar-fill">{{ barFill }}</span><span class="ow-bar-empty">{{ barEmpty }}</span></span>
|
||||
{{ completedCount }} of 4 explored
|
||||
<button
|
||||
v-if="currentSuggestion.key"
|
||||
type="button"
|
||||
class="ow-skip"
|
||||
@click="handleSkip"
|
||||
>Skip this</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -62,7 +68,12 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
const { goals, isComplete, currentSuggestion, trackGoal, loading } = useOnboarding()
|
||||
const { goals, isComplete, currentSuggestion, trackGoal, skipSuggestion, loading } = useOnboarding()
|
||||
|
||||
const handleSkip = () => {
|
||||
const key = currentSuggestion.value?.key
|
||||
if (key) skipSuggestion(key)
|
||||
}
|
||||
|
||||
const completedCount = computed(() => {
|
||||
const g = goals.value
|
||||
|
|
@ -144,4 +155,22 @@ const barEmpty = computed(() => '-'.repeat((4 - completedCount.value) * 2) + ']'
|
|||
.ow-bar-empty {
|
||||
color: rgba(237, 228, 208, 0.2);
|
||||
}
|
||||
|
||||
.ow-skip {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--parch-text-dim);
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.ow-skip:hover {
|
||||
color: var(--parch-accent);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,66 +1,44 @@
|
|||
<template>
|
||||
<div class="priv segmented">
|
||||
<span
|
||||
v-for="opt in options"
|
||||
:key="opt.value"
|
||||
:class="{ on: modelValue === opt.value }"
|
||||
@click="$emit('update:modelValue', opt.value)"
|
||||
>{{ opt.label }}</span
|
||||
>
|
||||
</div>
|
||||
<label class="priv">
|
||||
<USwitch
|
||||
:model-value="isPrivate"
|
||||
aria-label="Private"
|
||||
size="xs"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
<span class="priv-label">Private</span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: String, default: "public" },
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: "members" },
|
||||
});
|
||||
|
||||
defineEmits(["update:modelValue"]);
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const options = [
|
||||
{ label: "Public", value: "public" },
|
||||
{ label: "Members", value: "members" },
|
||||
{ label: "Private", value: "private" },
|
||||
];
|
||||
// Treat legacy "public" values as "members" (visible to signed-in members).
|
||||
const isPrivate = computed(() => props.modelValue === "private");
|
||||
|
||||
const onChange = (val) => {
|
||||
emit("update:modelValue", val ? "private" : "members");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.priv {
|
||||
display: inline-flex;
|
||||
gap: 0;
|
||||
font-size: 9px;
|
||||
font-family: "Commit Mono", monospace;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.priv span {
|
||||
padding: 2px 7px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed var(--border);
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
font-family: "Commit Mono", monospace;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.priv span + span {
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.priv span:hover {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.priv span.on {
|
||||
background: var(--surface);
|
||||
color: var(--text-bright);
|
||||
border-color: var(--candle);
|
||||
border-style: solid;
|
||||
z-index: 1;
|
||||
.priv-label {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue