All files / web/src/db/schema player-session-preferences.ts

100% Statements 104/104
100% Branches 1/1
100% Functions 1/1
100% Lines 104/104

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 102 103 104 1052x 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 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  
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { players } from './players'
 
/**
 * Configuration stored in the player_session_preferences JSON config column.
 * These are the settings that persist between StartPracticeModal opens.
 */
export interface PlayerSessionPreferencesConfig {
  durationMinutes: number
  problemLengthPreference: 'shorter' | 'recommended' | 'longer'
  partWeights: { abacus: number; visualization: number; linear: number }
  purposeWeights: { focus: number; reinforce: number; review: number; challenge: number }
  shufflePurposes: boolean
  gameBreakEnabled: boolean
  gameBreakMinutes: number
  gameBreakSelectionMode: string
  gameBreakSelectedGame: string | null
  gameBreakDifficultyPreset: string | null
  /** Whether the selected game should use custom break-game settings instead of a preset. */
  gameBreakShowCustomize?: boolean
  /** Custom settings for the selected break game. */
  gameBreakCustomConfig?: Record<string, unknown>
  gameBreakEnabledGames?: string[]
  /** Optional narration level for kid-facing explanations. */
  kidLanguageStyle?: KidLanguageStyle
  /** Whether celebration songs are enabled for this student. Defaults to true when feature flag is on. */
  sessionSongEnabled?: boolean
  /** Preferred genre for celebration songs. 'shuffle' picks a fresh 2-3 genre mix each song, 'any' rotates single genres. */
  sessionSongGenre?: SessionSongGenre
}
 
export type KidLanguageStyle = 'simple' | 'standard' | 'classical'
 
/**
 * Song genre preference — any string is valid (custom genres supported).
 * Preset IDs are convenience values shown in the UI picker.
 */
export type SessionSongGenre = string
 
export const SESSION_SONG_GENRES: { id: string; label: string }[] = [
  { id: 'shuffle', label: 'Surprise Mix' },
  { id: 'any', label: 'Single Genre (rotate)' },
  // Pop / Electronic
  { id: 'pop', label: 'Pop' },
  { id: 'disco', label: 'Disco' },
  { id: 'edm', label: 'EDM' },
  { id: 'chiptune', label: '8-Bit' },
  // Soul / Groove
  { id: 'funk', label: 'Funk' },
  { id: 'hip-hop', label: 'Hip-Hop' },
  { id: 'reggae', label: 'Reggae' },
  { id: 'jazz', label: 'Jazz' },
  // World / Latin
  { id: 'afrobeat', label: 'Afrobeat' },
  { id: 'salsa', label: 'Salsa' },
  { id: 'bossa-nova', label: 'Bossa Nova' },
  { id: 'bollywood', label: 'Bollywood' },
  // Rock / Acoustic
  { id: 'rock', label: 'Rock' },
  { id: 'folk', label: 'Folk' },
  { id: 'country', label: 'Country' },
  // Fun / Theatrical
  { id: 'musical-theater', label: 'Broadway' },
  { id: 'marching-band', label: 'Marching Band' },
  { id: 'electro-swing', label: 'Electro Swing' },
]
 
export const DEFAULT_SESSION_PREFERENCES: PlayerSessionPreferencesConfig = {
  durationMinutes: 10,
  problemLengthPreference: 'recommended',
  partWeights: { abacus: 2, visualization: 1, linear: 0 },
  purposeWeights: { focus: 3, reinforce: 1, review: 1, challenge: 1 },
  shufflePurposes: true,
  gameBreakEnabled: true,
  gameBreakMinutes: 5,
  gameBreakSelectionMode: 'kid-chooses',
  gameBreakSelectedGame: null,
  gameBreakDifficultyPreset: 'medium',
  gameBreakShowCustomize: false,
  gameBreakCustomConfig: {},
  gameBreakEnabledGames: [],
}
 
/**
 * Player session preferences table - persists StartPracticeModal settings per student
 *
 * One row per player. Config is a JSON blob containing all persisted settings.
 * Cascade-deletes when the player is deleted.
 */
export const playerSessionPreferences = sqliteTable('player_session_preferences', {
  /** Player ID (primary key, FK → players) */
  playerId: text('player_id')
    .primaryKey()
    .references(() => players.id, { onDelete: 'cascade' }),
 
  /** JSON blob containing session preferences */
  config: text('config').notNull(),
 
  /** Timestamp of last update (unix ms) */
  updatedAt: integer('updated_at').notNull(),
})
 
export type PlayerSessionPreferences = typeof playerSessionPreferences.$inferSelect
export type NewPlayerSessionPreferences = typeof playerSessionPreferences.$inferInsert