/** * Onboarding Composable * Tracks new member onboarding goals and provides post-graduation suggestions. */ export function useOnboarding(options = {}) { const goals = useState('onboarding.goals', () => ({ hasProfileTags: false, hasVisitedEvent: false, hasEngagedBoard: false, hasClickedWiki: false, })) const completedAt = useState('onboarding.completedAt', () => null) const loading = useState('onboarding.loading', () => false) const recommendations = useState('onboarding.recommendations', () => ({ events: [], wiki: [], })) // Track whether we've already fetched status this session const _fetched = useState('onboarding._fetched', () => false) const isComplete = computed(() => !!completedAt.value || (goals.value.hasProfileTags && goals.value.hasVisitedEvent && goals.value.hasEngagedBoard && goals.value.hasClickedWiki) ) const pickCategory = options.pickCategory || ((categories) => { return categories[Math.floor(Math.random() * categories.length)] }) const currentSuggestion = computed(() => { // Not graduated — return highest-priority incomplete goal if (!isComplete.value) { if (!goals.value.hasProfileTags) { return { key: 'profileTags', text: 'Complete your profile by adding your craft and community tags', action: '/member/profile', actionText: 'Set up tags', } } if (!goals.value.hasVisitedEvent) { return { key: 'visitEvent', text: 'Check out upcoming events', action: '/events', actionText: 'Browse events', } } if (!goals.value.hasEngagedBoard) { return { key: 'board', text: 'Explore the board to find collaborators', action: '/board', actionText: 'Explore board', } } if (!goals.value.hasClickedWiki) { return { key: 'wiki', text: 'Browse the wiki for resources and guides', action: 'https://wiki.ghostguild.org', actionText: 'Browse wiki', isExternal: true, } } } // Graduated — suggestion mode const cats = ['events', 'wiki'].filter( (c) => recommendations.value[c]?.length > 0 ) if (cats.length === 0) { return { key: 'empty', text: 'No suggestions right now' } } const selected = pickCategory(cats) const items = recommendations.value[selected] if (items?.length > 0) { return buildRecommendation(selected, items[0]) } return { key: 'empty', text: 'No suggestions right now' } }) function buildRecommendation(category, item) { if (category === 'events') { return { key: 'event', text: `Upcoming event: ${item.title}`, action: `/events/${item.slug}`, actionText: 'View event', } } if (category === 'wiki') { return { key: 'wiki', text: `Recommended: ${item.title}`, action: item.url || null, actionText: 'Read article', isExternal: true, } } return { key: 'empty', text: 'No suggestions right now' } } async function fetchStatus() { if (_fetched.value) return loading.value = true try { const data = await $fetch('/api/onboarding/status') if (data?.goals) { goals.value = { ...goals.value, ...data.goals } } if (data?.completedAt) { completedAt.value = data.completedAt } _fetched.value = true // If graduated, fetch recommendations if (completedAt.value) { await fetchRecommendations() } } catch { // Silently fail — goals stay at defaults } finally { loading.value = false } } async function fetchRecommendations() { const [events, wiki] = await Promise.allSettled([ $fetch('/api/events/recommended'), $fetch('/api/wiki/recommended'), ]) recommendations.value = { events: events.status === 'fulfilled' ? (events.value || []) : [], wiki: wiki.status === 'fulfilled' ? (wiki.value || []) : [], } } async function trackGoal(goalName) { if (isComplete.value) return try { await $fetch('/api/onboarding/track', { method: 'POST', body: { goal: goalName }, }) } catch { // Fire-and-forget } } // Initialize on first use fetchStatus() return { goals: readonly(goals), isComplete: readonly(isComplete), completedAt: readonly(completedAt), currentSuggestion, trackGoal, recommendations: readonly(recommendations), loading: readonly(loading), } }