Agent Skills: Practice & Gamification System

>

UncategorizedID: Spectaculous-Code/raamattu-nyt/practice-gamification

Install this agent skill to your local

pnpm dlx add-skill https://github.com/Spectaculous-Code/raamattu-nyt/tree/HEAD/.claude/skills/practice-gamification

Skill Files

Browse the full folder contents for practice-gamification.

Download Skill

Loading file tree…

.claude/skills/practice-gamification/SKILL.md

Skill Metadata

Name
practice-gamification
Description
>

Practice & Gamification System

Architecture

packages/shared-practices/     Shared monorepo package
├── src/
│   ├── index.ts               Public API (hooks, components, utils)
│   ├── types.ts               All TypeScript interfaces (incl. Grand Plan types)
│   ├── hooks/                  React Query hooks (Supabase RPCs)
│   │   ├── useGrandPlan.ts    Grand plan CRUD + reorder
│   │   ├── useTodayTasks.ts   Unified task builder (practices + plans + prayers)
│   │   └── ...                Practice hooks
│   ├── components/             Shared UI components
│   └── utils/practiceIcons.ts  Icon mapping (PRACTICE_ICON_MAP)
│
DB: practice schema             10+ tables, 20+ RPCs (incl. grand_plan tables)
Apps: apps/raamattu-nyt/        Primary consumer (TanaanPage, CinemaReader, 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_streaks RPC
  • 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 | | useGrandPlan | Grand plan CRUD + reorder | Multiple RPCs (see Grand Plan) | | useTodayTasks | Unified task builder | — (pure computation) | | useDiscipleshipTasks | All tasks for discipleship cinema | Composes multiple hooks |

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 | | FeaturedReadingPlanPicker | apps/raamattu-nyt/src/components/today/ | Top 3 reading plans with join/active badges for "Valitse tehtävä" section | | 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

  1. Insert into practice.practice_templates (migration)
  2. Add icon to PRACTICE_ICON_MAP in utils/practiceIcons.ts
  3. 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

  1. Extract hook/component from apps/raamattu-nyt/
  2. Place in packages/shared-practices/src/
  3. Export from index.ts
  4. Update app imports to @shared-practices/...
  5. Avoid app-specific imports (use generic Supabase client pattern)

Grand Plan System

Orchestration layer that groups diverse content types into named task collections with sort order.

Concepts

  • Personal plan: auto-created per user, holds their active practices/plans/prayers
  • Curated plan: admin-created, shared across users (e.g. "30 Days of Prayer")
  • Auto-linking: triggers automatically add/remove items when practices, reading plans, or prayers are activated/deactivated
  • Sort order: grand_plan_items.sort_order defines task display order on Tänään page

DB Tables (practice schema)

| Table | Purpose | |-------|---------| | grand_plans | Plan metadata: name, type (personal/curated), progression_type, created_by | | user_grand_plans | Membership: user_id, is_primary, current_day, status | | grand_plan_items | Tasks: item_type, item_ref (UUID), template_ref, sort_order, day_number |

Item types: practice, reading_plan, prayer, prayer_calendar, kooste

RPCs

| RPC | Purpose | |-----|---------| | get_or_create_personal_plan(user_id) | Returns/creates personal plan UUID | | add_item_to_grand_plan(...) | Add item with auto sort_order | | remove_item_from_grand_plan(plan_id, item_id) | Remove item | | reorder_grand_plan_items(plan_id, item_ids[]) | Batch reorder by array position | | get_user_grand_plans() | List user's plans with item_count | | get_grand_plan_items(plan_id) | Items with resolved_name/icon | | join_grand_plan(plan_id) | Join curated plan |

Auto-Link Triggers

| Trigger | Source table | Action | |---------|-------------|--------| | trg_practice_auto_link_grand_plan | practice.practices INSERT | Add item_type='practice' | | trg_practice_auto_unlink_grand_plan | practice.practices UPDATE (deactivation) | Delete item | | trg_reading_plan_auto_link_grand_plan | bible_schema.user_reading_plans INSERT | Add item_type='reading_plan' | | trg_reading_plan_auto_unlink_grand_plan | bible_schema.user_reading_plans UPDATE | Delete item | | trg_prayer_auto_link_grand_plan | public.prayers INSERT (status='active') | Add item_type='prayer' | | trg_prayer_auto_unlink_grand_plan | public.prayers UPDATE (deactivated) | Delete item |

useGrandPlan Hook

const { plan, items, reorderItems, addItem, removeItem, loading } = useGrandPlan();
// reorderItems(itemIds[]) — optimistic UI + invalidates cache

Admin

AdminGrandPlansTab in AdminPracticesPage — CRUD for plans and items, accessed via admin RPCs.

Task Reordering (Tänään Page)

Users reorder tasks via useTodayDashboard().reorderItems.

Flow

TanaanPage
├── useTodayDashboard() → { items, reorderItems }
│   items already sorted by sort_order from the RPC
└── Pass onReorder to TodayTaskList

TodayTaskList
├── "Muokkaa järjestystä" toggle → shows ChevronUp/Down per row
└── handleSwap → collect item_ids in new order → onReorder(itemIds)
    └── reorderItems(itemIds) → reorder_grand_plan_items RPC + cache invalidation

Discipleship Mode

Full-screen Cinema Reader orchestration for structured daily routines.

Entry Point

TanaanPage → two cinema buttons → CinemaReaderScreen with discipleshipTasks

Two Launch Modes

| Mode | Button | Props | Behavior | |------|--------|-------|----------| | Reading Plan Cinema | Blue Play | discipleshipTasks={readingPlanCinemaTasks} autoStart | Auto-plays reading plans only. No task selector, no quizzes, no transitions. | | Discipleship Cinema | Amber Clapperboard | discipleshipTasks={allTasks} | Full flow: task selector → reading + prayer + practice → quizzes → kooste. |

Hook: useDiscipleshipTasks

Location: apps/raamattu-nyt/src/hooks/useDiscipleshipTasks.ts

Composes multiple data sources into UnifiedTodayTask[]:

  • Active practices (filtered: no prayer_set, no empty prayer_calendar)
  • Active reading plans (with duration from useReadingPlanDurations, passes progression_type)
  • Today's scheduled prayers
  • Appends "Pohdittavat jakeet" (kooste) task at the end (NYT_KOOSTE_TASK_ID)

useTodayTasks (Unified Task Builder)

Location: packages/shared-practices/src/hooks/useTodayTasks.ts

Normalizes heterogeneous sources into UnifiedTodayTask[]:

  • ID format: rp-{id}, pr-{id}, py-{id}
  • Duration: reading plans use estimateReadingMinutes(verseCount), practices use template default, prayers use fallback
  • ReadingPlanInput accepts optional progression_type for correct completion detection
  • isCompletedToday for reading plans: Checks completed_days.includes(current_day) OR for completion-type plans (not calendar/day_of_year) also checks completed_days.includes(current_day - 1) because mark_reading_day_complete advances current_day immediately
  • Returns: { tasks, completed, uncompleted, totalDurationMinutes }

CinemaReaderScreen Discipleship Flow

Open with discipleshipTasks + optional autoStart/initialTask
    │
    ├─► autoStart=true → queue all uncompleted, start first
    │   No selector, no toggle, no quizzes, no transitions
    │
    ├─► initialTask provided → start immediately
    │
    └─► No initialTask → show DiscipleshipTaskSelector
         │
         ▼
    Task execution:
    ├─ reading_plan → fetch verses, display in cinema
    │   → mark_reading_day_complete RPC on finish
    │   → auto-insert memory quiz after (discipleship only, NOT autoStart)
    │   → quiz prioritizes user-marked verses (marked_refs from VerseBar)
    ├─ practice/prayer → DiscipleshipInlineTask (inline view)
    └─ kooste → load accumulated verse refs, display in cinema
         │
         ▼
    Task complete → DiscipleshipTransitionOverlay (auto-skipped in autoStart)
    ├─ Show completion summary
    ├─ "Next task" / free task selection
    └─ Skip completed tasks in queue

Reading Plan Completion Gotcha

mark_reading_day_complete advances current_day immediately (5→6). This affects:

  1. Dashboard (get_today_dashboard): For completion-type plans, checks completed_at::date = CURRENT_DATE (not current_day)
  2. Client (useTodayTasks): Also checks completed_days.includes(current_day - 1) for completion-type plans
  3. Query invalidation: Must invalidate ["today-dashboard"], ["reading-plan-streaks"], AND ["user-reading-plans"]

Quiz Marked Verse Priority

When user marks verses via DiscipleshipVerseBar (add to Nyt Kooste), those refs are passed as marked_refs in the quiz task metadata. useReadingPlanQuiz prioritizes them:

  • 2+ marked in plan → both quiz verses from marked
  • 1 marked → first from marked, second from remaining
  • 0 marked → prefer NT/Psalms/Proverbs

Reading Duration Estimates

Location: apps/raamattu-nyt/src/lib/readingDuration.ts

  • countVersesFromReadings(readings) — total verses across references (handles multi-chapter ranges)
  • estimateReadingMinutes(verseCount)Math.max(1, Math.round(verseCount * 0.13)) (~8 sec/verse)
  • Used by useReadingPlanDurations hook to compute per-plan estimated minutes

Tänään Page Integration

The Tänään (Today) page (TanaanPage.tsx) is the primary consumer of practice data.

Architecture

TanaanPage
├── useTodayDashboard() → unified dashboard (replaces ~13 hooks)
│   └── get_today_dashboard RPC → DashboardItem[] (practices, plans, prayers, kooste)
├── useDiscipleshipTasks() → tasks for Cinema discipleship mode
├── Daily tasks section (#today)
│   └── TodayTaskList
│       ├── Empty state: "Ei tehtäviä" → scrolls to #subscribe
│       ├── Uncompleted tasks ("Mahdollisuudet")
│       ├── "Pohdittavat jakeet" (kooste) row
│       └── Completed tasks ("Tehty tänään")
│       ├── Blue Play button → Reading Plan Cinema (autoStart)
│       └── Amber Clapperboard → Discipleship Cinema
├── Tomorrow preview (#tomorrow)
│   └── TomorrowTasksSection
├── Permanent prayers (#permanent)
│   └── PermanentTasksSection (priority_level='always')
├── Recurring tasks (#recurring)
│   └── RecurringTasksSection
└── "Valitse tehtävä" section (#subscribe)
    ├── Always visible (not gated on templates.length)
    ├── Auto-opens when user has zero dashboard items
    ├── FeaturedReadingPlanPicker (top 3 global plans, "Aloita"/"Aktiivinen")
    └── Harjoitukset (PracticeActivationCard per template)

Unified Dashboard Hook: useTodayDashboard

Location: apps/raamattu-nyt/src/hooks/useTodayDashboard.ts

Replaces the old pattern of calling useGrandPlan + useTodayPractices + useReadingPlans separately. Single RPC get_today_dashboard(p_grand_plan_id) returns DashboardItem[] with:

  • item_type: practice | reading_plan | prayer | kooste | mini_task
  • is_completed_today, current_streak, sort_order
  • metadata object varies by item_type (see DashboardPracticeMetadata, etc.)

Provides converter functions: toPracticeItem(), toReadingPlan(), toPrayer().

"Valitse tehtävä" Section

Renamed from "Valitse harjoitus". Contains two sub-sections:

  1. LukusuunnitelmatFeaturedReadingPlanPicker component

    • Shows top 3 available plans from useReadingPlans().availablePlans
    • "Aloita" button calls joinPlanWithCheck, shows "Aktiivinen" badge if joined
    • Waits for userPlansLoading before rendering to avoid false "Aloita" state
    • "Näytä kaikki" link → /reading-plans
    • onPlanJoined callback invalidates dashboard
  2. Harjoitukset — existing PracticeActivationCard list (unchanged)

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:

const { data } = useTodayPractices();      // today (no date param)
const { data } = useTomorrowPractices();    // calls get_practice_items_for_date with tomorrow

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.