Practice & Gamification System
Architecture
packages/shared-practices/ Shared monorepo package
├── src/
│ ├── index.ts Public API (11 hooks, 4 components, 1 util)
│ ├── types.ts All TypeScript interfaces
│ ├── hooks/ React Query hooks (Supabase RPCs)
│ ├── components/ Shared UI components
│ └── utils/practiceIcons.ts Icon mapping (PRACTICE_ICON_MAP)
│
DB: practice schema 7+ tables, 14+ RPCs
Apps: apps/raamattu-nyt/ Primary consumer (PrayerPage, admin)
Key Concepts
Schedule Types
| Type | Behavior |
|------|----------|
| daily | Every day (default) |
| weekly | Specific weekdays [1,3,5] = Mon/Wed/Fri |
| every_n_days | Every N days |
| rotating_list | Cycle through list items |
| spontaneous | No schedule, manual only |
Content Types (Polymorphic)
| Type | Ref points to |
|------|---------------|
| prayer_set | prayer_sets.id |
| spiritual_path | spiritual_paths.id |
| prayer | prayers.id |
| prayer_calendar | prayer_calendars.id |
Session Flow
activate_practice_template(template, schedule, content)
│
▼
get_today_practice_items() → TodayPracticeItem[]
│
├─► Quick complete: start + complete in one roundtrip
│
└─► Timed session:
start_practice_session(practice_id)
│ usePracticeSession (client timer)
▼
complete_practice_session(session_id, notes, metrics)
│
▼
PracticeCompletionScreen (streak, rhythm, verse)
Streaks
- DB-computed via
get_practice_streaksRPC - Schedule-aware: weekly practices don't break on off-days
- Returns:
current_streak,longest_streak,total_completions
Guest Support
All RPCs accept p_guest_session_id. Unauthenticated users work via useGuestSession().
Hooks
| Hook | Purpose | Key RPC |
|------|---------|---------|
| useTodayPractices | Today's items + start/complete/quickComplete | get_today_practice_items |
| usePracticeTemplates(systemId) | Available templates | get_practice_templates |
| useActivatePractice | Activate with schedule + content | activate_practice_template |
| useDeactivatePractice | Remove practice | deactivate_practice |
| usePracticeSession | Client timer (no DB) | — |
| usePracticeHistory(practiceId?) | Paginated history | get_practice_history |
| usePracticeStreaks | Streaks per practice | get_practice_streaks |
| usePracticeStats(practiceId?) | Monthly/yearly stats | get_practice_stats |
| useUpdateSchedule | Change schedule | update_practice_schedule |
| useWeeklyRhythm(practiceId) | Week completed/scheduled | get_weekly_practice_rhythm |
| usePrayerSets(systemId) | Prayer set options | get_prayer_sets |
| useSpiritualPaths(systemId) | Spiritual path options | get_spiritual_paths |
| useUserPrayers | User's prayers | Direct query |
| useUserPrayerCalendars | User's calendars | Direct query |
Components
| Component | Purpose |
|-----------|---------|
| PracticeTaskRow | Practice row: icon, name, streak badge, quick-complete button, expandable verse |
| PracticeTimer | Running timer with pause/resume |
| PracticeScheduleEditor | Schedule type picker + weekday/interval config |
| PracticeCompletionScreen | Post-completion: checkmark, duration, streak, weekly rhythm bar, encouragement, verse |
Encouragement System
Post-completion encouragement messages shown on PracticeCompletionScreen.
DB Table: public.encouragement_messages
| Column | Type | Notes | |--------|------|-------| | id | uuid | PK | | system_id | text | 'raamattu-nyt' default | | text_fi | text | Finnish message | | text_en | text | English (optional) | | practice_type | text | null = generic, or specific type filter | | is_active | bool | | | sort_order | int | |
Service: apps/raamattu-nyt/src/lib/encouragementService.ts
getRandomEncouragement(systemId, practiceType?, lang)— random active message- Admin CRUD:
getAllEncouragements,createEncouragement,updateEncouragement,deleteEncouragement,toggleEncouragementActive
Hook: apps/raamattu-nyt/src/hooks/useEncouragementMessage.ts
useEncouragementMessage(practiceType?, osisRef?)→{ message, verseText, verseReference, loading }- Verse fallback chain: 1) practice osis_ref, 2) encouragement reading plan verse, 3) daily slogan
- Encouragement plan configured via
app_config.encouragement_reading_plan_id
Admin: AdminPracticesPage.tsx tabs
| Tab | Component | Purpose |
|-----|-----------|---------|
| Harjoitukset | inline | Template CRUD |
| Tilastot | inline | Aggregate stats |
| Rukoussetit | AdminPrayerSetsTab | Prayer set CRUD + items |
| Hengelliset polut | AdminSpiritualPathsTab | Spiritual path CRUD |
| Rohkaisut | AdminEncouragementTab | Encouragement message CRUD |
| Rohkaisujae | AdminEncouragementVerseTab | Reading plan verse config |
App-Side Components (not in shared-practices)
| Component | Location | Purpose |
|-----------|----------|---------|
| PracticeSessionDialog | apps/raamattu-nyt/src/components/practice/ | Full session dialog: content card, timer, notes, completion screen |
| PracticeActivationCard | same | Template activation with schedule + content picker |
| PracticeHistory | apps/raamattu-nyt/src/components/profile/ | Profile section history list |
| PracticeStatsCard | same | Profile section stats card |
| ProfilePractices | same | Profile practices section |
| AdminPracticesPage | apps/raamattu-nyt/src/pages/ | Full admin page with 6 tabs |
Completion Flow (PracticeSessionDialog)
User clicks practice row
│
├─► Quick complete (check button): quickComplete(practiceId)
│ └─► No dialog, just toggles done
│
└─► Timed session (row click): opens PracticeSessionDialog
│
├─ Shows content card (prayer set item / path step / custom)
├─ Shows verse text if osis_ref present
├─ PracticeTimer with pause/resume
├─ Notes textarea
│
└─► Complete button:
completeSession(sessionId, notes, metrics)
│
▼
PracticeCompletionScreen
├─ Duration + streak stats
├─ Weekly rhythm bar (X/Y)
├─ Encouragement message (random from DB)
└─ Verse card (osis_ref or encouragement plan)
Implementation Patterns
Adding a New Practice Type
- Insert into
practice.practice_templates(migration) - Add icon to
PRACTICE_ICON_MAPinutils/practiceIcons.ts - If content-linked: add content_type handler in activation flow
Dual-Write Pattern
Prayer practices write to both practice_sessions AND prayer_logs for backward compat. The complete_practice_session RPC handles this internally.
Query Key Conventions
["today-practices", stableId]
["practice-streaks", stableId]
["practice-stats", stableId, practiceId]
["practice-history", stableId, practiceId]
["practice-templates", systemId]
["weekly-practice-rhythm", practiceId, stableId]
Moving Features to shared-practices
- Extract hook/component from
apps/raamattu-nyt/ - Place in
packages/shared-practices/src/ - Export from
index.ts - Update app imports to
@shared-practices/... - Avoid app-specific imports (use generic Supabase client pattern)
Tänään Page Integration
The Tänään (Today) page (TanaanPage.tsx) is the primary consumer of practice data.
Architecture
TanaanPage
├── Daily tasks section (content_type !== "prayer_calendar")
│ └── PracticeTaskRow[] from useTodayPractices()
├── Recurring tasks section (prayer_calendar + prayer_set items)
│ └── RecurringTasksSection (calendarPracticeItems)
└── Tomorrow preview
└── TomorrowTasksSection (filters OUT prayer_calendar items)
prayer_calendar Content Type Gotcha
Prayer calendar items always resolve the latest active prayer from the calendar, not a today-scheduled prayer. The RPC fetches prayers where calendar_id = content_ref and status = 'active', ordered by most recent.
get_practice_items_for_date RPC
Parameterized version of get_today_practice_items that accepts a target date:
// Today's items (no date param)
const { data } = useTodayPractices();
// Tomorrow's items (with date param)
const { data } = useTomorrowPractices(); // calls get_practice_items_for_date with tomorrow
This RPC handles all content types (prayer_calendar, prayer_set, spiritual_path, prayer) and correctly computes schedule-based occurrence dates.
Future: Rewards & Badges (Not Yet Implemented)
Design considerations:
- Streak milestones (7, 30, 100 days) trigger rewards
- Badges for completing spiritual paths
- XP from session metrics (duration, verse count)
- Tables:
practice.user_rewards,practice.user_badges - PracticeCompletionScreen already shows streak — extend with badge unlock
DB Reference
See references/db-schema.md for full table and RPC reference.