Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 39x 39x 39x 39x 39x 39x 39x 39x 39x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x | import { createId } from '@paralleldrive/cuid2'
import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
import { players } from './players'
/**
* Skill Tutorial Progress
*
* Tracks whether a student has completed the tutorial for each skill.
* Tutorial completion is required before a skill can be added to practice rotation
* (unless teacher override is used).
*/
export const skillTutorialProgress = sqliteTable(
'skill_tutorial_progress',
{
id: text('id')
.primaryKey()
.$defaultFn(() => createId()),
/** Foreign key to players table */
playerId: text('player_id')
.notNull()
.references(() => players.id, { onDelete: 'cascade' }),
/**
* Skill identifier - matches the skill paths from curriculum phases
* Examples:
* - "basic.directAddition"
* - "fiveComplements.4=5-1"
* - "tenComplements.9=10-1"
*/
skillId: text('skill_id').notNull(),
// ---- Tutorial Completion ----
/** Whether the student has completed the tutorial for this skill */
tutorialCompleted: integer('tutorial_completed', { mode: 'boolean' }).notNull().default(false),
/** When the tutorial was completed */
completedAt: integer('completed_at', { mode: 'timestamp' }),
// ---- Teacher Override ----
/**
* Teacher can mark a skill as "learned offline" to bypass tutorial requirement.
* Use case: student learned the technique in person with their teacher.
*/
teacherOverride: integer('teacher_override', { mode: 'boolean' }).notNull().default(false),
/** When the teacher override was applied */
overrideAt: integer('override_at', { mode: 'timestamp' }),
/** Optional reason for the override (e.g., "Learned in class with Kehkashan") */
overrideReason: text('override_reason'),
// ---- Tutorial Skip Tracking ----
/**
* Number of times the student has skipped the tutorial prompt.
* Used to surface to teacher if student is repeatedly avoiding tutorials.
*/
skipCount: integer('skip_count').notNull().default(0),
/** When the tutorial was last skipped */
lastSkippedAt: integer('last_skipped_at', { mode: 'timestamp' }),
// ---- Metadata ----
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
/** Index for fast lookups by playerId */
playerIdIdx: index('skill_tutorial_progress_player_id_idx').on(table.playerId),
/** Unique constraint: one record per player per skill */
playerSkillUnique: uniqueIndex('skill_tutorial_progress_player_skill_unique').on(
table.playerId,
table.skillId
),
})
)
export type SkillTutorialProgress = typeof skillTutorialProgress.$inferSelect
export type NewSkillTutorialProgress = typeof skillTutorialProgress.$inferInsert
/**
* Check if a skill's tutorial requirement is satisfied.
* Returns true if:
* - Tutorial has been completed, OR
* - Teacher has applied an override
*/
export function isTutorialSatisfied(progress: SkillTutorialProgress | null | undefined): boolean {
if (!progress) return false
return progress.tutorialCompleted || progress.teacherOverride
}
|