- 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
176 lines
4.5 KiB
Vue
176 lines
4.5 KiB
Vue
<template>
|
|
<ClientOnly>
|
|
<div v-if="!loading" class="onboarding-widget">
|
|
<!-- Welcome mode: onboarding in progress -->
|
|
<template v-if="!isComplete">
|
|
<div class="ow-prompt">> welcome</div>
|
|
<div class="ow-message">You are in the <strong>Ghost Guild</strong>. A few passages remain unexplored.</div>
|
|
<div class="ow-hint">Next: {{ currentSuggestion.text }}</div>
|
|
<NuxtLink
|
|
v-if="currentSuggestion.action && !currentSuggestion.isExternal"
|
|
:to="currentSuggestion.action"
|
|
class="ow-action"
|
|
>
|
|
{{ currentSuggestion.actionText }} →
|
|
</NuxtLink>
|
|
<a
|
|
v-else-if="currentSuggestion.isExternal"
|
|
:href="currentSuggestion.action"
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="ow-action"
|
|
@click="trackGoal('wikiClicked')"
|
|
>
|
|
{{ currentSuggestion.actionText }} →
|
|
</a>
|
|
<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>
|
|
|
|
<!-- Suggestion mode: onboarding complete -->
|
|
<template v-else>
|
|
<!-- Empty state -->
|
|
<div v-if="currentSuggestion.key === 'empty'" class="ow-prompt">> look</div>
|
|
<div v-if="currentSuggestion.key === 'empty'" class="ow-message ow-message--dim">{{ currentSuggestion.text }}</div>
|
|
|
|
<!-- Recommendation (event, board, or wiki) -->
|
|
<template v-if="currentSuggestion.key !== 'empty'">
|
|
<div class="ow-prompt">> look</div>
|
|
<div class="ow-message">{{ currentSuggestion.text }}</div>
|
|
<a
|
|
v-if="currentSuggestion.isExternal && currentSuggestion.action"
|
|
:href="currentSuggestion.action"
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="ow-action"
|
|
>
|
|
{{ currentSuggestion.actionText }} →
|
|
</a>
|
|
<NuxtLink
|
|
v-else-if="currentSuggestion.action"
|
|
:to="currentSuggestion.action"
|
|
class="ow-action"
|
|
>
|
|
{{ currentSuggestion.actionText }} →
|
|
</NuxtLink>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
</ClientOnly>
|
|
</template>
|
|
|
|
<script setup>
|
|
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
|
|
return [g.hasProfileTags, g.hasVisitedEvent, g.hasEngagedBoard, g.hasClickedWiki]
|
|
.filter(Boolean).length
|
|
})
|
|
|
|
const barFill = computed(() => '[' + '#'.repeat(completedCount.value * 2))
|
|
const barEmpty = computed(() => '-'.repeat((4 - completedCount.value) * 2) + ']')
|
|
</script>
|
|
|
|
<style scoped>
|
|
.onboarding-widget {
|
|
padding: 16px 20px;
|
|
border-bottom: 1px dashed var(--parch-border);
|
|
background: var(--parch);
|
|
color: var(--parch-text);
|
|
font-size: 12px;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.ow-prompt {
|
|
color: var(--parch-accent);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.ow-message {
|
|
color: var(--parch-text);
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.ow-message--dim {
|
|
color: var(--parch-text-dim);
|
|
}
|
|
|
|
.ow-hint {
|
|
color: var(--parch-text-dim);
|
|
font-size: 11px;
|
|
}
|
|
|
|
.ow-action {
|
|
display: inline-block;
|
|
margin-top: 8px;
|
|
padding: 4px 12px;
|
|
border: 1px dashed rgba(237, 228, 208, 0.25);
|
|
color: var(--parch-accent);
|
|
font-size: 11px;
|
|
text-decoration: none;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.ow-action:hover {
|
|
border-color: var(--parch-accent);
|
|
border-style: solid;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.ow-progress {
|
|
margin-top: 10px;
|
|
padding-top: 8px;
|
|
border-top: 1px dashed rgba(237, 228, 208, 0.12);
|
|
font-size: 11px;
|
|
color: var(--parch-text-dim);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.ow-bar {
|
|
display: inline-flex;
|
|
gap: 0;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.ow-bar-fill {
|
|
color: var(--parch-accent);
|
|
}
|
|
|
|
.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>
|