feat: add onboarding tutorial for new users
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 3m4s

- Introduced a modular tutorial layer to guide new parents through the app setup process.
- Implemented a 3-step forced intro after first sign-in (PIN setup → child creation → chore creation).
- Added just-in-time contextual hints for various features as users encounter them.
- Persisted user progress on the backend with new fields in the User model.
- Created a new tutorial controller and step registry in the frontend for managing tutorial states.
- Added Help button for easy access to tutorial tips and a restart option in the user profile.
- Ensured accessibility and mobile responsiveness for the tutorial overlay.
- Included tests for backend and frontend functionalities related to the tutorial.
This commit is contained in:
2026-06-19 17:27:35 -04:00
parent d147bd6f27
commit e2bb9cd6b9
24 changed files with 1764 additions and 627 deletions

View File

@@ -58,7 +58,12 @@
<!-- Selected day exception list -->
<div v-if="selectedDays.size > 0" class="exception-list">
<div v-for="idx in sortedSelectedDays" :key="idx" class="exception-row">
<div
v-for="(idx, i) in sortedSelectedDays"
:key="idx"
class="exception-row"
:data-tutorial="i === 0 ? 'schedule-days-exception' : undefined"
>
<span class="exception-day-name">{{ DAY_LABELS[idx] }}</span>
<div class="exception-right">
<template v-if="exceptions.has(idx)">
@@ -114,7 +119,7 @@
</div>
<span class="field-label">{{ intervalDays === 1 ? 'day' : 'days' }}</span>
</div>
<div class="interval-row">
<div class="interval-row" data-tutorial="schedule-interval-start">
<label class="field-label">Starting on</label>
<DateInputField
:modelValue="anchorDate"
@@ -161,11 +166,11 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import ModalDialog from './ModalDialog.vue'
import TimePickerPopover from './TimePickerPopover.vue'
import DateInputField from './DateInputField.vue'
import { maybeShow as tutorialMaybeShow } from '@/tutorial/controller'
import { maybeShow as tutorialMaybeShow, modalTutorialStepId } from '@/tutorial/controller'
import {
setChoreSchedule,
deleteChoreSchedule,
@@ -259,11 +264,33 @@ const intervalTime = ref<TimeValue>({
const saving = ref(false)
const errorMsg = ref<string | null>(null)
function triggerScheduleTutorial(isDays: boolean) {
modalTutorialStepId.value = isDays ? 'schedule-days-chips' : 'schedule-interval-frequency'
nextTick(() => {
if (isDays) {
tutorialMaybeShow(
'schedule-days-chips',
() => document.querySelector('.day-chips') as HTMLElement | null,
)
} else {
tutorialMaybeShow(
'schedule-interval-frequency',
() => document.querySelector('.stepper') as HTMLElement | null,
)
}
})
}
onMounted(() => {
tutorialMaybeShow(
'create-chore-schedule',
() => document.querySelector('.day-chips .chip') as HTMLElement | null,
)
triggerScheduleTutorial(mode.value === 'days')
})
onUnmounted(() => {
modalTutorialStepId.value = null
})
watch(mode, (newMode) => {
triggerScheduleTutorial(newMode === 'days')
})
// ── original snapshot (for dirty detection) ──────────────────────────────────