feat(tutorial): implement comprehensive tutorial system with step guidance
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m9s

- Added tutorial controller to manage tutorial state and progress.
- Introduced HelpButton component for contextual help throughout the application.
- Created various tutorial steps for onboarding and feature guidance.
- Integrated tutorial prompts in multiple components (ChildrenListView, LoginButton, ScheduleModal, etc.) to enhance user experience.
- Implemented logic to show tutorials based on user actions and state.
- Added functionality to dismiss and skip tutorial sessions.
- Established a mechanism to hydrate tutorial state from user profile.
This commit is contained in:
2026-05-26 16:54:45 -04:00
parent ec4912aa4a
commit d147bd6f27
23 changed files with 1338 additions and 5 deletions
@@ -27,6 +27,7 @@ import {
triggerRoutineAsParent,
} from '@/common/api'
import { eventBus } from '@/common/eventBus'
import { maybeShow as tutorialMaybeShow } from '@/tutorial/controller'
import '@/assets/styles.css'
import type {
Task,
@@ -687,6 +688,36 @@ function openChoreMenu(taskId: string, e: MouseEvent) {
menuPosition.value = { top: rect.bottom, left: rect.right - 140 }
}
activeMenuFor.value = taskId
tutorialMaybeShow(
'chore-kebab',
() => document.querySelector('.kebab-menu') as HTMLElement | null,
)
const items: ChildTask[] = childChoreListRef.value?.items ?? []
const task = items.find((t) => t.id === taskId)
if (task) {
if (isChoreExpired(task)) {
nextTick(() => {
tutorialMaybeShow(
'chore-kebab-extend-time',
() =>
Array.from(document.querySelectorAll('.kebab-menu .menu-item')).find((el) =>
/extend\s*time/i.test(el.textContent || ''),
) as HTMLElement | null,
)
})
}
if (isChoreCompletedToday(task)) {
nextTick(() => {
tutorialMaybeShow(
'chore-kebab-reset',
() =>
Array.from(document.querySelectorAll('.kebab-menu .menu-item')).find((el) =>
/^reset/i.test((el.textContent || '').trim()),
) as HTMLElement | null,
)
})
}
}
}
function closeChoreMenu() {
@@ -1002,6 +1033,13 @@ onMounted(async () => {
child.value = data
tasks.value = data.tasks || []
rewards.value = data.rewards || []
// Fire the per-child overview tour (chains into assign-* steps).
nextTick(() => {
tutorialMaybeShow(
'select-child',
() => document.querySelector('.assign-buttons') as HTMLElement | null,
)
})
}
loading.value = false
if (scrollToId) {
@@ -1028,6 +1066,36 @@ onMounted(async () => {
}
})
// Fire status-badge tutorials when those badges first render for any chore.
watch(
() => {
const items: ChildTask[] = childChoreListRef.value?.items ?? []
return items.map((t) => ({ id: t.id, expired: isChoreExpired(t), pending: isChorePending(t) }))
},
(list) => {
if (list.some((t) => t.expired)) {
nextTick(() => {
tutorialMaybeShow(
'status-too-late',
() =>
Array.from(document.querySelectorAll('.chore-stamp')).find(
(el) => (el.textContent || '').trim() === 'TOO LATE',
) as HTMLElement | null,
)
})
}
if (list.some((t) => t.pending)) {
nextTick(() => {
tutorialMaybeShow(
'status-pending',
() => document.querySelector('.chore-stamp.pending-stamp') as HTMLElement | null,
)
})
}
},
{ deep: true },
)
onUnmounted(() => {
eventBus.off('child_task_triggered', handleTaskTriggered)
eventBus.off('child_reward_triggered', handleRewardTriggered)