@vue-skuilder/common-ui 0.1.33 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"common-ui.es.js","names":[],"sources":["../src/components/HeatMap.vue","../src/components/HeatMap.vue","../src/components/SkMouseTrap.vue","../src/components/SkMouseTrap.vue","../src/components/SkMouseTrapToolTip.vue","../src/components/SkMouseTrapToolTip.vue","../src/components/SnackbarService.ts","../src/components/SnackbarService.vue","../src/components/SnackbarService.vue","../src/components/PaginatingToolbar.vue","../src/components/PaginatingToolbar.vue","../src/components/CardSearch.vue","../src/components/CardSearch.vue","../src/components/CardSearchResults.vue","../src/components/CardSearchResults.vue","../src/components/CardHistoryViewer.vue","../src/components/CardHistoryViewer.vue","../src/composables/Displayable.ts","../src/services/authAPI.ts","../src/composables/useEntitlements.ts","../src/components/StudySessionTimer.vue","../src/components/StudySessionTimer.vue","../src/components/cardRendering/CardViewer.vue","../src/components/cardRendering/CardViewer.vue","../src/components/SessionControllerDebug.vue","../src/components/SessionControllerDebug.vue","../../../node_modules/canvas-confetti/dist/confetti.module.mjs","../src/components/StudySession.vue","../src/components/StudySession.vue","../src/components/studentInputs/MultipleChoiceOption.vue","../src/components/studentInputs/MultipleChoiceOption.vue","../src/components/studentInputs/RadioMultipleChoice.vue","../src/components/studentInputs/RadioMultipleChoice.vue","../src/components/studentInputs/TrueFalse.vue","../src/components/studentInputs/TrueFalse.vue","../src/components/studentInputs/UserInputNumber.vue","../src/components/studentInputs/UserInputNumber.vue","../src/components/cardRendering/CardLoader.vue","../src/components/cardRendering/CardLoader.vue","../src/stores/useAuthStore.ts","../src/stores/useConfigStore.ts","../src/composables/useAuthUI.ts","../src/components/auth/UserChip.vue","../src/components/auth/UserChip.vue","../src/components/auth/UserLogin.vue","../src/components/auth/UserLogin.vue","../src/utils/passwordValidation.ts","../src/components/auth/UserRegistration.vue","../src/components/auth/UserRegistration.vue","../src/components/auth/RequestPasswordReset.vue","../src/components/auth/RequestPasswordReset.vue","../src/components/auth/UserLoginAndRegistrationContainer.vue","../src/components/auth/UserLoginAndRegistrationContainer.vue","../src/components/auth/VerifyEmail.vue","../src/components/auth/VerifyEmail.vue","../src/components/auth/ResetPassword.vue","../src/components/auth/ResetPassword.vue","../src/components/TagsInput.vue","../src/components/TagsInput.vue","../src/components/CourseCardBrowser.vue","../src/components/CourseCardBrowser.vue","../src/components/CourseInformation.vue","../src/components/CourseInformation.vue","../src/components/CardBrowser.vue","../src/components/CardBrowser.vue","../src/components/CourseTagFilterWidget.vue","../src/components/CourseTagFilterWidget.vue","../src/components/UserTagPreferences.vue","../src/components/UserTagPreferences.vue"],"sourcesContent":["<template>\n <div>\n <svg :width=\"width\" :height=\"height\">\n <g\n v-for=\"(week, weekIndex) in weeks\"\n :key=\"weekIndex\"\n :transform=\"`translate(${weekIndex * (cellSize + cellMargin)}, 0)`\"\n >\n <rect\n v-for=\"(day, dayIndex) in week\"\n :key=\"day.date\"\n :x=\"0\"\n :y=\"dayIndex * (cellSize + cellMargin)\"\n :width=\"cellSize\"\n :height=\"cellSize\"\n :fill=\"getColor(day.count)\"\n @mouseover=\"showTooltip(day, $event)\"\n @mouseout=\"hideTooltip\"\n />\n </g>\n </svg>\n <div v-if=\"tooltipData\" class=\"tooltip\" :style=\"tooltipStyle\">\n {{ tooltipData.count }} review{{ tooltipData.count !== 1 ? 's' : '' }} on {{ toDateString(tooltipData.date) }}\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport moment from 'moment';\nimport { DayData, Color, ActivityRecord } from './HeatMap.types';\n\nexport default defineComponent({\n name: 'HeatMap',\n\n props: {\n // Accept activity records directly as a prop\n activityRecords: {\n type: Array as PropType<ActivityRecord[]>,\n default: () => [],\n },\n // Accept a function that can retrieve activity records\n activityRecordsGetter: {\n type: Function as PropType<() => Promise<ActivityRecord[]>>,\n default: null,\n },\n // Customize colors\n inactiveColor: {\n type: Object as PropType<Color>,\n default: () => ({ h: 0, s: 0, l: 0.9 }),\n },\n activeColor: {\n type: Object as PropType<Color>,\n default: () => ({ h: 155, s: 1, l: 0.5 }),\n },\n // Customize size\n cellSize: {\n type: Number,\n default: 12,\n },\n cellMargin: {\n type: Number,\n default: 3,\n },\n // Enable/disable seasonal colors\n enableSeasonalColors: {\n type: Boolean,\n default: true,\n },\n },\n\n data() {\n return {\n isLoading: false,\n localActivityRecords: [] as ActivityRecord[],\n heatmapData: {} as { [key: string]: number },\n weeks: [] as DayData[][],\n tooltipData: null as DayData | null,\n tooltipStyle: {} as { [key: string]: string },\n maxInRange: 0,\n };\n },\n\n computed: {\n width(): number {\n return 53 * (this.cellSize + this.cellMargin);\n },\n height(): number {\n return 7 * (this.cellSize + this.cellMargin);\n },\n effectiveActivityRecords(): ActivityRecord[] {\n const useLocal = Array.isArray(this.localActivityRecords) && this.localActivityRecords.length > 0;\n const records = useLocal ? this.localActivityRecords : this.activityRecords || [];\n console.log('Using effectiveActivityRecords, count:', records.length, 'source:', useLocal ? 'local' : 'prop');\n return records;\n },\n },\n\n watch: {\n activityRecords: {\n handler() {\n this.processRecords();\n this.createWeeksData();\n },\n immediate: true,\n },\n },\n\n async created() {\n if (this.activityRecordsGetter) {\n try {\n this.isLoading = true;\n console.log('Fetching activity records using getter...');\n \n // Ensure the getter is called safely with proper error handling\n let result = await this.activityRecordsGetter();\n \n // Handle the result - ensure it's an array of activity records\n if (Array.isArray(result)) {\n // Filter out records with invalid timestamps before processing\n this.localActivityRecords = result.filter(record => {\n if (!record || !record.timeStamp) return false;\n \n // Basic validation check for timestamps\n try {\n const m = moment(record.timeStamp);\n return m.isValid() && m.year() > 2000 && m.year() < 2100;\n } catch (e) {\n return false;\n }\n });\n \n console.log(`Received ${result.length} records, ${this.localActivityRecords.length} valid after filtering`);\n \n // Process the loaded records\n this.processRecords();\n this.createWeeksData();\n } else {\n console.error('Activity records getter did not return an array:', result);\n this.localActivityRecords = [];\n }\n } catch (error) {\n console.error('Error fetching activity records:', error);\n this.localActivityRecords = [];\n } finally {\n this.isLoading = false;\n }\n } else {\n console.log('No activityRecordsGetter provided, using direct activityRecords prop');\n }\n },\n\n methods: {\n toDateString(d: string): string {\n const m = moment(d);\n return moment.months()[m.month()] + ' ' + m.date();\n },\n\n processRecords() {\n const records = this.effectiveActivityRecords || [];\n console.log(`Processing ${records.length} records`);\n\n const data: { [key: string]: number } = {};\n\n if (records.length === 0) {\n console.log('No records to process');\n this.heatmapData = data;\n return;\n }\n\n // Sample logging of a few records to understand structure without flooding console\n const uniqueDates = new Set<string>();\n const dateDistribution: Record<string, number> = {};\n let validCount = 0;\n let invalidCount = 0;\n \n for (let i = 0; i < records.length; i++) {\n const record = records[i];\n \n if (!record || typeof record !== 'object' || !record.timeStamp) {\n invalidCount++;\n continue;\n }\n \n try {\n // Attempt to normalize the timestamp\n let normalizedDate: string;\n \n if (typeof record.timeStamp === 'string') {\n // For ISO strings, parse directly with moment\n normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');\n } else if (typeof record.timeStamp === 'number') {\n // For numeric timestamps, use Date constructor then moment\n normalizedDate = moment(new Date(record.timeStamp)).format('YYYY-MM-DD');\n } else if (typeof record.timeStamp === 'object') {\n // For objects (like Moment), try toString() or direct parsing\n if (typeof record.timeStamp.format === 'function') {\n // It's likely a Moment object\n normalizedDate = record.timeStamp.format('YYYY-MM-DD');\n } else if (record.timeStamp instanceof Date) {\n normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');\n } else {\n // Try to parse it as a string representation\n normalizedDate = moment(String(record.timeStamp)).format('YYYY-MM-DD');\n }\n } else {\n // Unhandled type\n invalidCount++;\n continue;\n }\n \n // Verify the date is valid before using it\n if (moment(normalizedDate, 'YYYY-MM-DD', true).isValid()) {\n data[normalizedDate] = (data[normalizedDate] || 0) + 1;\n uniqueDates.add(normalizedDate);\n \n // Track distribution by month for debugging\n const month = normalizedDate.substring(0, 7); // YYYY-MM\n dateDistribution[month] = (dateDistribution[month] || 0) + 1;\n \n validCount++;\n } else {\n invalidCount++;\n }\n } catch (e) {\n invalidCount++;\n }\n }\n \n // Log summary statistics\n console.log(`Processed ${validCount} valid dates, ${invalidCount} invalid dates`);\n console.log(`Found ${uniqueDates.size} unique dates`);\n console.log('Date distribution by month:', dateDistribution);\n\n this.heatmapData = data;\n },\n\n createWeeksData() {\n // Reset weeks and max count\n this.weeks = [];\n this.maxInRange = 0;\n \n const end = moment();\n const start = end.clone().subtract(52, 'weeks');\n const day = start.clone().startOf('week');\n \n console.log('Creating weeks data from', start.format('YYYY-MM-DD'), 'to', end.format('YYYY-MM-DD'));\n \n // Ensure we have data to display\n if (Object.keys(this.heatmapData).length === 0) {\n console.log('No heatmap data available to display');\n }\n\n // For debugging, log some sample dates from the heatmap data\n const sampleDates = Object.keys(this.heatmapData).slice(0, 5);\n console.log('Sample dates in heatmap data:', sampleDates);\n \n // Build the week data structure\n while (day.isSameOrBefore(end)) {\n const weekData: DayData[] = [];\n for (let i = 0; i < 7; i++) {\n const date = day.format('YYYY-MM-DD');\n const count = this.heatmapData[date] || 0;\n const dayData: DayData = {\n date,\n count,\n };\n weekData.push(dayData);\n if (dayData.count > this.maxInRange) {\n this.maxInRange = dayData.count;\n }\n\n day.add(1, 'day');\n }\n this.weeks.push(weekData);\n }\n \n console.log('Weeks data created, maxInRange:', this.maxInRange);\n \n // Calculate summary stats for display\n let totalDaysWithActivity = 0;\n let totalActivity = 0;\n \n Object.values(this.heatmapData).forEach(count => {\n totalDaysWithActivity++;\n totalActivity += count;\n });\n \n console.log(`Activity summary: ${totalActivity} activities across ${totalDaysWithActivity} days`);\n },\n\n getColor(count: number): string {\n if (this.maxInRange === 0) return this.hslToString(this.inactiveColor);\n\n const t = count === 0 ? 0 : Math.min((2 * count) / this.maxInRange, 1);\n\n let seasonalColor: Color = this.activeColor;\n\n if (this.enableSeasonalColors) {\n const now = moment();\n if (now.month() === 11 && now.date() >= 5) {\n // Christmas colors\n seasonalColor = Math.random() > 0.5 ? { h: 350, s: 0.8, l: 0.5 } : { h: 135, s: 0.8, l: 0.4 };\n } else if (now.month() === 9 && now.date() >= 25) {\n // Halloween colors\n seasonalColor =\n Math.random() > 0.5\n ? { h: 0, s: 0, l: 0 }\n : Math.random() > 0.5\n ? { h: 30, s: 1, l: 0.5 }\n : { h: 270, s: 1, l: 0.5 };\n }\n }\n\n const h = seasonalColor.h;\n const s = this.interpolate(this.inactiveColor.s, seasonalColor.s, t);\n const l = this.interpolate(this.inactiveColor.l, seasonalColor.l, t);\n\n return this.hslToString({ h, s, l });\n },\n\n interpolate(start: number, end: number, t: number): number {\n return start + (end - start) * t;\n },\n\n hslToString(color: Color): string {\n return `hsl(${color.h}, ${color.s * 100}%, ${color.l * 100}%)`;\n },\n\n showTooltip(day: DayData, event: MouseEvent) {\n this.tooltipData = day;\n this.tooltipStyle = {\n position: 'absolute',\n left: `${event.pageX + 10}px`,\n top: `${event.pageY + 10}px`,\n };\n },\n\n hideTooltip() {\n this.tooltipData = null;\n },\n },\n});\n</script>\n\n<style scoped>\n.tooltip {\n background-color: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 5px;\n border-radius: 3px;\n font-size: 12px;\n}\n</style>\n","<template>\n <div>\n <svg :width=\"width\" :height=\"height\">\n <g\n v-for=\"(week, weekIndex) in weeks\"\n :key=\"weekIndex\"\n :transform=\"`translate(${weekIndex * (cellSize + cellMargin)}, 0)`\"\n >\n <rect\n v-for=\"(day, dayIndex) in week\"\n :key=\"day.date\"\n :x=\"0\"\n :y=\"dayIndex * (cellSize + cellMargin)\"\n :width=\"cellSize\"\n :height=\"cellSize\"\n :fill=\"getColor(day.count)\"\n @mouseover=\"showTooltip(day, $event)\"\n @mouseout=\"hideTooltip\"\n />\n </g>\n </svg>\n <div v-if=\"tooltipData\" class=\"tooltip\" :style=\"tooltipStyle\">\n {{ tooltipData.count }} review{{ tooltipData.count !== 1 ? 's' : '' }} on {{ toDateString(tooltipData.date) }}\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport moment from 'moment';\nimport { DayData, Color, ActivityRecord } from './HeatMap.types';\n\nexport default defineComponent({\n name: 'HeatMap',\n\n props: {\n // Accept activity records directly as a prop\n activityRecords: {\n type: Array as PropType<ActivityRecord[]>,\n default: () => [],\n },\n // Accept a function that can retrieve activity records\n activityRecordsGetter: {\n type: Function as PropType<() => Promise<ActivityRecord[]>>,\n default: null,\n },\n // Customize colors\n inactiveColor: {\n type: Object as PropType<Color>,\n default: () => ({ h: 0, s: 0, l: 0.9 }),\n },\n activeColor: {\n type: Object as PropType<Color>,\n default: () => ({ h: 155, s: 1, l: 0.5 }),\n },\n // Customize size\n cellSize: {\n type: Number,\n default: 12,\n },\n cellMargin: {\n type: Number,\n default: 3,\n },\n // Enable/disable seasonal colors\n enableSeasonalColors: {\n type: Boolean,\n default: true,\n },\n },\n\n data() {\n return {\n isLoading: false,\n localActivityRecords: [] as ActivityRecord[],\n heatmapData: {} as { [key: string]: number },\n weeks: [] as DayData[][],\n tooltipData: null as DayData | null,\n tooltipStyle: {} as { [key: string]: string },\n maxInRange: 0,\n };\n },\n\n computed: {\n width(): number {\n return 53 * (this.cellSize + this.cellMargin);\n },\n height(): number {\n return 7 * (this.cellSize + this.cellMargin);\n },\n effectiveActivityRecords(): ActivityRecord[] {\n const useLocal = Array.isArray(this.localActivityRecords) && this.localActivityRecords.length > 0;\n const records = useLocal ? this.localActivityRecords : this.activityRecords || [];\n console.log('Using effectiveActivityRecords, count:', records.length, 'source:', useLocal ? 'local' : 'prop');\n return records;\n },\n },\n\n watch: {\n activityRecords: {\n handler() {\n this.processRecords();\n this.createWeeksData();\n },\n immediate: true,\n },\n },\n\n async created() {\n if (this.activityRecordsGetter) {\n try {\n this.isLoading = true;\n console.log('Fetching activity records using getter...');\n \n // Ensure the getter is called safely with proper error handling\n let result = await this.activityRecordsGetter();\n \n // Handle the result - ensure it's an array of activity records\n if (Array.isArray(result)) {\n // Filter out records with invalid timestamps before processing\n this.localActivityRecords = result.filter(record => {\n if (!record || !record.timeStamp) return false;\n \n // Basic validation check for timestamps\n try {\n const m = moment(record.timeStamp);\n return m.isValid() && m.year() > 2000 && m.year() < 2100;\n } catch (e) {\n return false;\n }\n });\n \n console.log(`Received ${result.length} records, ${this.localActivityRecords.length} valid after filtering`);\n \n // Process the loaded records\n this.processRecords();\n this.createWeeksData();\n } else {\n console.error('Activity records getter did not return an array:', result);\n this.localActivityRecords = [];\n }\n } catch (error) {\n console.error('Error fetching activity records:', error);\n this.localActivityRecords = [];\n } finally {\n this.isLoading = false;\n }\n } else {\n console.log('No activityRecordsGetter provided, using direct activityRecords prop');\n }\n },\n\n methods: {\n toDateString(d: string): string {\n const m = moment(d);\n return moment.months()[m.month()] + ' ' + m.date();\n },\n\n processRecords() {\n const records = this.effectiveActivityRecords || [];\n console.log(`Processing ${records.length} records`);\n\n const data: { [key: string]: number } = {};\n\n if (records.length === 0) {\n console.log('No records to process');\n this.heatmapData = data;\n return;\n }\n\n // Sample logging of a few records to understand structure without flooding console\n const uniqueDates = new Set<string>();\n const dateDistribution: Record<string, number> = {};\n let validCount = 0;\n let invalidCount = 0;\n \n for (let i = 0; i < records.length; i++) {\n const record = records[i];\n \n if (!record || typeof record !== 'object' || !record.timeStamp) {\n invalidCount++;\n continue;\n }\n \n try {\n // Attempt to normalize the timestamp\n let normalizedDate: string;\n \n if (typeof record.timeStamp === 'string') {\n // For ISO strings, parse directly with moment\n normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');\n } else if (typeof record.timeStamp === 'number') {\n // For numeric timestamps, use Date constructor then moment\n normalizedDate = moment(new Date(record.timeStamp)).format('YYYY-MM-DD');\n } else if (typeof record.timeStamp === 'object') {\n // For objects (like Moment), try toString() or direct parsing\n if (typeof record.timeStamp.format === 'function') {\n // It's likely a Moment object\n normalizedDate = record.timeStamp.format('YYYY-MM-DD');\n } else if (record.timeStamp instanceof Date) {\n normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');\n } else {\n // Try to parse it as a string representation\n normalizedDate = moment(String(record.timeStamp)).format('YYYY-MM-DD');\n }\n } else {\n // Unhandled type\n invalidCount++;\n continue;\n }\n \n // Verify the date is valid before using it\n if (moment(normalizedDate, 'YYYY-MM-DD', true).isValid()) {\n data[normalizedDate] = (data[normalizedDate] || 0) + 1;\n uniqueDates.add(normalizedDate);\n \n // Track distribution by month for debugging\n const month = normalizedDate.substring(0, 7); // YYYY-MM\n dateDistribution[month] = (dateDistribution[month] || 0) + 1;\n \n validCount++;\n } else {\n invalidCount++;\n }\n } catch (e) {\n invalidCount++;\n }\n }\n \n // Log summary statistics\n console.log(`Processed ${validCount} valid dates, ${invalidCount} invalid dates`);\n console.log(`Found ${uniqueDates.size} unique dates`);\n console.log('Date distribution by month:', dateDistribution);\n\n this.heatmapData = data;\n },\n\n createWeeksData() {\n // Reset weeks and max count\n this.weeks = [];\n this.maxInRange = 0;\n \n const end = moment();\n const start = end.clone().subtract(52, 'weeks');\n const day = start.clone().startOf('week');\n \n console.log('Creating weeks data from', start.format('YYYY-MM-DD'), 'to', end.format('YYYY-MM-DD'));\n \n // Ensure we have data to display\n if (Object.keys(this.heatmapData).length === 0) {\n console.log('No heatmap data available to display');\n }\n\n // For debugging, log some sample dates from the heatmap data\n const sampleDates = Object.keys(this.heatmapData).slice(0, 5);\n console.log('Sample dates in heatmap data:', sampleDates);\n \n // Build the week data structure\n while (day.isSameOrBefore(end)) {\n const weekData: DayData[] = [];\n for (let i = 0; i < 7; i++) {\n const date = day.format('YYYY-MM-DD');\n const count = this.heatmapData[date] || 0;\n const dayData: DayData = {\n date,\n count,\n };\n weekData.push(dayData);\n if (dayData.count > this.maxInRange) {\n this.maxInRange = dayData.count;\n }\n\n day.add(1, 'day');\n }\n this.weeks.push(weekData);\n }\n \n console.log('Weeks data created, maxInRange:', this.maxInRange);\n \n // Calculate summary stats for display\n let totalDaysWithActivity = 0;\n let totalActivity = 0;\n \n Object.values(this.heatmapData).forEach(count => {\n totalDaysWithActivity++;\n totalActivity += count;\n });\n \n console.log(`Activity summary: ${totalActivity} activities across ${totalDaysWithActivity} days`);\n },\n\n getColor(count: number): string {\n if (this.maxInRange === 0) return this.hslToString(this.inactiveColor);\n\n const t = count === 0 ? 0 : Math.min((2 * count) / this.maxInRange, 1);\n\n let seasonalColor: Color = this.activeColor;\n\n if (this.enableSeasonalColors) {\n const now = moment();\n if (now.month() === 11 && now.date() >= 5) {\n // Christmas colors\n seasonalColor = Math.random() > 0.5 ? { h: 350, s: 0.8, l: 0.5 } : { h: 135, s: 0.8, l: 0.4 };\n } else if (now.month() === 9 && now.date() >= 25) {\n // Halloween colors\n seasonalColor =\n Math.random() > 0.5\n ? { h: 0, s: 0, l: 0 }\n : Math.random() > 0.5\n ? { h: 30, s: 1, l: 0.5 }\n : { h: 270, s: 1, l: 0.5 };\n }\n }\n\n const h = seasonalColor.h;\n const s = this.interpolate(this.inactiveColor.s, seasonalColor.s, t);\n const l = this.interpolate(this.inactiveColor.l, seasonalColor.l, t);\n\n return this.hslToString({ h, s, l });\n },\n\n interpolate(start: number, end: number, t: number): number {\n return start + (end - start) * t;\n },\n\n hslToString(color: Color): string {\n return `hsl(${color.h}, ${color.s * 100}%, ${color.l * 100}%)`;\n },\n\n showTooltip(day: DayData, event: MouseEvent) {\n this.tooltipData = day;\n this.tooltipStyle = {\n position: 'absolute',\n left: `${event.pageX + 10}px`,\n top: `${event.pageY + 10}px`,\n };\n },\n\n hideTooltip() {\n this.tooltipData = null;\n },\n },\n});\n</script>\n\n<style scoped>\n.tooltip {\n background-color: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 5px;\n border-radius: 3px;\n font-size: 12px;\n}\n</style>\n","<template>\n <v-dialog v-if=\"display\" max-width=\"500px\" transition=\"dialog-transition\">\n <template #activator=\"{ props }\">\n <v-btn icon color=\"primary\" v-bind=\"props\">\n <v-icon>mdi-keyboard</v-icon>\n </v-btn>\n </template>\n\n <v-card>\n <v-toolbar color=\"secondary\" dark dense>\n <v-toolbar-title class=\"text-subtitle-1\">Shortcut keys:</v-toolbar-title>\n </v-toolbar>\n <v-list dense>\n <v-list-item\n v-for=\"hk in commands\"\n :key=\"Array.isArray(hk.hotkey) ? hk.hotkey.join(',') : hk.hotkey\"\n class=\"py-1\"\n >\n <v-btn variant=\"outlined\" color=\"primary\" class=\"text-white\" size=\"small\">\n {{ Array.isArray(hk.hotkey) ? hk.hotkey[0] : hk.hotkey }}\n </v-btn>\n <v-spacer></v-spacer>\n <span class=\"text-caption ml-2\">{{ hk.command }}</span>\n </v-list-item>\n </v-list>\n </v-card>\n </v-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';\nimport { SkMouseTrapProps } from './SkMouseTrap.types';\n\nexport default defineComponent({\n name: 'SkMouseTrap',\n\n props: {\n refreshInterval: {\n type: Number as PropType<SkMouseTrapProps['refreshInterval']>,\n default: 500,\n },\n },\n\n data() {\n return {\n commands: [] as HotKeyMetaData[],\n display: false,\n intervalId: null as number | null,\n };\n },\n\n created() {\n this.intervalId = window.setInterval(this.refreshState, this.refreshInterval);\n },\n\n beforeUnmount() {\n if (this.intervalId !== null) {\n clearInterval(this.intervalId);\n }\n },\n\n methods: {\n refreshState() {\n this.commands = SkldrMouseTrap.commands;\n this.display = this.commands.length > 0;\n },\n },\n});\n</script>\n","<template>\n <v-dialog v-if=\"display\" max-width=\"500px\" transition=\"dialog-transition\">\n <template #activator=\"{ props }\">\n <v-btn icon color=\"primary\" v-bind=\"props\">\n <v-icon>mdi-keyboard</v-icon>\n </v-btn>\n </template>\n\n <v-card>\n <v-toolbar color=\"secondary\" dark dense>\n <v-toolbar-title class=\"text-subtitle-1\">Shortcut keys:</v-toolbar-title>\n </v-toolbar>\n <v-list dense>\n <v-list-item\n v-for=\"hk in commands\"\n :key=\"Array.isArray(hk.hotkey) ? hk.hotkey.join(',') : hk.hotkey\"\n class=\"py-1\"\n >\n <v-btn variant=\"outlined\" color=\"primary\" class=\"text-white\" size=\"small\">\n {{ Array.isArray(hk.hotkey) ? hk.hotkey[0] : hk.hotkey }}\n </v-btn>\n <v-spacer></v-spacer>\n <span class=\"text-caption ml-2\">{{ hk.command }}</span>\n </v-list-item>\n </v-list>\n </v-card>\n </v-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';\nimport { SkMouseTrapProps } from './SkMouseTrap.types';\n\nexport default defineComponent({\n name: 'SkMouseTrap',\n\n props: {\n refreshInterval: {\n type: Number as PropType<SkMouseTrapProps['refreshInterval']>,\n default: 500,\n },\n },\n\n data() {\n return {\n commands: [] as HotKeyMetaData[],\n display: false,\n intervalId: null as number | null,\n };\n },\n\n created() {\n this.intervalId = window.setInterval(this.refreshState, this.refreshInterval);\n },\n\n beforeUnmount() {\n if (this.intervalId !== null) {\n clearInterval(this.intervalId);\n }\n },\n\n methods: {\n refreshState() {\n this.commands = SkldrMouseTrap.commands;\n this.display = this.commands.length > 0;\n },\n },\n});\n</script>\n","<template>\n <div\n class=\"sk-mousetrap-tooltip-wrapper\"\n ref=\"wrapperElement\"\n :class=\"[\n isControlKeyPressed && !disabled && highlightEffect !== 'none' ? `sk-mousetrap-highlight-${highlightEffect}` : '',\n ]\"\n >\n <slot></slot>\n <transition name=\"fade\">\n <div\n v-if=\"showTooltip && isControlKeyPressed && !disabled\"\n class=\"sk-mousetrap-tooltip\"\n :class=\"{\n 'sk-mt-tooltip-top': position === 'top',\n 'sk-mt-tooltip-bottom': position === 'bottom',\n 'sk-mt-tooltip-left': position === 'left',\n 'sk-mt-tooltip-right': position === 'right',\n }\"\n >\n {{ formattedHotkey }}\n </div>\n </transition>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';\nimport { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';\n\nexport default defineComponent({\n name: 'SkMouseTrapToolTip',\n\n props: {\n hotkey: {\n type: [String, Array] as PropType<string | string[]>,\n required: true,\n },\n command: {\n type: String,\n required: true,\n },\n disabled: {\n type: Boolean,\n default: false,\n },\n position: {\n type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,\n default: 'top',\n },\n showTooltip: {\n type: Boolean,\n default: true,\n },\n highlightEffect: {\n type: String as PropType<'glow' | 'scale' | 'border' | 'none'>,\n default: 'glow',\n },\n },\n\n emits: ['hotkey-triggered'],\n\n setup(props, { emit }) {\n const wrapperElement = ref<HTMLElement | null>(null);\n const isControlKeyPressed = ref(false);\n const hotkeyId = ref(`hotkey-${Math.random().toString(36).substring(2, 15)}`);\n\n // Format hotkey for display\n const formattedHotkey = computed(() => {\n const hotkey = Array.isArray(props.hotkey) ? props.hotkey[0] : props.hotkey;\n // Check if this is a sequence (has spaces) or a combination (has +)\n if (hotkey.includes(' ')) {\n // For sequences like \"g h\", display as \"g, h\"\n return hotkey\n .toLowerCase()\n .split(' ')\n .map((part) => part.charAt(0) + part.slice(1))\n .join(', ');\n } else {\n // For combinations like \"ctrl+s\", display as \"Ctrl + S\"\n return hotkey\n .toLowerCase()\n .split('+')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' + ');\n }\n });\n\n // Apply highlight effect to the actual button/control when Ctrl is pressed\n watch(\n () => isControlKeyPressed.value,\n (pressed) => {\n if (!wrapperElement.value || props.disabled) return;\n\n const clickableElement = wrapperElement.value.querySelector(\n 'button, a, input[type=\"button\"], [role=\"button\"]'\n ) as HTMLElement;\n\n if (clickableElement) {\n clickableElement.style.transition = 'all 250ms ease';\n\n if (pressed && props.highlightEffect !== 'none') {\n // Add slight brightness increase to the inner element\n clickableElement.style.filter = 'brightness(1.1)';\n } else {\n clickableElement.style.filter = '';\n }\n }\n }\n );\n\n // Handle Ctrl key detection\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Control') {\n isControlKeyPressed.value = true;\n }\n };\n\n const handleKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Control') {\n isControlKeyPressed.value = false;\n }\n };\n\n // Handle clicking the element when hotkey is pressed\n const handleHotkeyPress = () => {\n if (props.disabled || !wrapperElement.value) return;\n\n // Try finding a clickable element within our wrapper\n let clickableElement = wrapperElement.value.querySelector(\n 'button, a, input[type=\"button\"], [role=\"button\"]'\n ) as HTMLElement;\n\n // If no standard clickable element found, try to find navigation or list items\n if (!clickableElement) {\n clickableElement = wrapperElement.value.querySelector(\n 'v-list-item, .v-list-item, router-link, .router-link, a, [to]'\n ) as HTMLElement;\n }\n\n // If still no element found, try the wrapper itself - it might be clickable\n if (\n !clickableElement &&\n (wrapperElement.value.hasAttribute('to') ||\n wrapperElement.value.tagName === 'A' ||\n wrapperElement.value.classList.contains('v-list-item'))\n ) {\n clickableElement = wrapperElement.value;\n }\n\n // Get closest parent list item or router link if we found a title/content element\n if (!clickableElement) {\n const closestClickableParent = wrapperElement.value.closest('v-list-item, .v-list-item, a, [to]');\n if (closestClickableParent) {\n clickableElement = closestClickableParent as HTMLElement;\n }\n }\n\n if (clickableElement) {\n if (clickableElement.hasAttribute('to')) {\n // Handle router-link style navigation\n const routePath = clickableElement.getAttribute('to');\n if (routePath && window.location.pathname !== routePath) {\n // Use parent router if available (Vue component)\n const router = (window as any).$nuxt?.$router || (window as any).$router;\n if (router && typeof router.push === 'function') {\n router.push(routePath);\n } else {\n // Fallback to regular navigation\n window.location.pathname = routePath;\n }\n }\n emit('hotkey-triggered', props.hotkey);\n } else {\n // Regular click for standard elements\n clickableElement.click();\n emit('hotkey-triggered', props.hotkey);\n }\n } else {\n // If no clickable element found, emit the event for parent handling\n console.log('No clickable element found for hotkey', props.hotkey);\n emit('hotkey-triggered', props.hotkey);\n }\n };\n\n // Register/unregister the hotkey binding\n const registerHotkey = () => {\n if (!props.disabled) {\n SkldrMouseTrap.addBinding({\n hotkey: props.hotkey,\n command: props.command,\n callback: handleHotkeyPress,\n });\n }\n };\n\n const unregisterHotkey = () => {\n if (!props.disabled) {\n SkldrMouseTrap.removeBinding(props.hotkey);\n }\n };\n\n // Watch for changes to the disabled prop\n watch(\n () => props.disabled,\n (newValue) => {\n if (newValue) {\n unregisterHotkey();\n } else {\n registerHotkey();\n }\n }\n );\n\n onMounted(() => {\n // Register global keyboard listeners for the Ctrl key\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('keyup', handleKeyUp);\n\n // Register the hotkey\n registerHotkey();\n });\n\n onBeforeUnmount(() => {\n // Clean up event listeners\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('keyup', handleKeyUp);\n\n // Unregister the hotkey\n unregisterHotkey();\n });\n\n return {\n wrapperElement,\n isControlKeyPressed,\n formattedHotkey,\n };\n },\n});\n</script>\n\n<style scoped>\n.sk-mousetrap-tooltip-wrapper {\n display: inline-block;\n position: relative;\n}\n\n.sk-mousetrap-tooltip {\n position: absolute;\n background-color: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 12px;\n white-space: nowrap;\n pointer-events: none;\n z-index: 9999;\n}\n\n.sk-mt-tooltip-top {\n bottom: 100%;\n margin-bottom: 5px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.sk-mt-tooltip-bottom {\n top: 100%;\n margin-top: 5px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.sk-mt-tooltip-left {\n right: 100%;\n margin-right: 5px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n.sk-mt-tooltip-right {\n left: 100%;\n margin-left: 5px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n/* Highlight effects when Ctrl is pressed */\n.sk-mousetrap-highlight-glow {\n box-shadow: 0 0 8px 2px rgba(25, 118, 210, 0.6);\n transition: box-shadow 250ms ease;\n}\n\n.sk-mousetrap-highlight-scale {\n transform: scale(1.03);\n transition: transform 250ms ease;\n}\n\n.sk-mousetrap-highlight-border {\n outline: 2px solid rgba(25, 118, 210, 0.8);\n outline-offset: 2px;\n border-radius: 4px;\n transition: outline 250ms ease, outline-offset 250ms ease;\n}\n\n/* Fade transition */\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 250ms ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n</style>\n","<template>\n <div\n class=\"sk-mousetrap-tooltip-wrapper\"\n ref=\"wrapperElement\"\n :class=\"[\n isControlKeyPressed && !disabled && highlightEffect !== 'none' ? `sk-mousetrap-highlight-${highlightEffect}` : '',\n ]\"\n >\n <slot></slot>\n <transition name=\"fade\">\n <div\n v-if=\"showTooltip && isControlKeyPressed && !disabled\"\n class=\"sk-mousetrap-tooltip\"\n :class=\"{\n 'sk-mt-tooltip-top': position === 'top',\n 'sk-mt-tooltip-bottom': position === 'bottom',\n 'sk-mt-tooltip-left': position === 'left',\n 'sk-mt-tooltip-right': position === 'right',\n }\"\n >\n {{ formattedHotkey }}\n </div>\n </transition>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';\nimport { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';\n\nexport default defineComponent({\n name: 'SkMouseTrapToolTip',\n\n props: {\n hotkey: {\n type: [String, Array] as PropType<string | string[]>,\n required: true,\n },\n command: {\n type: String,\n required: true,\n },\n disabled: {\n type: Boolean,\n default: false,\n },\n position: {\n type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,\n default: 'top',\n },\n showTooltip: {\n type: Boolean,\n default: true,\n },\n highlightEffect: {\n type: String as PropType<'glow' | 'scale' | 'border' | 'none'>,\n default: 'glow',\n },\n },\n\n emits: ['hotkey-triggered'],\n\n setup(props, { emit }) {\n const wrapperElement = ref<HTMLElement | null>(null);\n const isControlKeyPressed = ref(false);\n const hotkeyId = ref(`hotkey-${Math.random().toString(36).substring(2, 15)}`);\n\n // Format hotkey for display\n const formattedHotkey = computed(() => {\n const hotkey = Array.isArray(props.hotkey) ? props.hotkey[0] : props.hotkey;\n // Check if this is a sequence (has spaces) or a combination (has +)\n if (hotkey.includes(' ')) {\n // For sequences like \"g h\", display as \"g, h\"\n return hotkey\n .toLowerCase()\n .split(' ')\n .map((part) => part.charAt(0) + part.slice(1))\n .join(', ');\n } else {\n // For combinations like \"ctrl+s\", display as \"Ctrl + S\"\n return hotkey\n .toLowerCase()\n .split('+')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' + ');\n }\n });\n\n // Apply highlight effect to the actual button/control when Ctrl is pressed\n watch(\n () => isControlKeyPressed.value,\n (pressed) => {\n if (!wrapperElement.value || props.disabled) return;\n\n const clickableElement = wrapperElement.value.querySelector(\n 'button, a, input[type=\"button\"], [role=\"button\"]'\n ) as HTMLElement;\n\n if (clickableElement) {\n clickableElement.style.transition = 'all 250ms ease';\n\n if (pressed && props.highlightEffect !== 'none') {\n // Add slight brightness increase to the inner element\n clickableElement.style.filter = 'brightness(1.1)';\n } else {\n clickableElement.style.filter = '';\n }\n }\n }\n );\n\n // Handle Ctrl key detection\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Control') {\n isControlKeyPressed.value = true;\n }\n };\n\n const handleKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Control') {\n isControlKeyPressed.value = false;\n }\n };\n\n // Handle clicking the element when hotkey is pressed\n const handleHotkeyPress = () => {\n if (props.disabled || !wrapperElement.value) return;\n\n // Try finding a clickable element within our wrapper\n let clickableElement = wrapperElement.value.querySelector(\n 'button, a, input[type=\"button\"], [role=\"button\"]'\n ) as HTMLElement;\n\n // If no standard clickable element found, try to find navigation or list items\n if (!clickableElement) {\n clickableElement = wrapperElement.value.querySelector(\n 'v-list-item, .v-list-item, router-link, .router-link, a, [to]'\n ) as HTMLElement;\n }\n\n // If still no element found, try the wrapper itself - it might be clickable\n if (\n !clickableElement &&\n (wrapperElement.value.hasAttribute('to') ||\n wrapperElement.value.tagName === 'A' ||\n wrapperElement.value.classList.contains('v-list-item'))\n ) {\n clickableElement = wrapperElement.value;\n }\n\n // Get closest parent list item or router link if we found a title/content element\n if (!clickableElement) {\n const closestClickableParent = wrapperElement.value.closest('v-list-item, .v-list-item, a, [to]');\n if (closestClickableParent) {\n clickableElement = closestClickableParent as HTMLElement;\n }\n }\n\n if (clickableElement) {\n if (clickableElement.hasAttribute('to')) {\n // Handle router-link style navigation\n const routePath = clickableElement.getAttribute('to');\n if (routePath && window.location.pathname !== routePath) {\n // Use parent router if available (Vue component)\n const router = (window as any).$nuxt?.$router || (window as any).$router;\n if (router && typeof router.push === 'function') {\n router.push(routePath);\n } else {\n // Fallback to regular navigation\n window.location.pathname = routePath;\n }\n }\n emit('hotkey-triggered', props.hotkey);\n } else {\n // Regular click for standard elements\n clickableElement.click();\n emit('hotkey-triggered', props.hotkey);\n }\n } else {\n // If no clickable element found, emit the event for parent handling\n console.log('No clickable element found for hotkey', props.hotkey);\n emit('hotkey-triggered', props.hotkey);\n }\n };\n\n // Register/unregister the hotkey binding\n const registerHotkey = () => {\n if (!props.disabled) {\n SkldrMouseTrap.addBinding({\n hotkey: props.hotkey,\n command: props.command,\n callback: handleHotkeyPress,\n });\n }\n };\n\n const unregisterHotkey = () => {\n if (!props.disabled) {\n SkldrMouseTrap.removeBinding(props.hotkey);\n }\n };\n\n // Watch for changes to the disabled prop\n watch(\n () => props.disabled,\n (newValue) => {\n if (newValue) {\n unregisterHotkey();\n } else {\n registerHotkey();\n }\n }\n );\n\n onMounted(() => {\n // Register global keyboard listeners for the Ctrl key\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('keyup', handleKeyUp);\n\n // Register the hotkey\n registerHotkey();\n });\n\n onBeforeUnmount(() => {\n // Clean up event listeners\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('keyup', handleKeyUp);\n\n // Unregister the hotkey\n unregisterHotkey();\n });\n\n return {\n wrapperElement,\n isControlKeyPressed,\n formattedHotkey,\n };\n },\n});\n</script>\n\n<style scoped>\n.sk-mousetrap-tooltip-wrapper {\n display: inline-block;\n position: relative;\n}\n\n.sk-mousetrap-tooltip {\n position: absolute;\n background-color: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 12px;\n white-space: nowrap;\n pointer-events: none;\n z-index: 9999;\n}\n\n.sk-mt-tooltip-top {\n bottom: 100%;\n margin-bottom: 5px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.sk-mt-tooltip-bottom {\n top: 100%;\n margin-top: 5px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.sk-mt-tooltip-left {\n right: 100%;\n margin-right: 5px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n.sk-mt-tooltip-right {\n left: 100%;\n margin-left: 5px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n/* Highlight effects when Ctrl is pressed */\n.sk-mousetrap-highlight-glow {\n box-shadow: 0 0 8px 2px rgba(25, 118, 210, 0.6);\n transition: box-shadow 250ms ease;\n}\n\n.sk-mousetrap-highlight-scale {\n transform: scale(1.03);\n transition: transform 250ms ease;\n}\n\n.sk-mousetrap-highlight-border {\n outline: 2px solid rgba(25, 118, 210, 0.8);\n outline-offset: 2px;\n border-radius: 4px;\n transition: outline 250ms ease, outline-offset 250ms ease;\n}\n\n/* Fade transition */\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 250ms ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n</style>\n","// common-ui/src/components/SnackbarService.ts\nimport { Status } from '@vue-skuilder/common';\n\nexport interface SnackbarOptions {\n text: string;\n status: Status;\n timeout?: number;\n}\n\n// Module for managing the snackbar service\nconst SnackbarServiceModule = (() => {\n // Private variable to hold the instance\n let _instance: { addSnack: (snack: SnackbarOptions) => void } | null = null;\n\n return {\n // Register the instance\n setInstance(instance: { addSnack: (snack: SnackbarOptions) => void }): void {\n _instance = instance;\n },\n\n // Get the current instance\n getInstance(): { addSnack: (snack: SnackbarOptions) => void } | null {\n return _instance;\n },\n\n // Alert user function\n alertUser(msg: SnackbarOptions): void {\n // Try getting the instance\n const snackBarService = _instance;\n if (snackBarService) {\n snackBarService.addSnack(msg);\n return;\n }\n console.error('SnackbarService not found');\n },\n };\n})();\n\nexport const { setInstance, getInstance, alertUser } = SnackbarServiceModule;\n","<template>\n <div>\n <v-snackbar\n v-for=\"snack in snacks\"\n :key=\"snacks.indexOf(snack)\"\n v-model=\"show[snacks.indexOf(snack)]\"\n :timeout=\"snack.timeout\"\n location=\"bottom right\"\n :color=\"getColor(snack)\"\n >\n <div class=\"d-flex align-center justify-space-between w-100\">\n <span>{{ snack.text }}</span>\n <v-btn icon variant=\"text\" @click=\"close()\">\n <v-icon>mdi-close</v-icon>\n </v-btn>\n </div>\n </v-snackbar>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { Status } from '@vue-skuilder/common';\nimport { SnackbarOptions, setInstance } from './SnackbarService';\n\nexport default defineComponent({\n name: 'SnackbarService',\n\n data() {\n return {\n /**\n * A history of snacks served in this session.\n *\n * Possible future work: write these to localstorage/pouchdb\n * for persistance\n */\n snacks: [] as SnackbarOptions[],\n show: [] as boolean[],\n };\n },\n mounted() {\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n setInstance(this);\n },\n\n methods: {\n addSnack(snack: SnackbarOptions): void {\n this.snacks.push(snack);\n this.show.push(true);\n },\n\n close(): void {\n this.show.pop();\n this.show.push(false);\n },\n\n getColor(snack: SnackbarOptions): string | undefined {\n if (snack.status === Status.ok) {\n return 'success';\n } else if (snack.status === Status.error) {\n return 'error';\n } else if (snack.status === Status.warning) {\n return 'yellow';\n }\n return undefined;\n },\n },\n});\n</script>\n","<template>\n <div>\n <v-snackbar\n v-for=\"snack in snacks\"\n :key=\"snacks.indexOf(snack)\"\n v-model=\"show[snacks.indexOf(snack)]\"\n :timeout=\"snack.timeout\"\n location=\"bottom right\"\n :color=\"getColor(snack)\"\n >\n <div class=\"d-flex align-center justify-space-between w-100\">\n <span>{{ snack.text }}</span>\n <v-btn icon variant=\"text\" @click=\"close()\">\n <v-icon>mdi-close</v-icon>\n </v-btn>\n </div>\n </v-snackbar>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { Status } from '@vue-skuilder/common';\nimport { SnackbarOptions, setInstance } from './SnackbarService';\n\nexport default defineComponent({\n name: 'SnackbarService',\n\n data() {\n return {\n /**\n * A history of snacks served in this session.\n *\n * Possible future work: write these to localstorage/pouchdb\n * for persistance\n */\n snacks: [] as SnackbarOptions[],\n show: [] as boolean[],\n };\n },\n mounted() {\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n setInstance(this);\n },\n\n methods: {\n addSnack(snack: SnackbarOptions): void {\n this.snacks.push(snack);\n this.show.push(true);\n },\n\n close(): void {\n this.show.pop();\n this.show.push(false);\n },\n\n getColor(snack: SnackbarOptions): string | undefined {\n if (snack.status === Status.ok) {\n return 'success';\n } else if (snack.status === Status.error) {\n return 'error';\n } else if (snack.status === Status.warning) {\n return 'yellow';\n }\n return undefined;\n },\n },\n});\n</script>\n","<template>\n <v-toolbar density=\"compact\">\n <v-toolbar-title>\n <span>{{ title }}</span>\n <span v-if=\"subtitle\" class=\"ms-2 text-subtitle-2\" data-cy=\"paginating-toolbar-subtitle\">{{ subtitle }}</span>\n </v-toolbar-title>\n <v-spacer></v-spacer>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == 1\" @click=\"$emit('first')\">\n <v-icon>mdi-page-first</v-icon>\n </v-btn>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == 1\" @click=\"$emit('prev')\">\n <v-icon>mdi-chevron-left</v-icon>\n </v-btn>\n\n <v-select\n :model-value=\"page\"\n :items=\"pages\"\n class=\"pageSelect\"\n density=\"compact\"\n hide-details\n :return-object=\"false\"\n variant=\"outlined\"\n @update:model-value=\"(val: unknown) => $emit('set-page', val)\"\n >\n <template #selection=\"{ item }\">\n {{ item.value }}\n </template>\n </v-select>\n\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == pages.length\" @click=\"$emit('next')\">\n <v-icon>mdi-chevron-right</v-icon>\n </v-btn>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == pages.length\" @click=\"$emit('last')\">\n <v-icon>mdi-page-last</v-icon>\n </v-btn>\n </v-toolbar>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport type { PaginatingToolbarProps, PaginatingToolbarEvents } from './PaginatingToolbar.types';\n\nexport default defineComponent({\n name: 'PaginatingToolbar',\n\n props: {\n pages: {\n type: Array as PropType<number[]>,\n required: true,\n },\n page: {\n type: Number,\n required: true,\n },\n title: {\n type: String,\n required: false,\n default: '',\n },\n subtitle: {\n type: String,\n required: false,\n default: '',\n },\n } as const,\n\n emits: ['first', 'prev', 'next', 'last', 'set-page'],\n});\n</script>\n\n<style scoped>\n.pageSelect {\n max-width: 60px;\n}\n</style>\n","<template>\n <v-toolbar density=\"compact\">\n <v-toolbar-title>\n <span>{{ title }}</span>\n <span v-if=\"subtitle\" class=\"ms-2 text-subtitle-2\" data-cy=\"paginating-toolbar-subtitle\">{{ subtitle }}</span>\n </v-toolbar-title>\n <v-spacer></v-spacer>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == 1\" @click=\"$emit('first')\">\n <v-icon>mdi-page-first</v-icon>\n </v-btn>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == 1\" @click=\"$emit('prev')\">\n <v-icon>mdi-chevron-left</v-icon>\n </v-btn>\n\n <v-select\n :model-value=\"page\"\n :items=\"pages\"\n class=\"pageSelect\"\n density=\"compact\"\n hide-details\n :return-object=\"false\"\n variant=\"outlined\"\n @update:model-value=\"(val: unknown) => $emit('set-page', val)\"\n >\n <template #selection=\"{ item }\">\n {{ item.value }}\n </template>\n </v-select>\n\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == pages.length\" @click=\"$emit('next')\">\n <v-icon>mdi-chevron-right</v-icon>\n </v-btn>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == pages.length\" @click=\"$emit('last')\">\n <v-icon>mdi-page-last</v-icon>\n </v-btn>\n </v-toolbar>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport type { PaginatingToolbarProps, PaginatingToolbarEvents } from './PaginatingToolbar.types';\n\nexport default defineComponent({\n name: 'PaginatingToolbar',\n\n props: {\n pages: {\n type: Array as PropType<number[]>,\n required: true,\n },\n page: {\n type: Number,\n required: true,\n },\n title: {\n type: String,\n required: false,\n default: '',\n },\n subtitle: {\n type: String,\n required: false,\n default: '',\n },\n } as const,\n\n emits: ['first', 'prev', 'next', 'last', 'set-page'],\n});\n</script>\n\n<style scoped>\n.pageSelect {\n max-width: 60px;\n}\n</style>\n","<template>\n <div class=\"card-search\">\n <v-text-field\n v-model=\"query\"\n label=\"Search for cards...\"\n append-icon=\"mdi-magnify\"\n @click:append=\"search\"\n @keydown.enter=\"search\"\n ></v-text-field>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n name: 'CardSearch',\n emits: {\n search: (query: string) => typeof query === 'string'\n },\n data() {\n return {\n query: '',\n };\n },\n methods: {\n search() {\n this.$emit('search', this.query);\n },\n },\n});\n</script>\n","<template>\n <div class=\"card-search\">\n <v-text-field\n v-model=\"query\"\n label=\"Search for cards...\"\n append-icon=\"mdi-magnify\"\n @click:append=\"search\"\n @keydown.enter=\"search\"\n ></v-text-field>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n name: 'CardSearch',\n emits: {\n search: (query: string) => typeof query === 'string'\n },\n data() {\n return {\n query: '',\n };\n },\n methods: {\n search() {\n this.$emit('search', this.query);\n },\n },\n});\n</script>\n","<template>\n <div class=\"card-search-results\">\n <div v-if=\"loading\">Loading...</div>\n <div v-else-if=\"error\">{{ error }}</div>\n <div v-else>\n <v-list>\n <v-list-item \n v-for=\"card in cards\" \n :key=\"card._id\"\n @click=\"selectCard(card)\"\n :class=\"{'selected-card': card._id === selectedCardId}\"\n class=\"cursor-pointer\"\n >\n <v-list-item-title>{{ card._id }}</v-list-item-title>\n <v-list-item-subtitle>Course: {{ card.courseId }}</v-list-item-subtitle>\n </v-list-item>\n </v-list>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { DataLayerProvider, CardData } from '@vue-skuilder/db';\n\ninterface CardWithCourse extends CardData {\n courseId: string;\n}\n\nexport default defineComponent({\n name: 'CardSearchResults',\n emits: {\n 'card-selected': (payload: { cardId: string; courseId: string }) => \n typeof payload.cardId === 'string' && typeof payload.courseId === 'string'\n },\n props: {\n query: {\n type: String,\n required: true,\n },\n dataLayer: {\n type: Object as PropType<DataLayerProvider>,\n required: true,\n },\n courseFilter: {\n type: String as PropType<string | null>,\n default: null,\n },\n },\n data() {\n return {\n cards: [] as CardWithCourse[],\n loading: false,\n error: null as string | null,\n selectedCardId: null as string | null,\n };\n },\n watch: {\n query: {\n immediate: true,\n handler(newQuery) {\n if (newQuery) {\n this.fetchResults(newQuery);\n }\n },\n },\n },\n methods: {\n async fetchResults(query: string) {\n this.loading = true;\n this.error = null;\n try {\n let courseIds: string[] = [];\n \n // Get course IDs efficiently\n if (this.courseFilter) {\n // Single course search - no need to fetch all courses\n courseIds = [this.courseFilter];\n console.log(`Filtering search to course: ${this.courseFilter}`);\n } else {\n // Get all course IDs without expensive config lookups\n const { CourseLookup } = await import('@vue-skuilder/db');\n const lookupCourses = await CourseLookup.allCourseWare();\n courseIds = lookupCourses.map(c => c._id).filter(Boolean);\n console.log(`Searching across all ${courseIds.length} courses`);\n }\n \n const allCards: CardWithCourse[] = [];\n\n for (const courseId of courseIds) {\n const courseDB = this.dataLayer.getCourseDB(courseId);\n const cards = await courseDB.searchCards(query);\n \n for (const card of cards) {\n allCards.push({\n ...card,\n courseId: courseId,\n });\n }\n }\n this.cards = allCards;\n console.log(`Search completed: found ${allCards.length} cards across ${courseIds.length} courses`);\n } catch (e) {\n this.error = 'Error fetching search results.';\n console.error('Search error:', e);\n } finally {\n this.loading = false;\n }\n },\n \n selectCard(card: CardWithCourse) {\n this.selectedCardId = card._id;\n this.$emit('card-selected', { cardId: card._id, courseId: card.courseId });\n },\n },\n});\n</script>\n\n<style scoped>\n.selected-card {\n background-color: #e0f2f7; /* Light blue background */\n border-left: 4px solid #2196f3; /* Blue left border */\n}\n\n.cursor-pointer {\n cursor: pointer;\n}\n</style>\n","<template>\n <div class=\"card-search-results\">\n <div v-if=\"loading\">Loading...</div>\n <div v-else-if=\"error\">{{ error }}</div>\n <div v-else>\n <v-list>\n <v-list-item \n v-for=\"card in cards\" \n :key=\"card._id\"\n @click=\"selectCard(card)\"\n :class=\"{'selected-card': card._id === selectedCardId}\"\n class=\"cursor-pointer\"\n >\n <v-list-item-title>{{ card._id }}</v-list-item-title>\n <v-list-item-subtitle>Course: {{ card.courseId }}</v-list-item-subtitle>\n </v-list-item>\n </v-list>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { DataLayerProvider, CardData } from '@vue-skuilder/db';\n\ninterface CardWithCourse extends CardData {\n courseId: string;\n}\n\nexport default defineComponent({\n name: 'CardSearchResults',\n emits: {\n 'card-selected': (payload: { cardId: string; courseId: string }) => \n typeof payload.cardId === 'string' && typeof payload.courseId === 'string'\n },\n props: {\n query: {\n type: String,\n required: true,\n },\n dataLayer: {\n type: Object as PropType<DataLayerProvider>,\n required: true,\n },\n courseFilter: {\n type: String as PropType<string | null>,\n default: null,\n },\n },\n data() {\n return {\n cards: [] as CardWithCourse[],\n loading: false,\n error: null as string | null,\n selectedCardId: null as string | null,\n };\n },\n watch: {\n query: {\n immediate: true,\n handler(newQuery) {\n if (newQuery) {\n this.fetchResults(newQuery);\n }\n },\n },\n },\n methods: {\n async fetchResults(query: string) {\n this.loading = true;\n this.error = null;\n try {\n let courseIds: string[] = [];\n \n // Get course IDs efficiently\n if (this.courseFilter) {\n // Single course search - no need to fetch all courses\n courseIds = [this.courseFilter];\n console.log(`Filtering search to course: ${this.courseFilter}`);\n } else {\n // Get all course IDs without expensive config lookups\n const { CourseLookup } = await import('@vue-skuilder/db');\n const lookupCourses = await CourseLookup.allCourseWare();\n courseIds = lookupCourses.map(c => c._id).filter(Boolean);\n console.log(`Searching across all ${courseIds.length} courses`);\n }\n \n const allCards: CardWithCourse[] = [];\n\n for (const courseId of courseIds) {\n const courseDB = this.dataLayer.getCourseDB(courseId);\n const cards = await courseDB.searchCards(query);\n \n for (const card of cards) {\n allCards.push({\n ...card,\n courseId: courseId,\n });\n }\n }\n this.cards = allCards;\n console.log(`Search completed: found ${allCards.length} cards across ${courseIds.length} courses`);\n } catch (e) {\n this.error = 'Error fetching search results.';\n console.error('Search error:', e);\n } finally {\n this.loading = false;\n }\n },\n \n selectCard(card: CardWithCourse) {\n this.selectedCardId = card._id;\n this.$emit('card-selected', { cardId: card._id, courseId: card.courseId });\n },\n },\n});\n</script>\n\n<style scoped>\n.selected-card {\n background-color: #e0f2f7; /* Light blue background */\n border-left: 4px solid #2196f3; /* Blue left border */\n}\n\n.cursor-pointer {\n cursor: pointer;\n}\n</style>\n","<template>\n <div class=\"card-history-viewer\">\n <h3 v-if=\"userId\">Card History for User: {{ userId }}</h3>\n <div v-if=\"loading\">Loading...</div>\n <div v-else-if=\"error\" class=\"error-message\">{{ error }}</div>\n <div v-else-if=\"cardHistory\">\n <v-card class=\"mb-4\">\n <v-card-title>Summary</v-card-title>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title\n >Best Interval: {{ cardHistory.bestInterval }} seconds ({{ bestIntervalHumanized }})</v-list-item-title\n >\n <v-list-item-subtitle>The to-date largest interval between successful card reviews.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title>Lapses: {{ cardHistory.lapses }}</v-list-item-title>\n <v-list-item-subtitle>The number of times that a card has been failed in review.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title>Streak: {{ cardHistory.streak }}</v-list-item-title>\n <v-list-item-subtitle>The number of consecutive successful impressions on this card.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n </v-card>\n <v-data-table :headers=\"headers\" :items=\"history\" class=\"elevation-1\"></v-data-table>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { UserDBInterface, UserDBReader, CardHistory, CardRecord, getCardHistoryID } from '@vue-skuilder/db';\nimport moment, { Moment } from 'moment';\n\ninterface FormattedRecord extends CardRecord {\n formattedTimeStamp: string;\n intervalFromPrevious?: number;\n userFriendlyInterval?: string;\n timeSpentSeconds: number;\n}\n\nexport default defineComponent({\n name: 'CardHistoryViewer',\n props: {\n cardId: {\n type: String,\n required: true,\n },\n courseId: {\n type: String,\n required: true,\n },\n userId: {\n type: String,\n required: true,\n },\n userDB: {\n type: Object as PropType<UserDBReader>,\n required: true,\n },\n },\n data() {\n return {\n history: [] as FormattedRecord[],\n cardHistory: null as CardHistory<CardRecord> | null,\n bestIntervalHumanized: '',\n loading: false,\n error: null as string | null,\n headers: [\n { title: 'Timestamp', key: 'formattedTimeStamp' },\n { title: 'Interval', key: 'userFriendlyInterval' },\n { title: 'Time Spent (s)', key: 'timeSpentSeconds' },\n { title: 'Correct?', key: 'isCorrect' },\n { title: 'Performance', key: 'performance' },\n { title: 'Prior Attempts', key: 'priorAttemps' },\n { title: 'User Answer', key: 'userAnswer' },\n ],\n };\n },\n watch: {\n cardId: {\n immediate: true,\n handler(newCardId) {\n if (newCardId && this.userDB) {\n this.fetchHistory();\n }\n },\n },\n userDB: {\n handler(newUserDB) {\n if (newUserDB && this.cardId) {\n this.fetchHistory();\n }\n },\n },\n },\n methods: {\n async fetchHistory() {\n this.loading = true;\n this.error = null;\n try {\n const cardHistoryID = getCardHistoryID(this.courseId, this.cardId);\n const historyDoc: CardHistory<CardRecord> = await this.userDB.get(cardHistoryID);\n this.cardHistory = historyDoc;\n this.bestIntervalHumanized = moment.duration(historyDoc.bestInterval, 'seconds').humanize();\n\n // Sort records by timestamp and format them\n const sortedRecords = [...historyDoc.records].sort(\n (a, b) => moment(a.timeStamp).valueOf() - moment(b.timeStamp).valueOf()\n );\n\n this.history = sortedRecords.map((record, index) => {\n const currentTime = moment(record.timeStamp);\n const formatted: FormattedRecord = {\n ...record,\n formattedTimeStamp: currentTime.format('YYYY-MM-DD HH:mm:ss'),\n timeSpentSeconds: Math.round(record.timeSpent / 1000),\n isCorrect: (record as any).isCorrect,\n performance: (record as any).performance,\n priorAttemps: (record as any).priorAttemps,\n userAnswer: (record as any).userAnswer,\n };\n\n // Calculate interval from previous record\n if (index > 0) {\n const previousTime = moment(sortedRecords[index - 1].timeStamp);\n const intervalSeconds = currentTime.diff(previousTime, 'seconds', true);\n formatted.intervalFromPrevious = Math.round(intervalSeconds * 100) / 100;\n formatted.userFriendlyInterval = `${formatted.intervalFromPrevious} sec (${moment.duration(intervalSeconds, 'seconds').humanize()})`;\n }\n\n return formatted;\n });\n } catch (e) {\n this.error = 'Error fetching card history.';\n console.error(e);\n } finally {\n this.loading = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.error-message {\n color: #f44336;\n padding: 16px;\n background-color: #ffebee;\n border-radius: 4px;\n margin: 8px 0;\n}\n\n.negative-interval {\n color: #f44336;\n font-weight: bold;\n background-color: #ffebee;\n padding: 2px 4px;\n border-radius: 3px;\n}\n\n.invalid-timestamp {\n color: #f44336;\n font-weight: bold;\n}\n\n.card-history-viewer {\n margin: 16px 0;\n}\n</style>\n","<template>\n <div class=\"card-history-viewer\">\n <h3 v-if=\"userId\">Card History for User: {{ userId }}</h3>\n <div v-if=\"loading\">Loading...</div>\n <div v-else-if=\"error\" class=\"error-message\">{{ error }}</div>\n <div v-else-if=\"cardHistory\">\n <v-card class=\"mb-4\">\n <v-card-title>Summary</v-card-title>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title\n >Best Interval: {{ cardHistory.bestInterval }} seconds ({{ bestIntervalHumanized }})</v-list-item-title\n >\n <v-list-item-subtitle>The to-date largest interval between successful card reviews.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title>Lapses: {{ cardHistory.lapses }}</v-list-item-title>\n <v-list-item-subtitle>The number of times that a card has been failed in review.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title>Streak: {{ cardHistory.streak }}</v-list-item-title>\n <v-list-item-subtitle>The number of consecutive successful impressions on this card.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n </v-card>\n <v-data-table :headers=\"headers\" :items=\"history\" class=\"elevation-1\"></v-data-table>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { UserDBInterface, UserDBReader, CardHistory, CardRecord, getCardHistoryID } from '@vue-skuilder/db';\nimport moment, { Moment } from 'moment';\n\ninterface FormattedRecord extends CardRecord {\n formattedTimeStamp: string;\n intervalFromPrevious?: number;\n userFriendlyInterval?: string;\n timeSpentSeconds: number;\n}\n\nexport default defineComponent({\n name: 'CardHistoryViewer',\n props: {\n cardId: {\n type: String,\n required: true,\n },\n courseId: {\n type: String,\n required: true,\n },\n userId: {\n type: String,\n required: true,\n },\n userDB: {\n type: Object as PropType<UserDBReader>,\n required: true,\n },\n },\n data() {\n return {\n history: [] as FormattedRecord[],\n cardHistory: null as CardHistory<CardRecord> | null,\n bestIntervalHumanized: '',\n loading: false,\n error: null as string | null,\n headers: [\n { title: 'Timestamp', key: 'formattedTimeStamp' },\n { title: 'Interval', key: 'userFriendlyInterval' },\n { title: 'Time Spent (s)', key: 'timeSpentSeconds' },\n { title: 'Correct?', key: 'isCorrect' },\n { title: 'Performance', key: 'performance' },\n { title: 'Prior Attempts', key: 'priorAttemps' },\n { title: 'User Answer', key: 'userAnswer' },\n ],\n };\n },\n watch: {\n cardId: {\n immediate: true,\n handler(newCardId) {\n if (newCardId && this.userDB) {\n this.fetchHistory();\n }\n },\n },\n userDB: {\n handler(newUserDB) {\n if (newUserDB && this.cardId) {\n this.fetchHistory();\n }\n },\n },\n },\n methods: {\n async fetchHistory() {\n this.loading = true;\n this.error = null;\n try {\n const cardHistoryID = getCardHistoryID(this.courseId, this.cardId);\n const historyDoc: CardHistory<CardRecord> = await this.userDB.get(cardHistoryID);\n this.cardHistory = historyDoc;\n this.bestIntervalHumanized = moment.duration(historyDoc.bestInterval, 'seconds').humanize();\n\n // Sort records by timestamp and format them\n const sortedRecords = [...historyDoc.records].sort(\n (a, b) => moment(a.timeStamp).valueOf() - moment(b.timeStamp).valueOf()\n );\n\n this.history = sortedRecords.map((record, index) => {\n const currentTime = moment(record.timeStamp);\n const formatted: FormattedRecord = {\n ...record,\n formattedTimeStamp: currentTime.format('YYYY-MM-DD HH:mm:ss'),\n timeSpentSeconds: Math.round(record.timeSpent / 1000),\n isCorrect: (record as any).isCorrect,\n performance: (record as any).performance,\n priorAttemps: (record as any).priorAttemps,\n userAnswer: (record as any).userAnswer,\n };\n\n // Calculate interval from previous record\n if (index > 0) {\n const previousTime = moment(sortedRecords[index - 1].timeStamp);\n const intervalSeconds = currentTime.diff(previousTime, 'seconds', true);\n formatted.intervalFromPrevious = Math.round(intervalSeconds * 100) / 100;\n formatted.userFriendlyInterval = `${formatted.intervalFromPrevious} sec (${moment.duration(intervalSeconds, 'seconds').humanize()})`;\n }\n\n return formatted;\n });\n } catch (e) {\n this.error = 'Error fetching card history.';\n console.error(e);\n } finally {\n this.loading = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.error-message {\n color: #f44336;\n padding: 16px;\n background-color: #ffebee;\n border-radius: 4px;\n margin: 8px 0;\n}\n\n.negative-interval {\n color: #f44336;\n font-weight: bold;\n background-color: #ffebee;\n padding: 2px 4px;\n border-radius: 3px;\n}\n\n.invalid-timestamp {\n color: #f44336;\n font-weight: bold;\n}\n\n.card-history-viewer {\n margin: 16px 0;\n}\n</style>\n","import { type DefineComponent, defineComponent } from 'vue';\nimport { FieldType, DataShape, ViewData, Answer, Evaluation } from '@vue-skuilder/common';\n\n// [ ] #vue3 - post migration, specify this more precisely (no longer a hodge-podge)\nexport type ViewComponent =\n | DefineComponent<unknown, unknown, unknown>\n | ReturnType<typeof defineComponent>;\n\nexport function isDefineComponent(\n v: ViewComponent\n): v is DefineComponent<unknown, unknown, unknown> {\n return (v as DefineComponent<unknown, unknown, unknown>).__isFragment !== undefined;\n}\n\n// export function isComponentOptions(v: ViewComponent): v is ComponentOptions<any> {\n// return (v as ComponentOptions<Vue>).name !== undefined;\n// }\n\n// tslint:disable-next-line:max-classes-per-file\nexport abstract class Displayable {\n public static dataShapes: DataShape[];\n\n public static views: Array<ViewComponent>;\n public static seedData?: Array<unknown>;\n /**\n * True if this displayable content type is meant to have\n * user-submitted questions. False if supplied seedData array\n * is comprehensive for the content type. EG, a SingleDigitAddition\n * type may comprehensively supply 0+0,0+1,...,9+9 as its seed\n * data, and not want any user input.\n */\n public static acceptsUserData: boolean = true;\n\n /**\n *\n */\n constructor(viewData: ViewData[]) {\n if (viewData.length === 0) {\n throw new Error(`\nDisplayable Constructor was called with no view Data.\n `);\n }\n validateData(this.dataShapes(), viewData);\n }\n\n public abstract dataShapes(): DataShape[];\n public abstract views(): Array<ViewComponent>;\n}\n\nfunction validateData(shape: DataShape[], data: ViewData[]) {\n for (let i = 0; i < shape.length; i++) {\n shape[i].fields.forEach((field, j) => {\n console.log(`[Displayable] shape[${i}].field[${j}]:\\n ${JSON.stringify(field)}`);\n if (data[i][field.name] === undefined && field.type !== FieldType.MEDIA_UPLOADS) {\n // throw new Error(`field validation failed:\\n\\t${field.name}, (${field.type})`);\n console.warn(`[Displayable] missing data`);\n }\n });\n }\n}\n\n// tslint:disable-next-line:max-classes-per-file\nexport abstract class Question extends Displayable {\n /**\n * returns a yes/no evaluation of a user's answer. Informs the SRS\n * algorithm's decision to expand or reset a card's spacing\n * @param answer\n */\n protected abstract isCorrect(answer: Answer): boolean;\n /**\n * returns a number from [0,1] representing the user's performance on the question,\n * which informs elo adjustments and SRS multipliers\n *\n * @param answer the user's answer\n * @param timeSpent the time the user spent on the card in milliseconds\n * @returns a rating of the user's displayed skill, from 0-1\n */\n protected displayedSkill(answer: Answer, timeSpent: number): number {\n console.warn(`Question is running the reference implementation of displayedSkill.\n Consider overriding!`);\n // experts should answer this question in <= 5 secnods (5000 ms)\n const expertSpeed = 5000;\n const userSpeed = Math.min(timeSpent, 10 * expertSpeed);\n\n // if userResponse is > 10 x expertSpeed, discount as probably afk / distracted ?\n\n const speedPenalty = userSpeed / expertSpeed;\n const speedPenaltyMultiplier = userSpeed > expertSpeed ? Math.pow(0.8, speedPenalty) : 1;\n\n let ret = this.isCorrect(answer) ? 1 : 0;\n\n ret = ret * speedPenaltyMultiplier;\n\n return Math.min(ret, 1);\n }\n\n /**\n *\n * @param answer the student's answer\n * @param timeSpent the amount of time spent in ms\n * @returns\n */\n public evaluate(answer: Answer, timeSpent: number): Evaluation {\n return {\n isCorrect: this.isCorrect(answer),\n performance: this.displayedSkill(answer, timeSpent),\n };\n }\n\n /*\n TODO:\n\n This class and its interface are critical in this app - it defines the app's\n methodology, or its architecture of methodologies, for making inferences based on\n student performance.\n\n Some future directions:\n\n displayedSkill() should receive contextual data as well as the direct report of\n a user's interaction with this card. Context includes, eg, the status of the current\n study session (is the user on tilt? have there been leading Qs? have there been distractors?),\n the status of the user's interaction with the card (new? mature? history of resets?)\n\n displayedSkill() should reach out to a course (or just the card, or card-type?) in order to receive\n custom skill-dimension evaluators. EG: a card in the ear training course tagged 'articulation'\n could trigger a fetch of a particular evaluator fcn for articulation. In general, we\n want high-dimension evaluations.\n\n answers should be processed for the likelihood of non-substantive-incorrectness. eg,\n 7 * 4 = 8 is much more likely to be a typo than a substantive error when compared\n with 7 * 4 = 21\n */\n}\n","/**\n * Authentication API service for interacting with Express backend auth endpoints.\n * Uses configurable API base path from environment or runtime config.\n */\n\n// Global runtime configuration (set by consuming app)\ndeclare global {\n interface Window {\n __SKUILDER_CONFIG__?: {\n apiBase?: string;\n [key: string]: unknown;\n };\n }\n}\n\n// Get API base path: check env first, then runtime config, then fallback to empty\nconst getApiBase = (): string => {\n let source = 'fallback (empty string)';\n let result = '';\n\n // Try import.meta.env first (build-time, only works in consuming app builds)\n if (typeof import.meta !== 'undefined' && import.meta.env) {\n const base = import.meta.env.VITE_API_BASE_URL;\n if (base) {\n const cleaned = base.replace(/\\/$/, '');\n result = cleaned.startsWith('/') ? cleaned : `/${cleaned}`;\n source = 'import.meta.env.VITE_API_BASE_URL';\n }\n }\n\n // Fallback to runtime config (works in library builds)\n if (!result && typeof window !== 'undefined' && window.__SKUILDER_CONFIG__?.apiBase) {\n const base = window.__SKUILDER_CONFIG__.apiBase;\n const cleaned = base.replace(/\\/$/, '');\n result = cleaned.startsWith('/') ? cleaned : `/${cleaned}`;\n source = 'window.__SKUILDER_CONFIG__.apiBase';\n }\n\n console.log('[authAPI] getApiBase() called');\n console.log('[authAPI] source:', source);\n console.log('[authAPI] result:', result);\n\n return result;\n};\n\nexport interface AuthResponse {\n ok: boolean;\n error?: string;\n}\n\nexport interface VerifyEmailResponse extends AuthResponse {\n username?: string;\n}\n\nexport interface Entitlement {\n status: 'trial' | 'paid';\n registrationDate: string;\n purchaseDate?: string;\n expires?: string;\n}\n\nexport type UserEntitlements = Record<string, Entitlement>;\n\nexport interface UserStatusResponse extends AuthResponse {\n username?: string;\n status?: 'pending_verification' | 'verified' | 'suspended';\n email?: string | null;\n entitlements?: UserEntitlements;\n}\n\n/**\n * Triggers verification email send for a newly created account.\n *\n * @param username - Username of the newly created account\n * @param email - User's email address (passed directly to avoid race conditions with DB sync)\n * @param origin - Optional frontend origin URL (e.g., window.location.origin)\n * Used to construct the correct verification link in the email.\n * If not provided, backend falls back to APP_URL env var.\n */\nexport async function sendVerificationEmail(\n username: string,\n email: string,\n origin?: string\n): Promise<AuthResponse> {\n try {\n const body: { username: string; email: string; origin?: string } = { username, email };\n if (origin) {\n body.origin = origin;\n }\n\n const response = await fetch(`${getApiBase()}/auth/send-verification`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Send verification email error:', errorText);\n return {\n ok: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n return await response.json();\n } catch (error) {\n console.error('Send verification email fetch error:', error);\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Network error',\n };\n }\n}\n\n/**\n * Verify user email using token from magic link.\n */\nexport async function verifyEmail(token: string): Promise<VerifyEmailResponse> {\n try {\n const response = await fetch(`${getApiBase()}/auth/verify`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ token }),\n });\n\n if (!response.ok) {\n return {\n ok: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n return await response.json();\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Network error',\n };\n }\n}\n\n/**\n * Get current user's account status (verification status, email).\n * Requires valid AuthSession cookie.\n */\nexport async function getUserStatus(): Promise<UserStatusResponse> {\n try {\n const response = await fetch(`${getApiBase()}/auth/status`, {\n method: 'GET',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n });\n\n if (!response.ok) {\n return {\n ok: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n return await response.json();\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Network error',\n };\n }\n}\n\n/**\n * Request password reset email for user identified by email.\n *\n * @param email - User's email address\n * @param origin - Optional frontend origin URL (e.g., window.location.origin)\n * Used to construct the correct password reset link in the email.\n * If not provided, backend falls back to APP_URL env var.\n */\nexport async function requestPasswordReset(\n email: string,\n origin?: string\n): Promise<AuthResponse> {\n try {\n const body: { email: string; origin?: string } = { email };\n if (origin) {\n body.origin = origin;\n }\n\n const response = await fetch(`${getApiBase()}/auth/request-reset`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n return {\n ok: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n return await response.json();\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Network error',\n };\n }\n}\n\n/**\n * Reset password using token from magic link.\n */\nexport async function resetPassword(\n token: string,\n newPassword: string\n): Promise<AuthResponse> {\n try {\n const response = await fetch(`${getApiBase()}/auth/reset-password`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ token, newPassword }),\n });\n\n if (!response.ok) {\n return {\n ok: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n return await response.json();\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Network error',\n };\n }\n}\n","import { ref, computed } from 'vue';\nimport { getUserStatus } from '../services/authAPI';\nimport type { Entitlement, UserEntitlements } from '../services/authAPI';\n\nexport interface TrialStatus {\n isActive: boolean;\n isPaid: boolean;\n daysRemaining: number | null;\n expiresDate: string | null;\n}\n\n/**\n * Composable for managing user entitlements and trial status\n *\n * @param courseId - The course identifier to check entitlements for\n * @returns Object with entitlements data and helper methods\n *\n * @example\n * ```typescript\n * const { trialStatus, hasPremiumAccess, fetchEntitlements, loading } = useEntitlements('letterspractice-basic');\n *\n * onMounted(async () => {\n * await fetchEntitlements();\n * console.log('Days remaining:', trialStatus.value.daysRemaining);\n * });\n * ```\n */\nexport function useEntitlements(courseId: string) {\n const entitlements = ref<UserEntitlements>({});\n const loading = ref(false);\n const error = ref<string | null>(null);\n\n /**\n * Get trial status for the specified course\n */\n const trialStatus = computed<TrialStatus>(() => {\n const entitlement: Entitlement | undefined = entitlements.value[courseId];\n\n if (!entitlement) {\n return {\n isActive: false,\n isPaid: false,\n daysRemaining: null,\n expiresDate: null,\n };\n }\n\n if (entitlement.status === 'paid') {\n return {\n isActive: true,\n isPaid: true,\n daysRemaining: null,\n expiresDate: null,\n };\n }\n\n // Trial status\n const expiresDate = entitlement.expires;\n if (!expiresDate) {\n return {\n isActive: true,\n isPaid: false,\n daysRemaining: null,\n expiresDate: null,\n };\n }\n\n const expires = new Date(expiresDate);\n const now = new Date();\n const daysLeft = Math.ceil((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));\n\n return {\n isActive: daysLeft > 0,\n isPaid: false,\n daysRemaining: Math.max(0, daysLeft),\n expiresDate,\n };\n });\n\n /**\n * Whether the user has premium (paid) access to the course\n */\n const hasPremiumAccess = computed<boolean>(() => {\n return trialStatus.value.isPaid;\n });\n\n /**\n * Fetch entitlements from the backend\n */\n async function fetchEntitlements(): Promise<void> {\n loading.value = true;\n error.value = null;\n\n try {\n const result = await getUserStatus();\n if (result.ok) {\n entitlements.value = result.entitlements || {};\n } else {\n error.value = result.error || 'Failed to fetch entitlements';\n console.error('[useEntitlements] Error:', error.value);\n }\n } catch (e) {\n error.value = e instanceof Error ? e.message : 'Unknown error';\n console.error('[useEntitlements] Exception:', e);\n } finally {\n loading.value = false;\n }\n }\n\n return {\n entitlements,\n trialStatus,\n hasPremiumAccess,\n loading,\n error,\n fetchEntitlements,\n };\n}\n","<!-- @vue-skuilder/common-ui/src/components/StudySessionTimer.vue -->\n<template>\n <v-tooltip location=\"right\" :open-delay=\"0\" :close-delay=\"200\" color=\"secondary\" class=\"text-subtitle-1\">\n <template #activator=\"{ props }\">\n <div class=\"timer-container\" v-bind=\"props\" @mouseenter=\"hovered = true\" @mouseleave=\"hovered = false\">\n <v-progress-circular\n alt=\"Time remaining in study session\"\n size=\"64\"\n width=\"8\"\n rotate=\"0\"\n :color=\"timerColor\"\n :model-value=\"percentageRemaining\"\n >\n <v-btn\n v-if=\"timeRemaining > 0 && hovered\"\n icon\n color=\"transparent\"\n location=\"bottom left\"\n @click=\"addSessionTime\"\n >\n <v-icon size=\"large\">mdi-plus</v-icon>\n </v-btn>\n </v-progress-circular>\n </div>\n </template>\n {{ formattedTimeRemaining }}\n </v-tooltip>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, computed, ref } from 'vue';\n\nexport default defineComponent({\n name: 'StudySessionTimer',\n\n props: {\n /**\n * Time remaining in seconds\n */\n timeRemaining: {\n type: Number,\n required: true,\n },\n /**\n * Total session time limit in minutes\n */\n sessionTimeLimit: {\n type: Number,\n required: true,\n default: 5,\n },\n },\n\n emits: ['add-time'],\n\n setup(props, { emit }) {\n const hovered = ref(false);\n\n /**\n * Formats the time remaining into a readable string\n */\n const formattedTimeRemaining = computed(() => {\n let timeString = '';\n const seconds = props.timeRemaining;\n\n if (seconds > 60) {\n timeString = Math.floor(seconds / 60).toString() + ':';\n }\n\n const secondsRemaining = seconds % 60;\n timeString += secondsRemaining >= 10 ? secondsRemaining : '0' + secondsRemaining;\n\n if (seconds <= 60) {\n timeString += ' seconds';\n }\n\n timeString += ' left!';\n\n return timeString;\n });\n\n /**\n * Calculates the percentage of time remaining for the progress indicator\n */\n const percentageRemaining = computed(() => {\n return props.timeRemaining > 60\n ? 100 * (props.timeRemaining / (60 * props.sessionTimeLimit))\n : 100 * (props.timeRemaining / 60);\n });\n\n /**\n * Determines the color of the timer based on time remaining\n */\n const timerColor = computed(() => {\n return props.timeRemaining > 60 ? 'primary' : 'orange darken-3';\n });\n\n /**\n * Handles adding time to the session\n */\n const addSessionTime = () => {\n emit('add-time');\n };\n\n return {\n hovered,\n formattedTimeRemaining,\n percentageRemaining,\n timerColor,\n addSessionTime,\n };\n },\n});\n</script>\n\n<style scoped>\n.timer-container {\n display: inline-flex;\n cursor: pointer;\n}\n</style>\n","<!-- @vue-skuilder/common-ui/src/components/StudySessionTimer.vue -->\n<template>\n <v-tooltip location=\"right\" :open-delay=\"0\" :close-delay=\"200\" color=\"secondary\" class=\"text-subtitle-1\">\n <template #activator=\"{ props }\">\n <div class=\"timer-container\" v-bind=\"props\" @mouseenter=\"hovered = true\" @mouseleave=\"hovered = false\">\n <v-progress-circular\n alt=\"Time remaining in study session\"\n size=\"64\"\n width=\"8\"\n rotate=\"0\"\n :color=\"timerColor\"\n :model-value=\"percentageRemaining\"\n >\n <v-btn\n v-if=\"timeRemaining > 0 && hovered\"\n icon\n color=\"transparent\"\n location=\"bottom left\"\n @click=\"addSessionTime\"\n >\n <v-icon size=\"large\">mdi-plus</v-icon>\n </v-btn>\n </v-progress-circular>\n </div>\n </template>\n {{ formattedTimeRemaining }}\n </v-tooltip>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, computed, ref } from 'vue';\n\nexport default defineComponent({\n name: 'StudySessionTimer',\n\n props: {\n /**\n * Time remaining in seconds\n */\n timeRemaining: {\n type: Number,\n required: true,\n },\n /**\n * Total session time limit in minutes\n */\n sessionTimeLimit: {\n type: Number,\n required: true,\n default: 5,\n },\n },\n\n emits: ['add-time'],\n\n setup(props, { emit }) {\n const hovered = ref(false);\n\n /**\n * Formats the time remaining into a readable string\n */\n const formattedTimeRemaining = computed(() => {\n let timeString = '';\n const seconds = props.timeRemaining;\n\n if (seconds > 60) {\n timeString = Math.floor(seconds / 60).toString() + ':';\n }\n\n const secondsRemaining = seconds % 60;\n timeString += secondsRemaining >= 10 ? secondsRemaining : '0' + secondsRemaining;\n\n if (seconds <= 60) {\n timeString += ' seconds';\n }\n\n timeString += ' left!';\n\n return timeString;\n });\n\n /**\n * Calculates the percentage of time remaining for the progress indicator\n */\n const percentageRemaining = computed(() => {\n return props.timeRemaining > 60\n ? 100 * (props.timeRemaining / (60 * props.sessionTimeLimit))\n : 100 * (props.timeRemaining / 60);\n });\n\n /**\n * Determines the color of the timer based on time remaining\n */\n const timerColor = computed(() => {\n return props.timeRemaining > 60 ? 'primary' : 'orange darken-3';\n });\n\n /**\n * Handles adding time to the session\n */\n const addSessionTime = () => {\n emit('add-time');\n };\n\n return {\n hovered,\n formattedTimeRemaining,\n percentageRemaining,\n timerColor,\n addSessionTime,\n };\n },\n});\n</script>\n\n<style scoped>\n.timer-container {\n display: inline-flex;\n cursor: pointer;\n}\n</style>\n","<template>\n <!-- Frameless mode: direct render -->\n <component\n v-if=\"frameless\"\n :is=\"view\"\n ref=\"activeView\"\n :key=\"course_id + '-' + card_id + '-' + sessionOrder\"\n :data=\"data\"\n :modify-difficulty=\"user_elo.global.score - card_elo\"\n class=\"cardView\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"$emit('requestReplan', $event)\"\n @ready-to-advance=\"$emit('readyToAdvance')\"\n />\n\n <!-- Traditional mode: v-card wrapper -->\n <v-card v-else elevation=\"12\">\n <component\n :is=\"view\"\n ref=\"activeView\"\n :key=\"course_id + '-' + card_id + '-' + sessionOrder\"\n :data=\"data\"\n :modify-difficulty=\"user_elo.global.score - card_elo\"\n class=\"cardView ma-2 pa-2\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"$emit('requestReplan', $event)\"\n @ready-to-advance=\"$emit('readyToAdvance')\"\n />\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { CardRecord } from '@vue-skuilder/db';\nimport { CourseElo, ViewData } from '@vue-skuilder/common';\nimport { ViewComponent } from '../../composables';\n\ninterface CardViewerRefs {\n activeView: ViewComponent;\n}\n\nexport default defineComponent({\n name: 'CardViewer',\n\n ref: {} as CardViewerRefs,\n\n props: {\n sessionOrder: {\n type: Number,\n required: false,\n default: 0,\n },\n card_id: {\n type: String as () => PouchDB.Core.DocumentId,\n required: true,\n default: '',\n },\n course_id: {\n type: String,\n required: true,\n default: '',\n },\n view: {\n type: [Function, Object] as PropType<ViewComponent>,\n required: true,\n },\n data: {\n type: Array as () => ViewData[],\n required: true,\n },\n user_elo: {\n type: Object as () => CourseElo,\n default: () => ({\n global: {\n score: 1000,\n count: 0,\n },\n tags: {},\n misc: {},\n }),\n },\n card_elo: {\n type: Number,\n default: 1000,\n },\n frameless: {\n type: Boolean,\n default: false,\n },\n },\n\n emits: ['emitResponse', 'requestReplan', 'readyToAdvance'],\n\n methods: {\n processResponse(r: CardRecord): void {\n console.log(`\n Card was displayed at ${r.timeStamp}\n User spent ${r.timeSpent} milliseconds with the card.\n `);\n this.$emit('emitResponse', r);\n },\n },\n});\n</script>\n\n<style scoped>\n.cardView {\n padding: 15px;\n border-radius: 8px;\n}\n\n</style>\n","<template>\n <!-- Frameless mode: direct render -->\n <component\n v-if=\"frameless\"\n :is=\"view\"\n ref=\"activeView\"\n :key=\"course_id + '-' + card_id + '-' + sessionOrder\"\n :data=\"data\"\n :modify-difficulty=\"user_elo.global.score - card_elo\"\n class=\"cardView\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"$emit('requestReplan', $event)\"\n @ready-to-advance=\"$emit('readyToAdvance')\"\n />\n\n <!-- Traditional mode: v-card wrapper -->\n <v-card v-else elevation=\"12\">\n <component\n :is=\"view\"\n ref=\"activeView\"\n :key=\"course_id + '-' + card_id + '-' + sessionOrder\"\n :data=\"data\"\n :modify-difficulty=\"user_elo.global.score - card_elo\"\n class=\"cardView ma-2 pa-2\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"$emit('requestReplan', $event)\"\n @ready-to-advance=\"$emit('readyToAdvance')\"\n />\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { CardRecord } from '@vue-skuilder/db';\nimport { CourseElo, ViewData } from '@vue-skuilder/common';\nimport { ViewComponent } from '../../composables';\n\ninterface CardViewerRefs {\n activeView: ViewComponent;\n}\n\nexport default defineComponent({\n name: 'CardViewer',\n\n ref: {} as CardViewerRefs,\n\n props: {\n sessionOrder: {\n type: Number,\n required: false,\n default: 0,\n },\n card_id: {\n type: String as () => PouchDB.Core.DocumentId,\n required: true,\n default: '',\n },\n course_id: {\n type: String,\n required: true,\n default: '',\n },\n view: {\n type: [Function, Object] as PropType<ViewComponent>,\n required: true,\n },\n data: {\n type: Array as () => ViewData[],\n required: true,\n },\n user_elo: {\n type: Object as () => CourseElo,\n default: () => ({\n global: {\n score: 1000,\n count: 0,\n },\n tags: {},\n misc: {},\n }),\n },\n card_elo: {\n type: Number,\n default: 1000,\n },\n frameless: {\n type: Boolean,\n default: false,\n },\n },\n\n emits: ['emitResponse', 'requestReplan', 'readyToAdvance'],\n\n methods: {\n processResponse(r: CardRecord): void {\n console.log(`\n Card was displayed at ${r.timeStamp}\n User spent ${r.timeSpent} milliseconds with the card.\n `);\n this.$emit('emitResponse', r);\n },\n },\n});\n</script>\n\n<style scoped>\n.cardView {\n padding: 15px;\n border-radius: 8px;\n}\n\n</style>\n","<template>\n <v-card v-if=\"sessionController\" class=\"session-debug ma-2\" elevation=\"2\">\n <v-card-title class=\"text-caption bg-grey-darken-3\">\n Session Controller Debug\n <v-spacer></v-spacer>\n <v-icon size=\"small\">mdi-bug</v-icon>\n </v-card-title>\n\n <v-card-text class=\"pa-2\">\n <v-row dense>\n <!-- Review Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-calendar-check</v-icon>\n <strong>Review Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.reviewQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.reviewQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.reviewQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.reviewQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.reviewQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.reviewQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n\n <!-- New Cards Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-file-document-plus</v-icon>\n <strong>New Cards Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.newQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.newQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.newQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.newQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.newQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.newQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n\n <!-- Failed Cards Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-alert-circle</v-icon>\n <strong>Failed Cards Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.failedQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.failedQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.failedQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.failedQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.failedQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.failedQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n </v-row>\n\n <!-- Hydrated Cards Cache -->\n <v-row dense>\n <v-col cols=\"12\">\n <v-divider class=\"my-2\"></v-divider>\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-database</v-icon>\n <strong>Hydrated Cards Cache</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Cached: {{ debugInfo.hydratedCache.count }}</span>\n <span class=\"text-caption ml-2\">Failed Cache: {{ debugInfo.hydratedCache.failedCacheSize }}</span>\n </div>\n <div v-if=\"debugInfo.hydratedCache.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.hydratedCache.items.slice(0, 8)\"\n :key=\"idx\"\n class=\"debug-item d-inline-block mr-3\"\n >\n <span class=\"text-caption\">{{ item.courseID }}::{{ item.cardID }}</span>\n </div>\n <div v-if=\"debugInfo.hydratedCache.items.length > 8\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.hydratedCache.items.length - 8 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n </v-row>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, computed, ref, onMounted, onUnmounted } from 'vue';\nimport { SessionController } from '@vue-skuilder/db';\n\ninterface QueueDebugInfo {\n length: number;\n dequeueCount: number;\n items: Array<{ courseID: string; cardID: string; status: string }>;\n}\n\ninterface HydratedCacheInfo {\n count: number;\n failedCacheSize: number;\n items: Array<{ courseID: string; cardID: string }>;\n}\n\nexport interface SessionDebugInfo {\n reviewQueue: QueueDebugInfo;\n newQueue: QueueDebugInfo;\n failedQueue: QueueDebugInfo;\n hydratedCache: HydratedCacheInfo;\n}\n\nexport default defineComponent({\n name: 'SessionControllerDebug',\n\n props: {\n sessionController: {\n type: Object as PropType<SessionController<any> | null>,\n required: true,\n },\n },\n\n setup(props) {\n const refreshTrigger = ref(0);\n let pollInterval: NodeJS.Timeout | null = null;\n\n onMounted(() => {\n // Poll every 500ms to update debug display\n pollInterval = setInterval(() => {\n refreshTrigger.value++;\n }, 500);\n });\n\n onUnmounted(() => {\n if (pollInterval) {\n clearInterval(pollInterval);\n }\n });\n\n const debugInfo = computed((): SessionDebugInfo => {\n // Create dependency on refreshTrigger to force updates\n refreshTrigger.value;\n\n if (!props.sessionController) {\n return {\n reviewQueue: { length: 0, dequeueCount: 0, items: [] },\n newQueue: { length: 0, dequeueCount: 0, items: [] },\n failedQueue: { length: 0, dequeueCount: 0, items: [] },\n hydratedCache: { count: 0, failedCacheSize: 0, items: [] },\n };\n }\n\n return props.sessionController.getDebugInfo();\n });\n\n return {\n debugInfo,\n };\n },\n});\n</script>\n\n<style scoped>\n.session-debug {\n font-family: 'Courier New', monospace;\n font-size: 0.75rem;\n max-height: 400px;\n overflow-y: auto;\n}\n\n.debug-section {\n padding: 8px;\n background-color: rgba(255, 255, 255, 0.05);\n border-radius: 4px;\n min-height: 120px;\n}\n\n.debug-header {\n display: flex;\n align-items: center;\n margin-bottom: 4px;\n padding-bottom: 4px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.debug-stats {\n margin-bottom: 8px;\n display: flex;\n gap: 8px;\n}\n\n.debug-items {\n margin-top: 4px;\n padding-left: 8px;\n}\n\n.debug-item {\n padding: 2px 0;\n border-left: 2px solid rgba(255, 255, 255, 0.2);\n padding-left: 6px;\n margin-bottom: 2px;\n}\n</style>\n","<template>\n <v-card v-if=\"sessionController\" class=\"session-debug ma-2\" elevation=\"2\">\n <v-card-title class=\"text-caption bg-grey-darken-3\">\n Session Controller Debug\n <v-spacer></v-spacer>\n <v-icon size=\"small\">mdi-bug</v-icon>\n </v-card-title>\n\n <v-card-text class=\"pa-2\">\n <v-row dense>\n <!-- Review Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-calendar-check</v-icon>\n <strong>Review Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.reviewQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.reviewQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.reviewQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.reviewQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.reviewQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.reviewQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n\n <!-- New Cards Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-file-document-plus</v-icon>\n <strong>New Cards Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.newQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.newQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.newQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.newQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.newQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.newQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n\n <!-- Failed Cards Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-alert-circle</v-icon>\n <strong>Failed Cards Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.failedQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.failedQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.failedQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.failedQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.failedQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.failedQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n </v-row>\n\n <!-- Hydrated Cards Cache -->\n <v-row dense>\n <v-col cols=\"12\">\n <v-divider class=\"my-2\"></v-divider>\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-database</v-icon>\n <strong>Hydrated Cards Cache</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Cached: {{ debugInfo.hydratedCache.count }}</span>\n <span class=\"text-caption ml-2\">Failed Cache: {{ debugInfo.hydratedCache.failedCacheSize }}</span>\n </div>\n <div v-if=\"debugInfo.hydratedCache.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.hydratedCache.items.slice(0, 8)\"\n :key=\"idx\"\n class=\"debug-item d-inline-block mr-3\"\n >\n <span class=\"text-caption\">{{ item.courseID }}::{{ item.cardID }}</span>\n </div>\n <div v-if=\"debugInfo.hydratedCache.items.length > 8\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.hydratedCache.items.length - 8 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n </v-row>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, computed, ref, onMounted, onUnmounted } from 'vue';\nimport { SessionController } from '@vue-skuilder/db';\n\ninterface QueueDebugInfo {\n length: number;\n dequeueCount: number;\n items: Array<{ courseID: string; cardID: string; status: string }>;\n}\n\ninterface HydratedCacheInfo {\n count: number;\n failedCacheSize: number;\n items: Array<{ courseID: string; cardID: string }>;\n}\n\nexport interface SessionDebugInfo {\n reviewQueue: QueueDebugInfo;\n newQueue: QueueDebugInfo;\n failedQueue: QueueDebugInfo;\n hydratedCache: HydratedCacheInfo;\n}\n\nexport default defineComponent({\n name: 'SessionControllerDebug',\n\n props: {\n sessionController: {\n type: Object as PropType<SessionController<any> | null>,\n required: true,\n },\n },\n\n setup(props) {\n const refreshTrigger = ref(0);\n let pollInterval: NodeJS.Timeout | null = null;\n\n onMounted(() => {\n // Poll every 500ms to update debug display\n pollInterval = setInterval(() => {\n refreshTrigger.value++;\n }, 500);\n });\n\n onUnmounted(() => {\n if (pollInterval) {\n clearInterval(pollInterval);\n }\n });\n\n const debugInfo = computed((): SessionDebugInfo => {\n // Create dependency on refreshTrigger to force updates\n refreshTrigger.value;\n\n if (!props.sessionController) {\n return {\n reviewQueue: { length: 0, dequeueCount: 0, items: [] },\n newQueue: { length: 0, dequeueCount: 0, items: [] },\n failedQueue: { length: 0, dequeueCount: 0, items: [] },\n hydratedCache: { count: 0, failedCacheSize: 0, items: [] },\n };\n }\n\n return props.sessionController.getDebugInfo();\n });\n\n return {\n debugInfo,\n };\n },\n});\n</script>\n\n<style scoped>\n.session-debug {\n font-family: 'Courier New', monospace;\n font-size: 0.75rem;\n max-height: 400px;\n overflow-y: auto;\n}\n\n.debug-section {\n padding: 8px;\n background-color: rgba(255, 255, 255, 0.05);\n border-radius: 4px;\n min-height: 120px;\n}\n\n.debug-header {\n display: flex;\n align-items: center;\n margin-bottom: 4px;\n padding-bottom: 4px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.debug-stats {\n margin-bottom: 8px;\n display: flex;\n gap: 8px;\n}\n\n.debug-items {\n margin-top: 4px;\n padding-left: 8px;\n}\n\n.debug-item {\n padding: 2px 0;\n border-left: 2px solid rgba(255, 255, 255, 0.2);\n padding-left: 6px;\n margin-bottom: 2px;\n}\n</style>\n","// canvas-confetti v1.9.3 built on 2024-04-30T22:19:17.794Z\nvar module = {};\n\n// source content\n/* globals Map */\n\n(function main(global, module, isWorker, workerSize) {\n var canUseWorker = !!(\n global.Worker &&\n global.Blob &&\n global.Promise &&\n global.OffscreenCanvas &&\n global.OffscreenCanvasRenderingContext2D &&\n global.HTMLCanvasElement &&\n global.HTMLCanvasElement.prototype.transferControlToOffscreen &&\n global.URL &&\n global.URL.createObjectURL);\n\n var canUsePaths = typeof Path2D === 'function' && typeof DOMMatrix === 'function';\n var canDrawBitmap = (function () {\n // this mostly supports ssr\n if (!global.OffscreenCanvas) {\n return false;\n }\n\n var canvas = new OffscreenCanvas(1, 1);\n var ctx = canvas.getContext('2d');\n ctx.fillRect(0, 0, 1, 1);\n var bitmap = canvas.transferToImageBitmap();\n\n try {\n ctx.createPattern(bitmap, 'no-repeat');\n } catch (e) {\n return false;\n }\n\n return true;\n })();\n\n function noop() {}\n\n // create a promise if it exists, otherwise, just\n // call the function directly\n function promise(func) {\n var ModulePromise = module.exports.Promise;\n var Prom = ModulePromise !== void 0 ? ModulePromise : global.Promise;\n\n if (typeof Prom === 'function') {\n return new Prom(func);\n }\n\n func(noop, noop);\n\n return null;\n }\n\n var bitmapMapper = (function (skipTransform, map) {\n // see https://github.com/catdad/canvas-confetti/issues/209\n // creating canvases is actually pretty expensive, so we should create a\n // 1:1 map for bitmap:canvas, so that we can animate the confetti in\n // a performant manner, but also not store them forever so that we don't\n // have a memory leak\n return {\n transform: function(bitmap) {\n if (skipTransform) {\n return bitmap;\n }\n\n if (map.has(bitmap)) {\n return map.get(bitmap);\n }\n\n var canvas = new OffscreenCanvas(bitmap.width, bitmap.height);\n var ctx = canvas.getContext('2d');\n ctx.drawImage(bitmap, 0, 0);\n\n map.set(bitmap, canvas);\n\n return canvas;\n },\n clear: function () {\n map.clear();\n }\n };\n })(canDrawBitmap, new Map());\n\n var raf = (function () {\n var TIME = Math.floor(1000 / 60);\n var frame, cancel;\n var frames = {};\n var lastFrameTime = 0;\n\n if (typeof requestAnimationFrame === 'function' && typeof cancelAnimationFrame === 'function') {\n frame = function (cb) {\n var id = Math.random();\n\n frames[id] = requestAnimationFrame(function onFrame(time) {\n if (lastFrameTime === time || lastFrameTime + TIME - 1 < time) {\n lastFrameTime = time;\n delete frames[id];\n\n cb();\n } else {\n frames[id] = requestAnimationFrame(onFrame);\n }\n });\n\n return id;\n };\n cancel = function (id) {\n if (frames[id]) {\n cancelAnimationFrame(frames[id]);\n }\n };\n } else {\n frame = function (cb) {\n return setTimeout(cb, TIME);\n };\n cancel = function (timer) {\n return clearTimeout(timer);\n };\n }\n\n return { frame: frame, cancel: cancel };\n }());\n\n var getWorker = (function () {\n var worker;\n var prom;\n var resolves = {};\n\n function decorate(worker) {\n function execute(options, callback) {\n worker.postMessage({ options: options || {}, callback: callback });\n }\n worker.init = function initWorker(canvas) {\n var offscreen = canvas.transferControlToOffscreen();\n worker.postMessage({ canvas: offscreen }, [offscreen]);\n };\n\n worker.fire = function fireWorker(options, size, done) {\n if (prom) {\n execute(options, null);\n return prom;\n }\n\n var id = Math.random().toString(36).slice(2);\n\n prom = promise(function (resolve) {\n function workerDone(msg) {\n if (msg.data.callback !== id) {\n return;\n }\n\n delete resolves[id];\n worker.removeEventListener('message', workerDone);\n\n prom = null;\n\n bitmapMapper.clear();\n\n done();\n resolve();\n }\n\n worker.addEventListener('message', workerDone);\n execute(options, id);\n\n resolves[id] = workerDone.bind(null, { data: { callback: id }});\n });\n\n return prom;\n };\n\n worker.reset = function resetWorker() {\n worker.postMessage({ reset: true });\n\n for (var id in resolves) {\n resolves[id]();\n delete resolves[id];\n }\n };\n }\n\n return function () {\n if (worker) {\n return worker;\n }\n\n if (!isWorker && canUseWorker) {\n var code = [\n 'var CONFETTI, SIZE = {}, module = {};',\n '(' + main.toString() + ')(this, module, true, SIZE);',\n 'onmessage = function(msg) {',\n ' if (msg.data.options) {',\n ' CONFETTI(msg.data.options).then(function () {',\n ' if (msg.data.callback) {',\n ' postMessage({ callback: msg.data.callback });',\n ' }',\n ' });',\n ' } else if (msg.data.reset) {',\n ' CONFETTI && CONFETTI.reset();',\n ' } else if (msg.data.resize) {',\n ' SIZE.width = msg.data.resize.width;',\n ' SIZE.height = msg.data.resize.height;',\n ' } else if (msg.data.canvas) {',\n ' SIZE.width = msg.data.canvas.width;',\n ' SIZE.height = msg.data.canvas.height;',\n ' CONFETTI = module.exports.create(msg.data.canvas);',\n ' }',\n '}',\n ].join('\\n');\n try {\n worker = new Worker(URL.createObjectURL(new Blob([code])));\n } catch (e) {\n // eslint-disable-next-line no-console\n typeof console !== undefined && typeof console.warn === 'function' ? console.warn('🎊 Could not load worker', e) : null;\n\n return null;\n }\n\n decorate(worker);\n }\n\n return worker;\n };\n })();\n\n var defaults = {\n particleCount: 50,\n angle: 90,\n spread: 45,\n startVelocity: 45,\n decay: 0.9,\n gravity: 1,\n drift: 0,\n ticks: 200,\n x: 0.5,\n y: 0.5,\n shapes: ['square', 'circle'],\n zIndex: 100,\n colors: [\n '#26ccff',\n '#a25afd',\n '#ff5e7e',\n '#88ff5a',\n '#fcff42',\n '#ffa62d',\n '#ff36ff'\n ],\n // probably should be true, but back-compat\n disableForReducedMotion: false,\n scalar: 1\n };\n\n function convert(val, transform) {\n return transform ? transform(val) : val;\n }\n\n function isOk(val) {\n return !(val === null || val === undefined);\n }\n\n function prop(options, name, transform) {\n return convert(\n options && isOk(options[name]) ? options[name] : defaults[name],\n transform\n );\n }\n\n function onlyPositiveInt(number){\n return number < 0 ? 0 : Math.floor(number);\n }\n\n function randomInt(min, max) {\n // [min, max)\n return Math.floor(Math.random() * (max - min)) + min;\n }\n\n function toDecimal(str) {\n return parseInt(str, 16);\n }\n\n function colorsToRgb(colors) {\n return colors.map(hexToRgb);\n }\n\n function hexToRgb(str) {\n var val = String(str).replace(/[^0-9a-f]/gi, '');\n\n if (val.length < 6) {\n val = val[0]+val[0]+val[1]+val[1]+val[2]+val[2];\n }\n\n return {\n r: toDecimal(val.substring(0,2)),\n g: toDecimal(val.substring(2,4)),\n b: toDecimal(val.substring(4,6))\n };\n }\n\n function getOrigin(options) {\n var origin = prop(options, 'origin', Object);\n origin.x = prop(origin, 'x', Number);\n origin.y = prop(origin, 'y', Number);\n\n return origin;\n }\n\n function setCanvasWindowSize(canvas) {\n canvas.width = document.documentElement.clientWidth;\n canvas.height = document.documentElement.clientHeight;\n }\n\n function setCanvasRectSize(canvas) {\n var rect = canvas.getBoundingClientRect();\n canvas.width = rect.width;\n canvas.height = rect.height;\n }\n\n function getCanvas(zIndex) {\n var canvas = document.createElement('canvas');\n\n canvas.style.position = 'fixed';\n canvas.style.top = '0px';\n canvas.style.left = '0px';\n canvas.style.pointerEvents = 'none';\n canvas.style.zIndex = zIndex;\n\n return canvas;\n }\n\n function ellipse(context, x, y, radiusX, radiusY, rotation, startAngle, endAngle, antiClockwise) {\n context.save();\n context.translate(x, y);\n context.rotate(rotation);\n context.scale(radiusX, radiusY);\n context.arc(0, 0, 1, startAngle, endAngle, antiClockwise);\n context.restore();\n }\n\n function randomPhysics(opts) {\n var radAngle = opts.angle * (Math.PI / 180);\n var radSpread = opts.spread * (Math.PI / 180);\n\n return {\n x: opts.x,\n y: opts.y,\n wobble: Math.random() * 10,\n wobbleSpeed: Math.min(0.11, Math.random() * 0.1 + 0.05),\n velocity: (opts.startVelocity * 0.5) + (Math.random() * opts.startVelocity),\n angle2D: -radAngle + ((0.5 * radSpread) - (Math.random() * radSpread)),\n tiltAngle: (Math.random() * (0.75 - 0.25) + 0.25) * Math.PI,\n color: opts.color,\n shape: opts.shape,\n tick: 0,\n totalTicks: opts.ticks,\n decay: opts.decay,\n drift: opts.drift,\n random: Math.random() + 2,\n tiltSin: 0,\n tiltCos: 0,\n wobbleX: 0,\n wobbleY: 0,\n gravity: opts.gravity * 3,\n ovalScalar: 0.6,\n scalar: opts.scalar,\n flat: opts.flat\n };\n }\n\n function updateFetti(context, fetti) {\n fetti.x += Math.cos(fetti.angle2D) * fetti.velocity + fetti.drift;\n fetti.y += Math.sin(fetti.angle2D) * fetti.velocity + fetti.gravity;\n fetti.velocity *= fetti.decay;\n\n if (fetti.flat) {\n fetti.wobble = 0;\n fetti.wobbleX = fetti.x + (10 * fetti.scalar);\n fetti.wobbleY = fetti.y + (10 * fetti.scalar);\n\n fetti.tiltSin = 0;\n fetti.tiltCos = 0;\n fetti.random = 1;\n } else {\n fetti.wobble += fetti.wobbleSpeed;\n fetti.wobbleX = fetti.x + ((10 * fetti.scalar) * Math.cos(fetti.wobble));\n fetti.wobbleY = fetti.y + ((10 * fetti.scalar) * Math.sin(fetti.wobble));\n\n fetti.tiltAngle += 0.1;\n fetti.tiltSin = Math.sin(fetti.tiltAngle);\n fetti.tiltCos = Math.cos(fetti.tiltAngle);\n fetti.random = Math.random() + 2;\n }\n\n var progress = (fetti.tick++) / fetti.totalTicks;\n\n var x1 = fetti.x + (fetti.random * fetti.tiltCos);\n var y1 = fetti.y + (fetti.random * fetti.tiltSin);\n var x2 = fetti.wobbleX + (fetti.random * fetti.tiltCos);\n var y2 = fetti.wobbleY + (fetti.random * fetti.tiltSin);\n\n context.fillStyle = 'rgba(' + fetti.color.r + ', ' + fetti.color.g + ', ' + fetti.color.b + ', ' + (1 - progress) + ')';\n\n context.beginPath();\n\n if (canUsePaths && fetti.shape.type === 'path' && typeof fetti.shape.path === 'string' && Array.isArray(fetti.shape.matrix)) {\n context.fill(transformPath2D(\n fetti.shape.path,\n fetti.shape.matrix,\n fetti.x,\n fetti.y,\n Math.abs(x2 - x1) * 0.1,\n Math.abs(y2 - y1) * 0.1,\n Math.PI / 10 * fetti.wobble\n ));\n } else if (fetti.shape.type === 'bitmap') {\n var rotation = Math.PI / 10 * fetti.wobble;\n var scaleX = Math.abs(x2 - x1) * 0.1;\n var scaleY = Math.abs(y2 - y1) * 0.1;\n var width = fetti.shape.bitmap.width * fetti.scalar;\n var height = fetti.shape.bitmap.height * fetti.scalar;\n\n var matrix = new DOMMatrix([\n Math.cos(rotation) * scaleX,\n Math.sin(rotation) * scaleX,\n -Math.sin(rotation) * scaleY,\n Math.cos(rotation) * scaleY,\n fetti.x,\n fetti.y\n ]);\n\n // apply the transform matrix from the confetti shape\n matrix.multiplySelf(new DOMMatrix(fetti.shape.matrix));\n\n var pattern = context.createPattern(bitmapMapper.transform(fetti.shape.bitmap), 'no-repeat');\n pattern.setTransform(matrix);\n\n context.globalAlpha = (1 - progress);\n context.fillStyle = pattern;\n context.fillRect(\n fetti.x - (width / 2),\n fetti.y - (height / 2),\n width,\n height\n );\n context.globalAlpha = 1;\n } else if (fetti.shape === 'circle') {\n context.ellipse ?\n context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) :\n ellipse(context, fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI);\n } else if (fetti.shape === 'star') {\n var rot = Math.PI / 2 * 3;\n var innerRadius = 4 * fetti.scalar;\n var outerRadius = 8 * fetti.scalar;\n var x = fetti.x;\n var y = fetti.y;\n var spikes = 5;\n var step = Math.PI / spikes;\n\n while (spikes--) {\n x = fetti.x + Math.cos(rot) * outerRadius;\n y = fetti.y + Math.sin(rot) * outerRadius;\n context.lineTo(x, y);\n rot += step;\n\n x = fetti.x + Math.cos(rot) * innerRadius;\n y = fetti.y + Math.sin(rot) * innerRadius;\n context.lineTo(x, y);\n rot += step;\n }\n } else {\n context.moveTo(Math.floor(fetti.x), Math.floor(fetti.y));\n context.lineTo(Math.floor(fetti.wobbleX), Math.floor(y1));\n context.lineTo(Math.floor(x2), Math.floor(y2));\n context.lineTo(Math.floor(x1), Math.floor(fetti.wobbleY));\n }\n\n context.closePath();\n context.fill();\n\n return fetti.tick < fetti.totalTicks;\n }\n\n function animate(canvas, fettis, resizer, size, done) {\n var animatingFettis = fettis.slice();\n var context = canvas.getContext('2d');\n var animationFrame;\n var destroy;\n\n var prom = promise(function (resolve) {\n function onDone() {\n animationFrame = destroy = null;\n\n context.clearRect(0, 0, size.width, size.height);\n bitmapMapper.clear();\n\n done();\n resolve();\n }\n\n function update() {\n if (isWorker && !(size.width === workerSize.width && size.height === workerSize.height)) {\n size.width = canvas.width = workerSize.width;\n size.height = canvas.height = workerSize.height;\n }\n\n if (!size.width && !size.height) {\n resizer(canvas);\n size.width = canvas.width;\n size.height = canvas.height;\n }\n\n context.clearRect(0, 0, size.width, size.height);\n\n animatingFettis = animatingFettis.filter(function (fetti) {\n return updateFetti(context, fetti);\n });\n\n if (animatingFettis.length) {\n animationFrame = raf.frame(update);\n } else {\n onDone();\n }\n }\n\n animationFrame = raf.frame(update);\n destroy = onDone;\n });\n\n return {\n addFettis: function (fettis) {\n animatingFettis = animatingFettis.concat(fettis);\n\n return prom;\n },\n canvas: canvas,\n promise: prom,\n reset: function () {\n if (animationFrame) {\n raf.cancel(animationFrame);\n }\n\n if (destroy) {\n destroy();\n }\n }\n };\n }\n\n function confettiCannon(canvas, globalOpts) {\n var isLibCanvas = !canvas;\n var allowResize = !!prop(globalOpts || {}, 'resize');\n var hasResizeEventRegistered = false;\n var globalDisableForReducedMotion = prop(globalOpts, 'disableForReducedMotion', Boolean);\n var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, 'useWorker');\n var worker = shouldUseWorker ? getWorker() : null;\n var resizer = isLibCanvas ? setCanvasWindowSize : setCanvasRectSize;\n var initialized = (canvas && worker) ? !!canvas.__confetti_initialized : false;\n var preferLessMotion = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion)').matches;\n var animationObj;\n\n function fireLocal(options, size, done) {\n var particleCount = prop(options, 'particleCount', onlyPositiveInt);\n var angle = prop(options, 'angle', Number);\n var spread = prop(options, 'spread', Number);\n var startVelocity = prop(options, 'startVelocity', Number);\n var decay = prop(options, 'decay', Number);\n var gravity = prop(options, 'gravity', Number);\n var drift = prop(options, 'drift', Number);\n var colors = prop(options, 'colors', colorsToRgb);\n var ticks = prop(options, 'ticks', Number);\n var shapes = prop(options, 'shapes');\n var scalar = prop(options, 'scalar');\n var flat = !!prop(options, 'flat');\n var origin = getOrigin(options);\n\n var temp = particleCount;\n var fettis = [];\n\n var startX = canvas.width * origin.x;\n var startY = canvas.height * origin.y;\n\n while (temp--) {\n fettis.push(\n randomPhysics({\n x: startX,\n y: startY,\n angle: angle,\n spread: spread,\n startVelocity: startVelocity,\n color: colors[temp % colors.length],\n shape: shapes[randomInt(0, shapes.length)],\n ticks: ticks,\n decay: decay,\n gravity: gravity,\n drift: drift,\n scalar: scalar,\n flat: flat\n })\n );\n }\n\n // if we have a previous canvas already animating,\n // add to it\n if (animationObj) {\n return animationObj.addFettis(fettis);\n }\n\n animationObj = animate(canvas, fettis, resizer, size , done);\n\n return animationObj.promise;\n }\n\n function fire(options) {\n var disableForReducedMotion = globalDisableForReducedMotion || prop(options, 'disableForReducedMotion', Boolean);\n var zIndex = prop(options, 'zIndex', Number);\n\n if (disableForReducedMotion && preferLessMotion) {\n return promise(function (resolve) {\n resolve();\n });\n }\n\n if (isLibCanvas && animationObj) {\n // use existing canvas from in-progress animation\n canvas = animationObj.canvas;\n } else if (isLibCanvas && !canvas) {\n // create and initialize a new canvas\n canvas = getCanvas(zIndex);\n document.body.appendChild(canvas);\n }\n\n if (allowResize && !initialized) {\n // initialize the size of a user-supplied canvas\n resizer(canvas);\n }\n\n var size = {\n width: canvas.width,\n height: canvas.height\n };\n\n if (worker && !initialized) {\n worker.init(canvas);\n }\n\n initialized = true;\n\n if (worker) {\n canvas.__confetti_initialized = true;\n }\n\n function onResize() {\n if (worker) {\n // TODO this really shouldn't be immediate, because it is expensive\n var obj = {\n getBoundingClientRect: function () {\n if (!isLibCanvas) {\n return canvas.getBoundingClientRect();\n }\n }\n };\n\n resizer(obj);\n\n worker.postMessage({\n resize: {\n width: obj.width,\n height: obj.height\n }\n });\n return;\n }\n\n // don't actually query the size here, since this\n // can execute frequently and rapidly\n size.width = size.height = null;\n }\n\n function done() {\n animationObj = null;\n\n if (allowResize) {\n hasResizeEventRegistered = false;\n global.removeEventListener('resize', onResize);\n }\n\n if (isLibCanvas && canvas) {\n if (document.body.contains(canvas)) {\n document.body.removeChild(canvas); \n }\n canvas = null;\n initialized = false;\n }\n }\n\n if (allowResize && !hasResizeEventRegistered) {\n hasResizeEventRegistered = true;\n global.addEventListener('resize', onResize, false);\n }\n\n if (worker) {\n return worker.fire(options, size, done);\n }\n\n return fireLocal(options, size, done);\n }\n\n fire.reset = function () {\n if (worker) {\n worker.reset();\n }\n\n if (animationObj) {\n animationObj.reset();\n }\n };\n\n return fire;\n }\n\n // Make default export lazy to defer worker creation until called.\n var defaultFire;\n function getDefaultFire() {\n if (!defaultFire) {\n defaultFire = confettiCannon(null, { useWorker: true, resize: true });\n }\n return defaultFire;\n }\n\n function transformPath2D(pathString, pathMatrix, x, y, scaleX, scaleY, rotation) {\n var path2d = new Path2D(pathString);\n\n var t1 = new Path2D();\n t1.addPath(path2d, new DOMMatrix(pathMatrix));\n\n var t2 = new Path2D();\n // see https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix/DOMMatrix\n t2.addPath(t1, new DOMMatrix([\n Math.cos(rotation) * scaleX,\n Math.sin(rotation) * scaleX,\n -Math.sin(rotation) * scaleY,\n Math.cos(rotation) * scaleY,\n x,\n y\n ]));\n\n return t2;\n }\n\n function shapeFromPath(pathData) {\n if (!canUsePaths) {\n throw new Error('path confetti are not supported in this browser');\n }\n\n var path, matrix;\n\n if (typeof pathData === 'string') {\n path = pathData;\n } else {\n path = pathData.path;\n matrix = pathData.matrix;\n }\n\n var path2d = new Path2D(path);\n var tempCanvas = document.createElement('canvas');\n var tempCtx = tempCanvas.getContext('2d');\n\n if (!matrix) {\n // attempt to figure out the width of the path, up to 1000x1000\n var maxSize = 1000;\n var minX = maxSize;\n var minY = maxSize;\n var maxX = 0;\n var maxY = 0;\n var width, height;\n\n // do some line skipping... this is faster than checking\n // every pixel and will be mostly still correct\n for (var x = 0; x < maxSize; x += 2) {\n for (var y = 0; y < maxSize; y += 2) {\n if (tempCtx.isPointInPath(path2d, x, y, 'nonzero')) {\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n }\n }\n\n width = maxX - minX;\n height = maxY - minY;\n\n var maxDesiredSize = 10;\n var scale = Math.min(maxDesiredSize/width, maxDesiredSize/height);\n\n matrix = [\n scale, 0, 0, scale,\n -Math.round((width/2) + minX) * scale,\n -Math.round((height/2) + minY) * scale\n ];\n }\n\n return {\n type: 'path',\n path: path,\n matrix: matrix\n };\n }\n\n function shapeFromText(textData) {\n var text,\n scalar = 1,\n color = '#000000',\n // see https://nolanlawson.com/2022/04/08/the-struggle-of-using-native-emoji-on-the-web/\n fontFamily = '\"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\", \"EmojiOne Color\", \"Android Emoji\", \"Twemoji Mozilla\", \"system emoji\", sans-serif';\n\n if (typeof textData === 'string') {\n text = textData;\n } else {\n text = textData.text;\n scalar = 'scalar' in textData ? textData.scalar : scalar;\n fontFamily = 'fontFamily' in textData ? textData.fontFamily : fontFamily;\n color = 'color' in textData ? textData.color : color;\n }\n\n // all other confetti are 10 pixels,\n // so this pixel size is the de-facto 100% scale confetti\n var fontSize = 10 * scalar;\n var font = '' + fontSize + 'px ' + fontFamily;\n\n var canvas = new OffscreenCanvas(fontSize, fontSize);\n var ctx = canvas.getContext('2d');\n\n ctx.font = font;\n var size = ctx.measureText(text);\n var width = Math.ceil(size.actualBoundingBoxRight + size.actualBoundingBoxLeft);\n var height = Math.ceil(size.actualBoundingBoxAscent + size.actualBoundingBoxDescent);\n\n var padding = 2;\n var x = size.actualBoundingBoxLeft + padding;\n var y = size.actualBoundingBoxAscent + padding;\n width += padding + padding;\n height += padding + padding;\n\n canvas = new OffscreenCanvas(width, height);\n ctx = canvas.getContext('2d');\n ctx.font = font;\n ctx.fillStyle = color;\n\n ctx.fillText(text, x, y);\n\n var scale = 1 / scalar;\n\n return {\n type: 'bitmap',\n // TODO these probably need to be transfered for workers\n bitmap: canvas.transferToImageBitmap(),\n matrix: [scale, 0, 0, scale, -width * scale / 2, -height * scale / 2]\n };\n }\n\n module.exports = function() {\n return getDefaultFire().apply(this, arguments);\n };\n module.exports.reset = function() {\n getDefaultFire().reset();\n };\n module.exports.create = confettiCannon;\n module.exports.shapeFromPath = shapeFromPath;\n module.exports.shapeFromText = shapeFromText;\n}((function () {\n if (typeof window !== 'undefined') {\n return window;\n }\n\n if (typeof self !== 'undefined') {\n return self;\n }\n\n return this || {};\n})(), module, false));\n\n// end source content\n\nexport default module.exports;\nexport var create = module.exports.create;\n","<template>\n <div v-if=\"sessionPrepared\" class=\"StudySession\">\n <v-row v-if=\"!frameless\" align=\"center\">\n <!-- <h1 class=\"text-h3\" v-if=\"courseNames[courseID]\">{{ courseNames[courseID] }}:</h1> -->\n <v-spacer></v-spacer>\n <v-progress-circular v-if=\"loading\" color=\"primary\" indeterminate size=\"32\" width=\"4\" />\n </v-row>\n\n <!-- Debug Panel (only visible if window.debugMode is true) -->\n <session-controller-debug v-if=\"debugMode\" :session-controller=\"sessionController\" />\n\n <br v-if=\"!frameless\" />\n\n <div v-if=\"sessionFinished\">\n <slot name=\"session-finished\" :session-record=\"sessionRecord\" :report=\"sessionController?.report\">\n <div class=\"text-h4\">\n <p>Study session finished! Great job!</p>\n <p v-if=\"sessionController\">{{ sessionController.report }}</p>\n <!-- <p>\n Start <a @click=\"$emit('session-finished')\">another study session</a>, or try\n <router-link :to=\"`/edit/${courseID}`\">adding some new content</router-link> to challenge yourself and others!\n </p> -->\n <heat-map :activity-records-getter=\"() => user.getActivityRecords()\" />\n </div>\n </slot>\n </div>\n\n <div v-else ref=\"shadowWrapper\" class=\"card-transition-container\">\n <transition :name=\"transitionName\">\n <card-viewer\n v-if=\"view\"\n ref=\"cardViewer\"\n :key=\"cardID\"\n :class=\"loading ? 'muted' : ''\"\n :view=\"view\"\n :data=\"data\"\n :card_id=\"cardID\"\n :course_id=\"courseID\"\n :session-order=\"cardCount\"\n :user_elo=\"user_elo(courseID)\"\n :card_elo=\"card_elo\"\n :frameless=\"frameless\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"handleReplanRequest\"\n @ready-to-advance=\"handleReadyToAdvance\"\n />\n </transition>\n </div>\n\n <template v-if=\"!frameless\">\n <br />\n <div v-if=\"sessionController\">\n <span v-for=\"i in sessionController.failedCount\" :key=\"i\" class=\"text-h5\">•</span>\n </div>\n </template>\n\n <!--\n todo: reinstate tag editing at session-time ?\n\n <div v-if=\"!sessionFinished && editTags\">\n <p>Add tags to this card:</p>\n <sk-tags-input :course-i-d=\"courseID\" :card-i-d=\"cardID\" />\n </div> -->\n\n <v-row v-if=\"!hideFooter\" align=\"center\" class=\"footer-controls pa-5\">\n <v-col cols=\"auto\" class=\"d-flex flex-grow-0 mr-auto\">\n <StudySessionTimer\n :time-remaining=\"timeRemaining\"\n :session-time-limit=\"sessionTimeLimit\"\n @add-time=\"incrementSessionClock\"\n />\n </v-col>\n\n <v-spacer></v-spacer>\n\n <v-col cols=\"auto\" class=\"footer-right\">\n <SkMouseTrap />\n </v-col>\n </v-row>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, markRaw, PropType } from 'vue';\nimport { ViewComponent } from '../composables';\nimport { isQuestionView } from '../composables/CompositionViewable';\nimport HeatMap from './HeatMap.vue';\nimport SkMouseTrap from './SkMouseTrap.vue';\nimport { alertUser } from './SnackbarService';\nimport StudySessionTimer from './StudySessionTimer.vue';\nimport CardViewer from './cardRendering/CardViewer.vue';\nimport SessionControllerDebug from './SessionControllerDebug.vue';\n\nimport { CourseElo, Status, toCourseElo, ViewData } from '@vue-skuilder/common';\nimport {\n CardHistory,\n CardRecord,\n ClassroomDBInterface,\n ContentSourceID,\n CourseRegistrationDoc,\n DataLayerProvider,\n getStudySource,\n HydratedCard,\n isQuestionRecord,\n ReplanOptions,\n ResponseResult,\n SessionController,\n StudyContentSource,\n StudySessionRecord,\n UserDBInterface,\n} from '@vue-skuilder/db';\nimport confetti from 'canvas-confetti';\n\nimport { StudySessionConfig, CardTransitionPreset, CardTransitionMode } from './StudySession.types';\n\ninterface StudyRefs {\n shadowWrapper: HTMLDivElement;\n cardViewer: InstanceType<typeof CardViewer>;\n}\n\ntype StudyInstance = ReturnType<typeof defineComponent> & {\n $refs: StudyRefs;\n};\n\nexport default defineComponent({\n name: 'StudySession',\n\n ref: {} as StudyRefs,\n\n components: {\n CardViewer,\n StudySessionTimer,\n SkMouseTrap,\n HeatMap,\n SessionControllerDebug,\n },\n\n props: {\n sessionTimeLimit: {\n type: Number,\n required: true,\n },\n contentSources: {\n type: Array as PropType<ContentSourceID[]>,\n required: true,\n },\n user: {\n type: Object as PropType<UserDBInterface>,\n required: true,\n },\n dataLayer: {\n type: Object as PropType<DataLayerProvider>,\n required: true,\n },\n sessionConfig: {\n type: Object as PropType<StudySessionConfig>,\n default: () => ({ likesConfetti: false }),\n },\n getViewComponent: {\n type: Function as PropType<(viewId: string) => ViewComponent>,\n required: true,\n },\n frameless: {\n type: Boolean,\n default: false,\n },\n hideFooter: {\n type: Boolean,\n default: false,\n },\n transitionName: {\n type: String as PropType<CardTransitionPreset | string>,\n default: 'component-fade',\n },\n transitionMode: {\n type: String as PropType<CardTransitionMode>,\n default: 'out-in',\n },\n },\n\n emits: [\n 'session-finished',\n 'session-started',\n 'card-loaded',\n 'card-response',\n 'time-changed',\n 'session-prepared',\n 'session-error',\n 'replan-requested',\n ],\n\n data() {\n return {\n // editTags: false,\n cardID: '',\n view: null as ViewComponent | null,\n data: [] as ViewData[],\n courseID: '',\n card_elo: 1000,\n courseNames: {} as { [courseID: string]: string },\n cardCount: 1,\n sessionController: null as SessionController<ViewComponent> | null,\n sessionPrepared: false,\n sessionFinished: false,\n sessionRecord: [] as StudySessionRecord[],\n percentageRemaining: 100,\n timerIsActive: true,\n loading: false,\n userCourseRegDoc: null as CourseRegistrationDoc | null,\n sessionContentSources: [] as StudyContentSource[],\n timeRemaining: 300, // 5 minutes * 60 seconds\n replanPending: false,\n replanOptions: null as ReplanOptions | null,\n deferredNextCardAction: null as string | null,\n intervalHandler: null as NodeJS.Timeout | null,\n cardType: '',\n debugMode: (window as any).debugMode === true,\n };\n },\n\n computed: {\n currentCard(): StudySessionRecord {\n return this.sessionRecord[this.sessionRecord.length - 1];\n },\n },\n\n // watch: {\n // editCard: {\n // async handler(value: boolean) {\n // if (value) {\n // this.dataInputFormStore.dataInputForm.dataShape = await getCardDataShape(this.courseID, this.cardID);\n\n // const cfg = await getCredentialledCourseConfig(this.courseID);\n // this.dataInputFormStore.dataInputForm.course = cfg!;\n\n // this.editCardReady = true;\n\n // for (const oldField in this.dataInputFormStore.dataInputForm.localStore) {\n // if (oldField) {\n // console.log(`[Study] Removing old data: ${oldField}`);\n // delete this.dataInputFormStore.dataInputForm.localStore[oldField];\n // }\n // }\n\n // for (const field in this.data[0]) {\n // if (field) {\n // console.log(`[Study] Writing ${field}: ${this.data[0][field]} to the dataInputForm state...`);\n // this.dataInputFormStore.dataInputForm.localStore[field] = this.data[0][field];\n // }\n // }\n // } else {\n // this.editCardReady = false;\n // }\n // },\n // },\n // },\n\n async created() {\n this.userCourseRegDoc = await this.user.getCourseRegistrationsDoc();\n console.log('[StudySession] Created lifecycle hook - starting initSession');\n await this.initSession();\n console.log('[StudySession] InitSession completed in created hook');\n },\n\n methods: {\n /**\n * Handle a `ready-to-advance` event from a card view that previously\n * submitted with `deferAdvance: true`. Triggers the stashed navigation.\n */\n async handleReadyToAdvance() {\n const action = this.deferredNextCardAction;\n if (!action) {\n console.warn('[StudySession] ready-to-advance received but no deferred action stashed — ignoring');\n return;\n }\n console.log(`[StudySession] ready-to-advance received — advancing with stashed action: ${action}`);\n this.deferredNextCardAction = null;\n this.loadCard(await this.sessionController!.nextCard(action));\n },\n\n /**\n * Handle a replan request from a card view component.\n *\n * Card views emit 'request-replan' when they've made state changes that\n * should be reflected in the session's content queue (e.g. a GPC intro\n * unlocking new exercise cards).\n *\n * The replan is deferred — not fired immediately. processResponse()\n * checks this flag after submitResponse() has recorded ELO/tag\n * interactions, ensuring the pipeline sees fully up-to-date state.\n *\n * Accepts either a ReplanOptions object (new API) or a bare hints\n * Record (legacy). SessionController.requestReplan() normalises both.\n */\n handleReplanRequest(options?: ReplanOptions) {\n if (this.sessionController) {\n const label = (options as ReplanOptions)?.label;\n const tag = label ? ` [${label}]` : '';\n console.log(`[StudySession] Replan requested by card view${tag}, deferring until after response processing`);\n this.replanPending = true;\n this.replanOptions = (options as ReplanOptions) ?? null;\n }\n },\n\n user_elo(courseID: string): CourseElo {\n const courseDoc = this.userCourseRegDoc!.courses.find((c) => c.courseID === courseID);\n if (courseDoc) {\n return toCourseElo(courseDoc.elo);\n }\n return toCourseElo(undefined);\n },\n\n handleClassroomMessage() {\n return (v: unknown) => {\n alertUser({\n text: this.user?.getUsername() || '[Unknown user]',\n status: Status.ok,\n });\n console.log(`[StudySession] There was a change in the classroom DB:`);\n console.log(`[StudySession] change: ${v}`);\n console.log(`[StudySession] Stringified change: ${JSON.stringify(v)}`);\n return {};\n };\n },\n\n incrementSessionClock() {\n const max = 60 * this.sessionTimeLimit - this.timeRemaining;\n this.sessionController!.addTime(Math.min(max, 60));\n this.tick();\n },\n\n tick() {\n this.timeRemaining = this.sessionController!.secondsRemaining;\n\n this.percentageRemaining =\n this.timeRemaining > 60\n ? 100 * (this.timeRemaining / (60 * this.sessionTimeLimit))\n : 100 * (this.timeRemaining / 60);\n\n this.$emit('time-changed', this.timeRemaining);\n\n if (this.timeRemaining === 0) {\n clearInterval(this.intervalHandler!);\n }\n },\n\n async initSession() {\n let sessionClassroomDBs: ClassroomDBInterface[] = [];\n try {\n console.log(`[StudySession] starting study session w/ sources: ${JSON.stringify(this.contentSources)}`);\n console.log('[StudySession] Beginning preparation process');\n\n this.sessionContentSources = markRaw(\n (\n await Promise.all(\n this.contentSources.map(async (s) => {\n try {\n return await getStudySource(s, this.user);\n } catch (e) {\n console.error(`Failed to load study source: ${s.type}/${s.id}`, e);\n return null;\n }\n })\n )\n ).filter((s: unknown) => s !== null)\n );\n\n this.timeRemaining = this.sessionTimeLimit * 60;\n\n sessionClassroomDBs = await Promise.all(\n this.contentSources\n .filter((s) => s.type === 'classroom')\n .map(async (c) => await this.dataLayer.getClassroomDB(c.id, 'student'))\n );\n\n sessionClassroomDBs.forEach((_db) => {\n // db.setChangeFcn(this.handleClassroomMessage());\n });\n\n const scOptions = this.sessionConfig?.defaultBatchLimit !== undefined\n ? { defaultBatchLimit: this.sessionConfig.defaultBatchLimit }\n : undefined;\n\n this.sessionController = markRaw(\n new SessionController<ViewComponent>(\n this.sessionContentSources,\n 60 * this.sessionTimeLimit,\n this.dataLayer,\n this.getViewComponent,\n undefined, // mixer — use default\n scOptions\n )\n );\n this.sessionController.sessionRecord = this.sessionRecord;\n\n // Apply init hints if provided (e.g. post-lesson boost tags)\n if (this.sessionConfig?.initHints) {\n for (const source of this.sessionContentSources) {\n source.setEphemeralHints?.(this.sessionConfig.initHints as any);\n }\n console.log('[StudySession] Applied init hints to content sources');\n }\n\n await this.sessionController.prepareSession();\n this.intervalHandler = setInterval(this.tick, 1000);\n\n this.sessionPrepared = true;\n\n console.log('[StudySession] Session preparation complete, emitting session-prepared event');\n this.$emit('session-prepared');\n console.log('[StudySession] Event emission completed');\n } catch (error) {\n console.error('[StudySession] Error during session preparation:', error);\n // Notify parent component about the error\n this.$emit('session-error', { message: 'Failed to prepare study session', error });\n }\n\n try {\n this.contentSources\n .filter((s) => s.type === 'course')\n .forEach(\n async (c) => (this.courseNames[c.id] = (await this.dataLayer.getCoursesDB().getCourseConfig(c.id)).name)\n );\n\n console.log(`[StudySession] Session created:\n ${this.sessionController?.toString() || 'Session controller not initialized'}\n User courses: ${this.contentSources\n .filter((s) => s.type === 'course')\n .map((c) => c.id)\n .toString()}\n User classrooms: ${sessionClassroomDBs.map((db: any) => db._id).toString() || 'No classrooms'}\n `);\n } catch (error) {\n console.error('[StudySession] Error during final session setup:', error);\n }\n\n if (this.sessionController) {\n try {\n this.$emit('session-started');\n this.loadCard(await this.sessionController.nextCard());\n } catch (error) {\n console.error('[StudySession] Error loading next card:', error);\n this.$emit('session-error', { message: 'Failed to load study card', error });\n }\n } else {\n console.error('[StudySession] Cannot load card: session controller not initialized');\n this.$emit('session-error', { message: 'Study session initialization failed' });\n }\n },\n\n countCardViews(course_id: string, card_id: string): number {\n return this.sessionRecord.filter((r) => r.card.course_id === course_id && r.card.card_id === card_id).length;\n },\n\n async processResponse(this: StudyInstance, r: CardRecord) {\n this.$emit('card-response', r);\n\n this.timerIsActive = true;\n\n r.cardID = this.cardID;\n r.courseID = this.courseID;\n this.currentCard.records.push(r);\n\n console.log(`[StudySession] StudySession.processResponse is running...`);\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] About to call logCardRecord...`);\n \n const cardHistory = this.logCardRecord(r).catch((e: unknown) => {\n console.error(`[StudySession] putCardRecord failed:`, e);\n throw e;\n });\n // console.log(`[StudySession] logCardRecord called, cardHistory promise created...`);\n\n // Get view constraints for response processing\n let maxAttemptsPerView = 1;\n let maxSessionViews = 1;\n if (isQuestionView(this.$refs.cardViewer?.$refs.activeView)) {\n const view = this.$refs.cardViewer.$refs.activeView;\n maxAttemptsPerView = view.maxAttemptsPerView;\n maxSessionViews = view.maxSessionViews;\n }\n const sessionViews = this.countCardViews(this.courseID, this.cardID);\n\n // Process response through SessionController\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] About to call submitResponse...`);\n const result: ResponseResult = await this.sessionController!.submitResponse(\n r,\n cardHistory,\n this.userCourseRegDoc!,\n this.currentCard,\n this.courseID,\n this.cardID,\n maxAttemptsPerView,\n maxSessionViews,\n sessionViews\n );\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] submitResponse completed, result:`, result);\n\n // Handle UI feedback based on result\n try {\n this.handleUIFeedback(result);\n } catch (error) {\n console.error(`[StudySession] Error handling UI feedback: ${error}.\\n\\nResult: ${JSON.stringify(result)}`);\n }\n\n // Fire deferred replan if requested — state is now fully recorded\n if (this.replanPending) {\n const opts = this.replanOptions;\n const label = opts?.label;\n const tag = label ? ` [${label}]` : '';\n console.log(`[StudySession] Firing deferred replan (post-submitResponse)${tag}`);\n this.replanPending = false;\n this.replanOptions = null;\n void this.sessionController!.requestReplan(opts ?? undefined);\n this.$emit('replan-requested');\n }\n\n // Handle navigation based on result\n if (result.deferred) {\n // Card requested deferred advancement — stash the action and wait\n // for a `ready-to-advance` event from the view before navigating.\n console.log(`[StudySession] Deferred advance — stashing action: ${result.nextCardAction}`);\n this.deferredNextCardAction = result.nextCardAction;\n } else if (result.shouldLoadNextCard) {\n this.loadCard(await this.sessionController!.nextCard(result.nextCardAction));\n }\n\n // Clear feedback shadow if requested\n if (result.shouldClearFeedbackShadow) {\n this.clearFeedbackShadow();\n }\n },\n\n handleUIFeedback(result: ResponseResult) {\n if (result.isCorrect) {\n // Handle correct response UI\n if (!this.frameless) {\n try {\n if (this.$refs.shadowWrapper && result.performanceScore !== undefined) {\n this.$refs.shadowWrapper.setAttribute('style', `--r: ${255 * (1 - result.performanceScore)}; --g:${255}`);\n this.$refs.shadowWrapper.classList.add('correct');\n }\n } catch (e) {\n console.warn(`[StudySession] Error setting shadowWrapper style: ${e}`);\n }\n }\n\n // Show confetti for correct responses\n if (this.sessionConfig.likesConfetti) {\n confetti({\n origin: {\n y: 1,\n x: 0.25 + 0.5 * Math.random(),\n },\n disableForReducedMotion: true,\n angle: 60 + 60 * Math.random(),\n });\n }\n } else {\n // Handle incorrect response UI\n if (!this.frameless) {\n try {\n if (this.$refs.shadowWrapper) {\n this.$refs.shadowWrapper.classList.add('incorrect');\n }\n } catch (e) {\n console.warn(`[StudySession] Error setting shadowWrapper style: ${e}`);\n }\n }\n }\n },\n\n clearFeedbackShadow() {\n if (this.frameless) return;\n\n setTimeout(() => {\n try {\n if (this.$refs.shadowWrapper) {\n (this.$refs.shadowWrapper as HTMLElement).classList.remove('correct', 'incorrect');\n }\n } catch (e) {\n // swallow error\n console.warn(`[StudySession] Error clearing shadowWrapper style: ${e}`);\n }\n }, 1250);\n },\n\n async logCardRecord(r: CardRecord): Promise<CardHistory<CardRecord>> {\n console.log(`[StudySession] About to call user.putCardRecord...`);\n const result = await this.user!.putCardRecord(r);\n console.log(`[StudySession] user.putCardRecord completed`);\n return result;\n },\n\n async loadCard(card: HydratedCard | null) {\n if (this.loading) {\n console.warn(`Attempted to load card while loading another...`);\n return;\n }\n\n console.log(`[StudySession] loading: ${JSON.stringify(card)}`);\n if (card === null) {\n this.sessionFinished = true;\n this.$emit('session-finished', this.sessionRecord);\n return;\n }\n this.cardType = card.item.status;\n\n this.loading = true;\n\n this.cardCount++;\n this.data = card.data;\n this.view = markRaw(card.view);\n this.cardID = card.item.cardID;\n this.courseID = card.item.courseID;\n this.card_elo = card.item.elo || 1000;\n\n this.sessionRecord.push({\n card: {\n course_id: card.item.courseID,\n card_id: card.item.cardID,\n card_elo: this.card_elo,\n tags: card.tags ?? [],\n },\n item: card.item,\n records: [],\n });\n\n this.$emit('card-loaded', {\n courseID: card.item.courseID,\n cardID: card.item.cardID,\n cardCount: this.cardCount,\n });\n\n this.loading = false;\n },\n },\n});\n</script>\n\n<style scoped>\n.footer-controls {\n position: fixed;\n bottom: 0;\n background-color: var(--v-background); /* Match your app's background color */\n z-index: 100;\n}\n\n.footer-right {\n position: fixed;\n bottom: 0;\n right: 0;\n background-color: var(--v-background); /* Match your app's background color */\n z-index: 100;\n}\n\n.correct {\n animation: varFade 1250ms ease-out;\n}\n\n.incorrect {\n animation: purpleFade 1250ms ease-out;\n}\n\na {\n text-decoration: underline;\n}\n\n.StudySession {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.card-transition-container {\n position: relative;\n overflow: hidden;\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n@keyframes varFade {\n 0% {\n box-shadow:\n rgba(var(--r), var(--g), 0, 0.25) 0px 7px 8px -4px,\n rgba(var(--r), var(--g), 0, 0.25) 0px 12px 17px 2px,\n rgba(var(--r), var(--g), 0, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(0, 150, 0, 0) 0px 0px;\n }\n}\n\n@keyframes greenFade {\n 0% {\n box-shadow:\n rgba(0, 150, 0, 0.25) 0px 7px 8px -4px,\n rgba(0, 150, 0, 0.25) 0px 12px 17px 2px,\n rgba(0, 150, 0, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(0, 150, 0, 0) 0px 0px;\n }\n}\n@keyframes purpleFade {\n 0% {\n box-shadow:\n rgba(115, 0, 75, 0.25) 0px 7px 8px -4px,\n rgba(115, 0, 75, 0.25) 0px 12px 17px 2px,\n rgba(115, 0, 75, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(115, 0, 75, 0) 0px 0px;\n }\n}\n</style>\n\n<style>\n/* Transition presets — unscoped so classes reach child component root elements.\n Vue's <Transition> adds these classes to the child's root DOM node, which\n may be a Vuetify component (v-card). Scoped selectors can fail to match\n when the scope attribute doesn't propagate through component boundaries. */\n\n/* Preset: component-fade (default) — simple opacity crossfade, use with mode=\"out-in\" */\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.2s ease;\n}\n.component-fade-enter-from,\n.component-fade-leave-to {\n opacity: 0;\n}\n\n/* Preset: card-slide — backOutLeft exit / slide-in-right enter, use with mode=\"default\" */\n/* !important on position/inset: child components with scoped styles (e.g.\n `.gpc-intro[data-v-xxx] { position: relative }`) have higher specificity\n than this unscoped class. Without !important the leaving element stays\n in-flow and the enter/leave elements split vertical space — visible as\n the new card sliding in \"low\" then popping to center. */\n.card-slide-leave-active {\n animation: cardBackOutLeft 400ms cubic-bezier(0.4, 0, 0.7, 0.2) both;\n position: absolute !important;\n inset: 0;\n margin: auto;\n height: fit-content;\n will-change: transform, opacity;\n pointer-events: none;\n}\n.card-slide-leave-active * {\n animation-play-state: paused !important;\n transition: none !important;\n}\n.card-slide-enter-active {\n animation: cardSlideInRight 250ms ease-out 100ms both;\n will-change: transform, opacity;\n}\n\n@keyframes cardBackOutLeft {\n 0% {\n transform: translateX(0) scale(1);\n opacity: 1;\n }\n 40% {\n transform: translateX(0) scale(0.88);\n opacity: 0.8;\n }\n 100% {\n transform: translateX(-100%) scale(0.88);\n opacity: 0;\n }\n}\n\n@keyframes cardSlideInRight {\n 0% {\n transform: translateX(60%);\n opacity: 0;\n }\n 100% {\n transform: translateX(0);\n opacity: 1;\n }\n}\n\n/* Preset: card-scale — scale-down exit / gentle overshoot enter, use with mode=\"out-in\" */\n.card-scale-leave-active {\n transition: transform 150ms ease-in, opacity 150ms ease-in;\n}\n.card-scale-enter-active {\n transition: transform 200ms cubic-bezier(0.34, 1.4, 0.64, 1), opacity 150ms ease-out;\n}\n.card-scale-leave-to {\n transform: scale(0.92);\n opacity: 0;\n}\n.card-scale-enter-from {\n transform: scale(1.03);\n opacity: 0;\n}\n</style>\n","<template>\n <div v-if=\"sessionPrepared\" class=\"StudySession\">\n <v-row v-if=\"!frameless\" align=\"center\">\n <!-- <h1 class=\"text-h3\" v-if=\"courseNames[courseID]\">{{ courseNames[courseID] }}:</h1> -->\n <v-spacer></v-spacer>\n <v-progress-circular v-if=\"loading\" color=\"primary\" indeterminate size=\"32\" width=\"4\" />\n </v-row>\n\n <!-- Debug Panel (only visible if window.debugMode is true) -->\n <session-controller-debug v-if=\"debugMode\" :session-controller=\"sessionController\" />\n\n <br v-if=\"!frameless\" />\n\n <div v-if=\"sessionFinished\">\n <slot name=\"session-finished\" :session-record=\"sessionRecord\" :report=\"sessionController?.report\">\n <div class=\"text-h4\">\n <p>Study session finished! Great job!</p>\n <p v-if=\"sessionController\">{{ sessionController.report }}</p>\n <!-- <p>\n Start <a @click=\"$emit('session-finished')\">another study session</a>, or try\n <router-link :to=\"`/edit/${courseID}`\">adding some new content</router-link> to challenge yourself and others!\n </p> -->\n <heat-map :activity-records-getter=\"() => user.getActivityRecords()\" />\n </div>\n </slot>\n </div>\n\n <div v-else ref=\"shadowWrapper\" class=\"card-transition-container\">\n <transition :name=\"transitionName\">\n <card-viewer\n v-if=\"view\"\n ref=\"cardViewer\"\n :key=\"cardID\"\n :class=\"loading ? 'muted' : ''\"\n :view=\"view\"\n :data=\"data\"\n :card_id=\"cardID\"\n :course_id=\"courseID\"\n :session-order=\"cardCount\"\n :user_elo=\"user_elo(courseID)\"\n :card_elo=\"card_elo\"\n :frameless=\"frameless\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"handleReplanRequest\"\n @ready-to-advance=\"handleReadyToAdvance\"\n />\n </transition>\n </div>\n\n <template v-if=\"!frameless\">\n <br />\n <div v-if=\"sessionController\">\n <span v-for=\"i in sessionController.failedCount\" :key=\"i\" class=\"text-h5\">•</span>\n </div>\n </template>\n\n <!--\n todo: reinstate tag editing at session-time ?\n\n <div v-if=\"!sessionFinished && editTags\">\n <p>Add tags to this card:</p>\n <sk-tags-input :course-i-d=\"courseID\" :card-i-d=\"cardID\" />\n </div> -->\n\n <v-row v-if=\"!hideFooter\" align=\"center\" class=\"footer-controls pa-5\">\n <v-col cols=\"auto\" class=\"d-flex flex-grow-0 mr-auto\">\n <StudySessionTimer\n :time-remaining=\"timeRemaining\"\n :session-time-limit=\"sessionTimeLimit\"\n @add-time=\"incrementSessionClock\"\n />\n </v-col>\n\n <v-spacer></v-spacer>\n\n <v-col cols=\"auto\" class=\"footer-right\">\n <SkMouseTrap />\n </v-col>\n </v-row>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, markRaw, PropType } from 'vue';\nimport { ViewComponent } from '../composables';\nimport { isQuestionView } from '../composables/CompositionViewable';\nimport HeatMap from './HeatMap.vue';\nimport SkMouseTrap from './SkMouseTrap.vue';\nimport { alertUser } from './SnackbarService';\nimport StudySessionTimer from './StudySessionTimer.vue';\nimport CardViewer from './cardRendering/CardViewer.vue';\nimport SessionControllerDebug from './SessionControllerDebug.vue';\n\nimport { CourseElo, Status, toCourseElo, ViewData } from '@vue-skuilder/common';\nimport {\n CardHistory,\n CardRecord,\n ClassroomDBInterface,\n ContentSourceID,\n CourseRegistrationDoc,\n DataLayerProvider,\n getStudySource,\n HydratedCard,\n isQuestionRecord,\n ReplanOptions,\n ResponseResult,\n SessionController,\n StudyContentSource,\n StudySessionRecord,\n UserDBInterface,\n} from '@vue-skuilder/db';\nimport confetti from 'canvas-confetti';\n\nimport { StudySessionConfig, CardTransitionPreset, CardTransitionMode } from './StudySession.types';\n\ninterface StudyRefs {\n shadowWrapper: HTMLDivElement;\n cardViewer: InstanceType<typeof CardViewer>;\n}\n\ntype StudyInstance = ReturnType<typeof defineComponent> & {\n $refs: StudyRefs;\n};\n\nexport default defineComponent({\n name: 'StudySession',\n\n ref: {} as StudyRefs,\n\n components: {\n CardViewer,\n StudySessionTimer,\n SkMouseTrap,\n HeatMap,\n SessionControllerDebug,\n },\n\n props: {\n sessionTimeLimit: {\n type: Number,\n required: true,\n },\n contentSources: {\n type: Array as PropType<ContentSourceID[]>,\n required: true,\n },\n user: {\n type: Object as PropType<UserDBInterface>,\n required: true,\n },\n dataLayer: {\n type: Object as PropType<DataLayerProvider>,\n required: true,\n },\n sessionConfig: {\n type: Object as PropType<StudySessionConfig>,\n default: () => ({ likesConfetti: false }),\n },\n getViewComponent: {\n type: Function as PropType<(viewId: string) => ViewComponent>,\n required: true,\n },\n frameless: {\n type: Boolean,\n default: false,\n },\n hideFooter: {\n type: Boolean,\n default: false,\n },\n transitionName: {\n type: String as PropType<CardTransitionPreset | string>,\n default: 'component-fade',\n },\n transitionMode: {\n type: String as PropType<CardTransitionMode>,\n default: 'out-in',\n },\n },\n\n emits: [\n 'session-finished',\n 'session-started',\n 'card-loaded',\n 'card-response',\n 'time-changed',\n 'session-prepared',\n 'session-error',\n 'replan-requested',\n ],\n\n data() {\n return {\n // editTags: false,\n cardID: '',\n view: null as ViewComponent | null,\n data: [] as ViewData[],\n courseID: '',\n card_elo: 1000,\n courseNames: {} as { [courseID: string]: string },\n cardCount: 1,\n sessionController: null as SessionController<ViewComponent> | null,\n sessionPrepared: false,\n sessionFinished: false,\n sessionRecord: [] as StudySessionRecord[],\n percentageRemaining: 100,\n timerIsActive: true,\n loading: false,\n userCourseRegDoc: null as CourseRegistrationDoc | null,\n sessionContentSources: [] as StudyContentSource[],\n timeRemaining: 300, // 5 minutes * 60 seconds\n replanPending: false,\n replanOptions: null as ReplanOptions | null,\n deferredNextCardAction: null as string | null,\n intervalHandler: null as NodeJS.Timeout | null,\n cardType: '',\n debugMode: (window as any).debugMode === true,\n };\n },\n\n computed: {\n currentCard(): StudySessionRecord {\n return this.sessionRecord[this.sessionRecord.length - 1];\n },\n },\n\n // watch: {\n // editCard: {\n // async handler(value: boolean) {\n // if (value) {\n // this.dataInputFormStore.dataInputForm.dataShape = await getCardDataShape(this.courseID, this.cardID);\n\n // const cfg = await getCredentialledCourseConfig(this.courseID);\n // this.dataInputFormStore.dataInputForm.course = cfg!;\n\n // this.editCardReady = true;\n\n // for (const oldField in this.dataInputFormStore.dataInputForm.localStore) {\n // if (oldField) {\n // console.log(`[Study] Removing old data: ${oldField}`);\n // delete this.dataInputFormStore.dataInputForm.localStore[oldField];\n // }\n // }\n\n // for (const field in this.data[0]) {\n // if (field) {\n // console.log(`[Study] Writing ${field}: ${this.data[0][field]} to the dataInputForm state...`);\n // this.dataInputFormStore.dataInputForm.localStore[field] = this.data[0][field];\n // }\n // }\n // } else {\n // this.editCardReady = false;\n // }\n // },\n // },\n // },\n\n async created() {\n this.userCourseRegDoc = await this.user.getCourseRegistrationsDoc();\n console.log('[StudySession] Created lifecycle hook - starting initSession');\n await this.initSession();\n console.log('[StudySession] InitSession completed in created hook');\n },\n\n methods: {\n /**\n * Handle a `ready-to-advance` event from a card view that previously\n * submitted with `deferAdvance: true`. Triggers the stashed navigation.\n */\n async handleReadyToAdvance() {\n const action = this.deferredNextCardAction;\n if (!action) {\n console.warn('[StudySession] ready-to-advance received but no deferred action stashed — ignoring');\n return;\n }\n console.log(`[StudySession] ready-to-advance received — advancing with stashed action: ${action}`);\n this.deferredNextCardAction = null;\n this.loadCard(await this.sessionController!.nextCard(action));\n },\n\n /**\n * Handle a replan request from a card view component.\n *\n * Card views emit 'request-replan' when they've made state changes that\n * should be reflected in the session's content queue (e.g. a GPC intro\n * unlocking new exercise cards).\n *\n * The replan is deferred — not fired immediately. processResponse()\n * checks this flag after submitResponse() has recorded ELO/tag\n * interactions, ensuring the pipeline sees fully up-to-date state.\n *\n * Accepts either a ReplanOptions object (new API) or a bare hints\n * Record (legacy). SessionController.requestReplan() normalises both.\n */\n handleReplanRequest(options?: ReplanOptions) {\n if (this.sessionController) {\n const label = (options as ReplanOptions)?.label;\n const tag = label ? ` [${label}]` : '';\n console.log(`[StudySession] Replan requested by card view${tag}, deferring until after response processing`);\n this.replanPending = true;\n this.replanOptions = (options as ReplanOptions) ?? null;\n }\n },\n\n user_elo(courseID: string): CourseElo {\n const courseDoc = this.userCourseRegDoc!.courses.find((c) => c.courseID === courseID);\n if (courseDoc) {\n return toCourseElo(courseDoc.elo);\n }\n return toCourseElo(undefined);\n },\n\n handleClassroomMessage() {\n return (v: unknown) => {\n alertUser({\n text: this.user?.getUsername() || '[Unknown user]',\n status: Status.ok,\n });\n console.log(`[StudySession] There was a change in the classroom DB:`);\n console.log(`[StudySession] change: ${v}`);\n console.log(`[StudySession] Stringified change: ${JSON.stringify(v)}`);\n return {};\n };\n },\n\n incrementSessionClock() {\n const max = 60 * this.sessionTimeLimit - this.timeRemaining;\n this.sessionController!.addTime(Math.min(max, 60));\n this.tick();\n },\n\n tick() {\n this.timeRemaining = this.sessionController!.secondsRemaining;\n\n this.percentageRemaining =\n this.timeRemaining > 60\n ? 100 * (this.timeRemaining / (60 * this.sessionTimeLimit))\n : 100 * (this.timeRemaining / 60);\n\n this.$emit('time-changed', this.timeRemaining);\n\n if (this.timeRemaining === 0) {\n clearInterval(this.intervalHandler!);\n }\n },\n\n async initSession() {\n let sessionClassroomDBs: ClassroomDBInterface[] = [];\n try {\n console.log(`[StudySession] starting study session w/ sources: ${JSON.stringify(this.contentSources)}`);\n console.log('[StudySession] Beginning preparation process');\n\n this.sessionContentSources = markRaw(\n (\n await Promise.all(\n this.contentSources.map(async (s) => {\n try {\n return await getStudySource(s, this.user);\n } catch (e) {\n console.error(`Failed to load study source: ${s.type}/${s.id}`, e);\n return null;\n }\n })\n )\n ).filter((s: unknown) => s !== null)\n );\n\n this.timeRemaining = this.sessionTimeLimit * 60;\n\n sessionClassroomDBs = await Promise.all(\n this.contentSources\n .filter((s) => s.type === 'classroom')\n .map(async (c) => await this.dataLayer.getClassroomDB(c.id, 'student'))\n );\n\n sessionClassroomDBs.forEach((_db) => {\n // db.setChangeFcn(this.handleClassroomMessage());\n });\n\n const scOptions = this.sessionConfig?.defaultBatchLimit !== undefined\n ? { defaultBatchLimit: this.sessionConfig.defaultBatchLimit }\n : undefined;\n\n this.sessionController = markRaw(\n new SessionController<ViewComponent>(\n this.sessionContentSources,\n 60 * this.sessionTimeLimit,\n this.dataLayer,\n this.getViewComponent,\n undefined, // mixer — use default\n scOptions\n )\n );\n this.sessionController.sessionRecord = this.sessionRecord;\n\n // Apply init hints if provided (e.g. post-lesson boost tags)\n if (this.sessionConfig?.initHints) {\n for (const source of this.sessionContentSources) {\n source.setEphemeralHints?.(this.sessionConfig.initHints as any);\n }\n console.log('[StudySession] Applied init hints to content sources');\n }\n\n await this.sessionController.prepareSession();\n this.intervalHandler = setInterval(this.tick, 1000);\n\n this.sessionPrepared = true;\n\n console.log('[StudySession] Session preparation complete, emitting session-prepared event');\n this.$emit('session-prepared');\n console.log('[StudySession] Event emission completed');\n } catch (error) {\n console.error('[StudySession] Error during session preparation:', error);\n // Notify parent component about the error\n this.$emit('session-error', { message: 'Failed to prepare study session', error });\n }\n\n try {\n this.contentSources\n .filter((s) => s.type === 'course')\n .forEach(\n async (c) => (this.courseNames[c.id] = (await this.dataLayer.getCoursesDB().getCourseConfig(c.id)).name)\n );\n\n console.log(`[StudySession] Session created:\n ${this.sessionController?.toString() || 'Session controller not initialized'}\n User courses: ${this.contentSources\n .filter((s) => s.type === 'course')\n .map((c) => c.id)\n .toString()}\n User classrooms: ${sessionClassroomDBs.map((db: any) => db._id).toString() || 'No classrooms'}\n `);\n } catch (error) {\n console.error('[StudySession] Error during final session setup:', error);\n }\n\n if (this.sessionController) {\n try {\n this.$emit('session-started');\n this.loadCard(await this.sessionController.nextCard());\n } catch (error) {\n console.error('[StudySession] Error loading next card:', error);\n this.$emit('session-error', { message: 'Failed to load study card', error });\n }\n } else {\n console.error('[StudySession] Cannot load card: session controller not initialized');\n this.$emit('session-error', { message: 'Study session initialization failed' });\n }\n },\n\n countCardViews(course_id: string, card_id: string): number {\n return this.sessionRecord.filter((r) => r.card.course_id === course_id && r.card.card_id === card_id).length;\n },\n\n async processResponse(this: StudyInstance, r: CardRecord) {\n this.$emit('card-response', r);\n\n this.timerIsActive = true;\n\n r.cardID = this.cardID;\n r.courseID = this.courseID;\n this.currentCard.records.push(r);\n\n console.log(`[StudySession] StudySession.processResponse is running...`);\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] About to call logCardRecord...`);\n \n const cardHistory = this.logCardRecord(r).catch((e: unknown) => {\n console.error(`[StudySession] putCardRecord failed:`, e);\n throw e;\n });\n // console.log(`[StudySession] logCardRecord called, cardHistory promise created...`);\n\n // Get view constraints for response processing\n let maxAttemptsPerView = 1;\n let maxSessionViews = 1;\n if (isQuestionView(this.$refs.cardViewer?.$refs.activeView)) {\n const view = this.$refs.cardViewer.$refs.activeView;\n maxAttemptsPerView = view.maxAttemptsPerView;\n maxSessionViews = view.maxSessionViews;\n }\n const sessionViews = this.countCardViews(this.courseID, this.cardID);\n\n // Process response through SessionController\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] About to call submitResponse...`);\n const result: ResponseResult = await this.sessionController!.submitResponse(\n r,\n cardHistory,\n this.userCourseRegDoc!,\n this.currentCard,\n this.courseID,\n this.cardID,\n maxAttemptsPerView,\n maxSessionViews,\n sessionViews\n );\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] submitResponse completed, result:`, result);\n\n // Handle UI feedback based on result\n try {\n this.handleUIFeedback(result);\n } catch (error) {\n console.error(`[StudySession] Error handling UI feedback: ${error}.\\n\\nResult: ${JSON.stringify(result)}`);\n }\n\n // Fire deferred replan if requested — state is now fully recorded\n if (this.replanPending) {\n const opts = this.replanOptions;\n const label = opts?.label;\n const tag = label ? ` [${label}]` : '';\n console.log(`[StudySession] Firing deferred replan (post-submitResponse)${tag}`);\n this.replanPending = false;\n this.replanOptions = null;\n void this.sessionController!.requestReplan(opts ?? undefined);\n this.$emit('replan-requested');\n }\n\n // Handle navigation based on result\n if (result.deferred) {\n // Card requested deferred advancement — stash the action and wait\n // for a `ready-to-advance` event from the view before navigating.\n console.log(`[StudySession] Deferred advance — stashing action: ${result.nextCardAction}`);\n this.deferredNextCardAction = result.nextCardAction;\n } else if (result.shouldLoadNextCard) {\n this.loadCard(await this.sessionController!.nextCard(result.nextCardAction));\n }\n\n // Clear feedback shadow if requested\n if (result.shouldClearFeedbackShadow) {\n this.clearFeedbackShadow();\n }\n },\n\n handleUIFeedback(result: ResponseResult) {\n if (result.isCorrect) {\n // Handle correct response UI\n if (!this.frameless) {\n try {\n if (this.$refs.shadowWrapper && result.performanceScore !== undefined) {\n this.$refs.shadowWrapper.setAttribute('style', `--r: ${255 * (1 - result.performanceScore)}; --g:${255}`);\n this.$refs.shadowWrapper.classList.add('correct');\n }\n } catch (e) {\n console.warn(`[StudySession] Error setting shadowWrapper style: ${e}`);\n }\n }\n\n // Show confetti for correct responses\n if (this.sessionConfig.likesConfetti) {\n confetti({\n origin: {\n y: 1,\n x: 0.25 + 0.5 * Math.random(),\n },\n disableForReducedMotion: true,\n angle: 60 + 60 * Math.random(),\n });\n }\n } else {\n // Handle incorrect response UI\n if (!this.frameless) {\n try {\n if (this.$refs.shadowWrapper) {\n this.$refs.shadowWrapper.classList.add('incorrect');\n }\n } catch (e) {\n console.warn(`[StudySession] Error setting shadowWrapper style: ${e}`);\n }\n }\n }\n },\n\n clearFeedbackShadow() {\n if (this.frameless) return;\n\n setTimeout(() => {\n try {\n if (this.$refs.shadowWrapper) {\n (this.$refs.shadowWrapper as HTMLElement).classList.remove('correct', 'incorrect');\n }\n } catch (e) {\n // swallow error\n console.warn(`[StudySession] Error clearing shadowWrapper style: ${e}`);\n }\n }, 1250);\n },\n\n async logCardRecord(r: CardRecord): Promise<CardHistory<CardRecord>> {\n console.log(`[StudySession] About to call user.putCardRecord...`);\n const result = await this.user!.putCardRecord(r);\n console.log(`[StudySession] user.putCardRecord completed`);\n return result;\n },\n\n async loadCard(card: HydratedCard | null) {\n if (this.loading) {\n console.warn(`Attempted to load card while loading another...`);\n return;\n }\n\n console.log(`[StudySession] loading: ${JSON.stringify(card)}`);\n if (card === null) {\n this.sessionFinished = true;\n this.$emit('session-finished', this.sessionRecord);\n return;\n }\n this.cardType = card.item.status;\n\n this.loading = true;\n\n this.cardCount++;\n this.data = card.data;\n this.view = markRaw(card.view);\n this.cardID = card.item.cardID;\n this.courseID = card.item.courseID;\n this.card_elo = card.item.elo || 1000;\n\n this.sessionRecord.push({\n card: {\n course_id: card.item.courseID,\n card_id: card.item.cardID,\n card_elo: this.card_elo,\n tags: card.tags ?? [],\n },\n item: card.item,\n records: [],\n });\n\n this.$emit('card-loaded', {\n courseID: card.item.courseID,\n cardID: card.item.cardID,\n cardCount: this.cardCount,\n });\n\n this.loading = false;\n },\n },\n});\n</script>\n\n<style scoped>\n.footer-controls {\n position: fixed;\n bottom: 0;\n background-color: var(--v-background); /* Match your app's background color */\n z-index: 100;\n}\n\n.footer-right {\n position: fixed;\n bottom: 0;\n right: 0;\n background-color: var(--v-background); /* Match your app's background color */\n z-index: 100;\n}\n\n.correct {\n animation: varFade 1250ms ease-out;\n}\n\n.incorrect {\n animation: purpleFade 1250ms ease-out;\n}\n\na {\n text-decoration: underline;\n}\n\n.StudySession {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.card-transition-container {\n position: relative;\n overflow: hidden;\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n@keyframes varFade {\n 0% {\n box-shadow:\n rgba(var(--r), var(--g), 0, 0.25) 0px 7px 8px -4px,\n rgba(var(--r), var(--g), 0, 0.25) 0px 12px 17px 2px,\n rgba(var(--r), var(--g), 0, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(0, 150, 0, 0) 0px 0px;\n }\n}\n\n@keyframes greenFade {\n 0% {\n box-shadow:\n rgba(0, 150, 0, 0.25) 0px 7px 8px -4px,\n rgba(0, 150, 0, 0.25) 0px 12px 17px 2px,\n rgba(0, 150, 0, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(0, 150, 0, 0) 0px 0px;\n }\n}\n@keyframes purpleFade {\n 0% {\n box-shadow:\n rgba(115, 0, 75, 0.25) 0px 7px 8px -4px,\n rgba(115, 0, 75, 0.25) 0px 12px 17px 2px,\n rgba(115, 0, 75, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(115, 0, 75, 0) 0px 0px;\n }\n}\n</style>\n\n<style>\n/* Transition presets — unscoped so classes reach child component root elements.\n Vue's <Transition> adds these classes to the child's root DOM node, which\n may be a Vuetify component (v-card). Scoped selectors can fail to match\n when the scope attribute doesn't propagate through component boundaries. */\n\n/* Preset: component-fade (default) — simple opacity crossfade, use with mode=\"out-in\" */\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.2s ease;\n}\n.component-fade-enter-from,\n.component-fade-leave-to {\n opacity: 0;\n}\n\n/* Preset: card-slide — backOutLeft exit / slide-in-right enter, use with mode=\"default\" */\n/* !important on position/inset: child components with scoped styles (e.g.\n `.gpc-intro[data-v-xxx] { position: relative }`) have higher specificity\n than this unscoped class. Without !important the leaving element stays\n in-flow and the enter/leave elements split vertical space — visible as\n the new card sliding in \"low\" then popping to center. */\n.card-slide-leave-active {\n animation: cardBackOutLeft 400ms cubic-bezier(0.4, 0, 0.7, 0.2) both;\n position: absolute !important;\n inset: 0;\n margin: auto;\n height: fit-content;\n will-change: transform, opacity;\n pointer-events: none;\n}\n.card-slide-leave-active * {\n animation-play-state: paused !important;\n transition: none !important;\n}\n.card-slide-enter-active {\n animation: cardSlideInRight 250ms ease-out 100ms both;\n will-change: transform, opacity;\n}\n\n@keyframes cardBackOutLeft {\n 0% {\n transform: translateX(0) scale(1);\n opacity: 1;\n }\n 40% {\n transform: translateX(0) scale(0.88);\n opacity: 0.8;\n }\n 100% {\n transform: translateX(-100%) scale(0.88);\n opacity: 0;\n }\n}\n\n@keyframes cardSlideInRight {\n 0% {\n transform: translateX(60%);\n opacity: 0;\n }\n 100% {\n transform: translateX(0);\n opacity: 1;\n }\n}\n\n/* Preset: card-scale — scale-down exit / gentle overshoot enter, use with mode=\"out-in\" */\n.card-scale-leave-active {\n transition: transform 150ms ease-in, opacity 150ms ease-in;\n}\n.card-scale-enter-active {\n transition: transform 200ms cubic-bezier(0.34, 1.4, 0.64, 1), opacity 150ms ease-out;\n}\n.card-scale-leave-to {\n transform: scale(0.92);\n opacity: 0;\n}\n.card-scale-enter-from {\n transform: scale(1.03);\n opacity: 0;\n}\n</style>\n","<template>\n <v-card :class=\"`${className}`\" @mouseover=\"select\" @click=\"submitThisOption\">\n <markdown-renderer :md=\"content\" />\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, defineAsyncComponent, PropType } from 'vue';\n\nexport default defineComponent({\n name: 'MultipleChoiceOption',\n\n components: {\n MarkdownRenderer: defineAsyncComponent(() => import('../../components/cardRendering/MarkdownRenderer.vue')),\n },\n\n props: {\n content: {\n type: String,\n required: true,\n },\n selected: {\n type: Boolean,\n required: true,\n },\n number: {\n type: Number,\n required: true,\n },\n setSelection: {\n type: Function as PropType<(selection: number) => void>,\n required: true,\n },\n submit: {\n type: Function as PropType<() => void>,\n required: true,\n },\n markedWrong: {\n type: Boolean,\n required: true,\n },\n },\n\n computed: {\n className(): string {\n let color: string;\n\n switch (this.number) {\n case 0:\n color = 'bg-red';\n break;\n case 1:\n color = 'bg-purple';\n break;\n case 2:\n color = 'bg-indigo';\n break;\n case 3:\n color = 'bg-light-blue';\n break;\n case 4:\n color = 'bg-teal';\n break;\n case 5:\n color = 'bg-deep-orange';\n break;\n default:\n color = 'bg-grey';\n break;\n }\n\n if (this.selected && !this.markedWrong) {\n return `choice selected ${color} lighten-3 elevation-8`;\n } else if (!this.selected && !this.markedWrong) {\n return `choice not-selected ${color} lighten-4 elevation-1`;\n } else if (this.selected && this.markedWrong) {\n return `choice selected grey lighten-2 elevation-8`;\n } else if (!this.selected && this.markedWrong) {\n return 'choice not-selected grey lighten-2 elevation-0';\n } else {\n throw new Error(\n `'selected' and 'markedWrong' props in MultipleChoiceOption are in an impossible configuration.`\n );\n }\n },\n },\n\n methods: {\n select(): void {\n this.setSelection(this.number);\n },\n\n submitThisOption(): void {\n if (this.markedWrong) {\n return;\n } else {\n this.select();\n this.submit();\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.choice {\n text-align: center;\n display: inline-block;\n border-radius: 4px;\n padding-left: 15px;\n padding-right: 15px;\n padding-top: 5px;\n padding-bottom: 5px;\n margin: 10px;\n min-width: 75px; /* prevent tiny click-btns on, eg, one-letter answers */\n transition: all 0.2s ease-in-out;\n}\n\n.selected {\n transform: translateY(-10px) /* rotate(3deg) */ scale(1.15);\n z-index: 1;\n}\n\n.not-selected {\n z-index: 0;\n}\n</style>\n","<template>\n <v-card :class=\"`${className}`\" @mouseover=\"select\" @click=\"submitThisOption\">\n <markdown-renderer :md=\"content\" />\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, defineAsyncComponent, PropType } from 'vue';\n\nexport default defineComponent({\n name: 'MultipleChoiceOption',\n\n components: {\n MarkdownRenderer: defineAsyncComponent(() => import('../../components/cardRendering/MarkdownRenderer.vue')),\n },\n\n props: {\n content: {\n type: String,\n required: true,\n },\n selected: {\n type: Boolean,\n required: true,\n },\n number: {\n type: Number,\n required: true,\n },\n setSelection: {\n type: Function as PropType<(selection: number) => void>,\n required: true,\n },\n submit: {\n type: Function as PropType<() => void>,\n required: true,\n },\n markedWrong: {\n type: Boolean,\n required: true,\n },\n },\n\n computed: {\n className(): string {\n let color: string;\n\n switch (this.number) {\n case 0:\n color = 'bg-red';\n break;\n case 1:\n color = 'bg-purple';\n break;\n case 2:\n color = 'bg-indigo';\n break;\n case 3:\n color = 'bg-light-blue';\n break;\n case 4:\n color = 'bg-teal';\n break;\n case 5:\n color = 'bg-deep-orange';\n break;\n default:\n color = 'bg-grey';\n break;\n }\n\n if (this.selected && !this.markedWrong) {\n return `choice selected ${color} lighten-3 elevation-8`;\n } else if (!this.selected && !this.markedWrong) {\n return `choice not-selected ${color} lighten-4 elevation-1`;\n } else if (this.selected && this.markedWrong) {\n return `choice selected grey lighten-2 elevation-8`;\n } else if (!this.selected && this.markedWrong) {\n return 'choice not-selected grey lighten-2 elevation-0';\n } else {\n throw new Error(\n `'selected' and 'markedWrong' props in MultipleChoiceOption are in an impossible configuration.`\n );\n }\n },\n },\n\n methods: {\n select(): void {\n this.setSelection(this.number);\n },\n\n submitThisOption(): void {\n if (this.markedWrong) {\n return;\n } else {\n this.select();\n this.submit();\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.choice {\n text-align: center;\n display: inline-block;\n border-radius: 4px;\n padding-left: 15px;\n padding-right: 15px;\n padding-top: 5px;\n padding-bottom: 5px;\n margin: 10px;\n min-width: 75px; /* prevent tiny click-btns on, eg, one-letter answers */\n transition: all 0.2s ease-in-out;\n}\n\n.selected {\n transform: translateY(-10px) /* rotate(3deg) */ scale(1.15);\n z-index: 1;\n}\n\n.not-selected {\n z-index: 0;\n}\n</style>\n","<template>\n <div ref=\"containerRef\" class=\"multipleChoice\">\n <MultipleChoiceOption\n v-for=\"(choice, i) in choiceList\"\n :key=\"i\"\n :content=\"choice\"\n :selected=\"choiceList.indexOf(choice) === currentSelection\"\n :number=\"choiceList.indexOf(choice)\"\n :set-selection=\"setSelection\"\n :submit=\"forwardSelection\"\n :marked-wrong=\"choiceIsWrong(choice)\"\n />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport UserInput from './BaseUserInput';\nimport MultipleChoiceOption from './MultipleChoiceOption.vue';\nimport { defineComponent, PropType } from 'vue';\nimport { SkldrMouseTrap } from '../../utils/SkldrMouseTrap';\nimport { RadioMultipleChoiceAnswer } from './RadioMultipleChoice.types';\n\nexport default defineComponent({\n name: 'RadioMultipleChoice',\n components: {\n MultipleChoiceOption,\n },\n extends: UserInput,\n props: {\n choiceList: {\n type: Array as PropType<string[]>,\n required: true,\n },\n },\n data() {\n return {\n currentSelection: -1,\n incorrectSelections: [] as number[],\n containerRef: null as null | HTMLElement,\n _registeredHotkeys: [] as (string | string[])[],\n };\n },\n watch: {\n choiceList: {\n immediate: true,\n handler(newList) {\n if (newList?.length) {\n this.bindKeys();\n }\n },\n },\n },\n mounted() {\n if (this.containerRef) {\n this.containerRef.focus();\n }\n },\n unmounted() {\n this.unbindKeys();\n },\n methods: {\n forwardSelection(): void {\n if (this.choiceIsWrong(this.choiceList[this.currentSelection])) {\n return;\n } else if (this.currentSelection !== -1) {\n const ans: RadioMultipleChoiceAnswer = {\n choiceList: this.choiceList,\n selection: this.currentSelection,\n };\n const record = this.submitAnswer(ans);\n\n if (!record.isCorrect) {\n this.incorrectSelections.push(this.currentSelection);\n }\n }\n },\n setSelection(selection: number): void {\n if (selection < this.choiceList.length) {\n this.currentSelection = selection;\n }\n },\n incrementSelection(): void {\n if (this.currentSelection === -1) {\n this.currentSelection = Math.ceil(this.choiceList.length / 2);\n } else {\n this.currentSelection = Math.min(this.choiceList.length - 1, this.currentSelection + 1);\n }\n },\n decrementSelection(): void {\n if (this.currentSelection === -1) {\n this.currentSelection = Math.floor(this.choiceList.length / 2 - 1);\n } else {\n this.currentSelection = Math.max(0, this.currentSelection - 1);\n }\n },\n choiceIsWrong(choice: string): boolean {\n let ret = false;\n this.incorrectSelections.forEach((sel) => {\n if (this.choiceList[sel] === choice) {\n ret = true;\n }\n });\n return ret;\n },\n bindKeys() {\n const hotkeys = [\n {\n hotkey: 'left',\n callback: this.decrementSelection,\n command: 'Move selection left',\n },\n {\n hotkey: 'right',\n callback: this.incrementSelection,\n command: 'Move selection right',\n },\n {\n hotkey: 'enter',\n callback: this.forwardSelection,\n command: 'Submit selection',\n },\n // Add number key bindings\n ...Array.from({ length: this.choiceList.length }, (_, i) => ({\n hotkey: (i + 1).toString(),\n callback: () => this.setSelection(i),\n command: `Select ${((i) => {\n switch (i) {\n case 0:\n return 'first';\n case 1:\n return 'second';\n case 2:\n return 'third';\n case 3:\n return 'fourth';\n case 4:\n return 'fifth';\n case 5:\n return 'sixth';\n default:\n return `${i + 1}th`;\n }\n })(i)} option`,\n })),\n ];\n\n SkldrMouseTrap.addBinding(hotkeys);\n \n // Store hotkeys for cleanup\n this._registeredHotkeys = hotkeys.map(k => k.hotkey);\n },\n\n unbindKeys() {\n // Remove specific hotkeys instead of resetting all\n if (this._registeredHotkeys) {\n this._registeredHotkeys.forEach(key => {\n SkldrMouseTrap.removeBinding(key);\n });\n }\n },\n // bindNumberKey(n: number): void {\n // this.MouseTrap.bind(n.toString(), () => {\n // this.currentSelection = n - 1;\n // });\n // },\n },\n});\n</script>\n","<template>\n <div ref=\"containerRef\" class=\"multipleChoice\">\n <MultipleChoiceOption\n v-for=\"(choice, i) in choiceList\"\n :key=\"i\"\n :content=\"choice\"\n :selected=\"choiceList.indexOf(choice) === currentSelection\"\n :number=\"choiceList.indexOf(choice)\"\n :set-selection=\"setSelection\"\n :submit=\"forwardSelection\"\n :marked-wrong=\"choiceIsWrong(choice)\"\n />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport UserInput from './BaseUserInput';\nimport MultipleChoiceOption from './MultipleChoiceOption.vue';\nimport { defineComponent, PropType } from 'vue';\nimport { SkldrMouseTrap } from '../../utils/SkldrMouseTrap';\nimport { RadioMultipleChoiceAnswer } from './RadioMultipleChoice.types';\n\nexport default defineComponent({\n name: 'RadioMultipleChoice',\n components: {\n MultipleChoiceOption,\n },\n extends: UserInput,\n props: {\n choiceList: {\n type: Array as PropType<string[]>,\n required: true,\n },\n },\n data() {\n return {\n currentSelection: -1,\n incorrectSelections: [] as number[],\n containerRef: null as null | HTMLElement,\n _registeredHotkeys: [] as (string | string[])[],\n };\n },\n watch: {\n choiceList: {\n immediate: true,\n handler(newList) {\n if (newList?.length) {\n this.bindKeys();\n }\n },\n },\n },\n mounted() {\n if (this.containerRef) {\n this.containerRef.focus();\n }\n },\n unmounted() {\n this.unbindKeys();\n },\n methods: {\n forwardSelection(): void {\n if (this.choiceIsWrong(this.choiceList[this.currentSelection])) {\n return;\n } else if (this.currentSelection !== -1) {\n const ans: RadioMultipleChoiceAnswer = {\n choiceList: this.choiceList,\n selection: this.currentSelection,\n };\n const record = this.submitAnswer(ans);\n\n if (!record.isCorrect) {\n this.incorrectSelections.push(this.currentSelection);\n }\n }\n },\n setSelection(selection: number): void {\n if (selection < this.choiceList.length) {\n this.currentSelection = selection;\n }\n },\n incrementSelection(): void {\n if (this.currentSelection === -1) {\n this.currentSelection = Math.ceil(this.choiceList.length / 2);\n } else {\n this.currentSelection = Math.min(this.choiceList.length - 1, this.currentSelection + 1);\n }\n },\n decrementSelection(): void {\n if (this.currentSelection === -1) {\n this.currentSelection = Math.floor(this.choiceList.length / 2 - 1);\n } else {\n this.currentSelection = Math.max(0, this.currentSelection - 1);\n }\n },\n choiceIsWrong(choice: string): boolean {\n let ret = false;\n this.incorrectSelections.forEach((sel) => {\n if (this.choiceList[sel] === choice) {\n ret = true;\n }\n });\n return ret;\n },\n bindKeys() {\n const hotkeys = [\n {\n hotkey: 'left',\n callback: this.decrementSelection,\n command: 'Move selection left',\n },\n {\n hotkey: 'right',\n callback: this.incrementSelection,\n command: 'Move selection right',\n },\n {\n hotkey: 'enter',\n callback: this.forwardSelection,\n command: 'Submit selection',\n },\n // Add number key bindings\n ...Array.from({ length: this.choiceList.length }, (_, i) => ({\n hotkey: (i + 1).toString(),\n callback: () => this.setSelection(i),\n command: `Select ${((i) => {\n switch (i) {\n case 0:\n return 'first';\n case 1:\n return 'second';\n case 2:\n return 'third';\n case 3:\n return 'fourth';\n case 4:\n return 'fifth';\n case 5:\n return 'sixth';\n default:\n return `${i + 1}th`;\n }\n })(i)} option`,\n })),\n ];\n\n SkldrMouseTrap.addBinding(hotkeys);\n \n // Store hotkeys for cleanup\n this._registeredHotkeys = hotkeys.map(k => k.hotkey);\n },\n\n unbindKeys() {\n // Remove specific hotkeys instead of resetting all\n if (this._registeredHotkeys) {\n this._registeredHotkeys.forEach(key => {\n SkldrMouseTrap.removeBinding(key);\n });\n }\n },\n // bindNumberKey(n: number): void {\n // this.MouseTrap.bind(n.toString(), () => {\n // this.currentSelection = n - 1;\n // });\n // },\n },\n});\n</script>\n","<template>\n <div data-viewable=\"TrueFalse\">\n <RadioMultipleChoice :choice-list=\"['True', 'False']\" :MouseTrap=\"MouseTrap\" :submit=\"submit\" />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport RadioMultipleChoice from './RadioMultipleChoice.vue';\n\nexport default defineComponent({\n name: 'TrueFalse',\n components: {\n RadioMultipleChoice,\n },\n props: {\n MouseTrap: {\n type: Object as PropType<any>,\n required: true,\n },\n submit: {\n type: Function as PropType<(selection: number) => void>,\n required: true,\n },\n },\n});\n</script>\n","<template>\n <div data-viewable=\"TrueFalse\">\n <RadioMultipleChoice :choice-list=\"['True', 'False']\" :MouseTrap=\"MouseTrap\" :submit=\"submit\" />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport RadioMultipleChoice from './RadioMultipleChoice.vue';\n\nexport default defineComponent({\n name: 'TrueFalse',\n components: {\n RadioMultipleChoice,\n },\n props: {\n MouseTrap: {\n type: Object as PropType<any>,\n required: true,\n },\n submit: {\n type: Function as PropType<(selection: number) => void>,\n required: true,\n },\n },\n});\n</script>\n","<template>\n <v-container class=\"pa-0\">\n <v-text-field\n ref=\"input\"\n v-model=\"answer\"\n prepend-icon=\"edit\"\n :autofocus=\"autofocus\"\n row-height=\"24\"\n toggle-keys=\"[13,32]\"\n class=\"text-h5\"\n :rules=\"[isNumeric]\"\n @keyup.enter=\"submitAnswer(makeNumeric(answer))\"\n ></v-text-field>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { Answer } from '@vue-skuilder/common';\nimport UserInput from './BaseUserInput';\nimport { defineComponent } from 'vue';\n\ntype InputNumberRefs = {\n input: HTMLInputElement;\n};\n\ntype InputNumberInstance = ReturnType<typeof defineComponent> & {\n $refs: InputNumberRefs;\n};\n\nexport default defineComponent({\n name: 'UserInputNumber',\n\n ref: {} as InputNumberRefs,\n\n extends: UserInput,\n\n methods: {\n mounted(this: InputNumberInstance) {\n this.$refs.input.focus();\n },\n\n isNumeric(s: string): boolean {\n return !isNaN(Number.parseFloat(s));\n },\n\n makeNumeric(num: Answer): number {\n if (typeof num === 'string') {\n return Number.parseFloat(num);\n } else {\n throw new Error('Expected a string, got ' + typeof num);\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.userInput {\n border: none;\n text-align: center;\n border-bottom: 1px black;\n}\n</style>\n","<template>\n <v-container class=\"pa-0\">\n <v-text-field\n ref=\"input\"\n v-model=\"answer\"\n prepend-icon=\"edit\"\n :autofocus=\"autofocus\"\n row-height=\"24\"\n toggle-keys=\"[13,32]\"\n class=\"text-h5\"\n :rules=\"[isNumeric]\"\n @keyup.enter=\"submitAnswer(makeNumeric(answer))\"\n ></v-text-field>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { Answer } from '@vue-skuilder/common';\nimport UserInput from './BaseUserInput';\nimport { defineComponent } from 'vue';\n\ntype InputNumberRefs = {\n input: HTMLInputElement;\n};\n\ntype InputNumberInstance = ReturnType<typeof defineComponent> & {\n $refs: InputNumberRefs;\n};\n\nexport default defineComponent({\n name: 'UserInputNumber',\n\n ref: {} as InputNumberRefs,\n\n extends: UserInput,\n\n methods: {\n mounted(this: InputNumberInstance) {\n this.$refs.input.focus();\n },\n\n isNumeric(s: string): boolean {\n return !isNaN(Number.parseFloat(s));\n },\n\n makeNumeric(num: Answer): number {\n if (typeof num === 'string') {\n return Number.parseFloat(num);\n } else {\n throw new Error('Expected a string, got ' + typeof num);\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.userInput {\n border: none;\n text-align: center;\n border-bottom: 1px black;\n}\n</style>\n","<template>\n <card-viewer\n v-if=\"!loading\"\n class=\"ma-2\"\n :class=\"loading ? 'muted' : ''\"\n :view=\"view\"\n :data=\"data\"\n :card_id=\"cardID\"\n :course_id=\"courseID\"\n :session-order=\"sessionOrder\"\n @emit-response=\"processResponse($event)\"\n />\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, markRaw } from 'vue';\nimport { getDataLayer, QualifiedCardID } from '@vue-skuilder/db';\nimport {\n log,\n displayableDataToViewData,\n ViewData,\n ViewDescriptor,\n CardData,\n CardRecord,\n DisplayableData,\n} from '@vue-skuilder/common';\nimport { ViewComponent } from '../../composables';\nimport CardViewer from './CardViewer.vue';\n\nexport default defineComponent({\n name: 'CardLoader',\n\n components: {\n CardViewer,\n },\n\n props: {\n sessionOrder: {\n type: Number,\n required: false,\n default: 0,\n },\n qualified_id: {\n type: Object as PropType<QualifiedCardID>,\n required: true,\n },\n viewLookup: {\n type: Function as PropType<(viewDescription: ViewDescriptor | string) => ViewComponent>,\n required: true,\n },\n },\n\n data() {\n return {\n loading: true,\n view: null as ViewComponent | null,\n data: [] as ViewData[],\n courseID: '',\n cardID: '',\n };\n },\n\n created() {\n this.loadCard();\n },\n\n watch: {\n qualified_id: {\n immediate: true,\n handler() {\n this.loadCard();\n },\n },\n },\n\n methods: {\n processResponse(r: CardRecord) {\n log(`\n Card was displayed at ${r.timeStamp}\n User spent ${r.timeSpent} milliseconds with the card.\n `);\n this.$emit('emitResponse', r);\n },\n\n async loadCard() {\n const qualified_id = this.qualified_id;\n console.log(`Card Loader displaying: ${qualified_id.courseID}::${qualified_id.cardID}`);\n\n this.loading = true;\n const _courseID = qualified_id.courseID;\n const _cardID = qualified_id.cardID;\n const courseDB = getDataLayer().getCourseDB(_courseID);\n\n try {\n const tmpCardData = (await courseDB.getCourseDoc(_cardID)) as CardData;\n const tmpView = this.viewLookup(tmpCardData.id_view);\n const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {\n return courseDB.getCourseDoc(id, {\n attachments: true,\n binary: true,\n });\n });\n\n const tmpData = [];\n\n for (const docPromise of tmpDataDocs) {\n const doc = (await docPromise) as DisplayableData;\n tmpData.unshift(displayableDataToViewData(doc));\n }\n\n this.data = tmpData;\n this.view = markRaw(tmpView as ViewComponent);\n this.cardID = _cardID;\n this.courseID = _courseID;\n } catch (e) {\n throw new Error(`[CardLoader] Error loading card: ${JSON.stringify(e)}, ${e}`);\n } finally {\n this.loading = false;\n this.$emit('card-loaded');\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.cardView {\n padding: 15px;\n border-radius: 8px;\n}\n\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.3s ease;\n}\n.component-fade-enter, .component-fade-leave-to\n/* .component-fade-leave-active below version 2.1.8 */ {\n opacity: 0;\n}\n</style>\n","<template>\n <card-viewer\n v-if=\"!loading\"\n class=\"ma-2\"\n :class=\"loading ? 'muted' : ''\"\n :view=\"view\"\n :data=\"data\"\n :card_id=\"cardID\"\n :course_id=\"courseID\"\n :session-order=\"sessionOrder\"\n @emit-response=\"processResponse($event)\"\n />\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, markRaw } from 'vue';\nimport { getDataLayer, QualifiedCardID } from '@vue-skuilder/db';\nimport {\n log,\n displayableDataToViewData,\n ViewData,\n ViewDescriptor,\n CardData,\n CardRecord,\n DisplayableData,\n} from '@vue-skuilder/common';\nimport { ViewComponent } from '../../composables';\nimport CardViewer from './CardViewer.vue';\n\nexport default defineComponent({\n name: 'CardLoader',\n\n components: {\n CardViewer,\n },\n\n props: {\n sessionOrder: {\n type: Number,\n required: false,\n default: 0,\n },\n qualified_id: {\n type: Object as PropType<QualifiedCardID>,\n required: true,\n },\n viewLookup: {\n type: Function as PropType<(viewDescription: ViewDescriptor | string) => ViewComponent>,\n required: true,\n },\n },\n\n data() {\n return {\n loading: true,\n view: null as ViewComponent | null,\n data: [] as ViewData[],\n courseID: '',\n cardID: '',\n };\n },\n\n created() {\n this.loadCard();\n },\n\n watch: {\n qualified_id: {\n immediate: true,\n handler() {\n this.loadCard();\n },\n },\n },\n\n methods: {\n processResponse(r: CardRecord) {\n log(`\n Card was displayed at ${r.timeStamp}\n User spent ${r.timeSpent} milliseconds with the card.\n `);\n this.$emit('emitResponse', r);\n },\n\n async loadCard() {\n const qualified_id = this.qualified_id;\n console.log(`Card Loader displaying: ${qualified_id.courseID}::${qualified_id.cardID}`);\n\n this.loading = true;\n const _courseID = qualified_id.courseID;\n const _cardID = qualified_id.cardID;\n const courseDB = getDataLayer().getCourseDB(_courseID);\n\n try {\n const tmpCardData = (await courseDB.getCourseDoc(_cardID)) as CardData;\n const tmpView = this.viewLookup(tmpCardData.id_view);\n const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {\n return courseDB.getCourseDoc(id, {\n attachments: true,\n binary: true,\n });\n });\n\n const tmpData = [];\n\n for (const docPromise of tmpDataDocs) {\n const doc = (await docPromise) as DisplayableData;\n tmpData.unshift(displayableDataToViewData(doc));\n }\n\n this.data = tmpData;\n this.view = markRaw(tmpView as ViewComponent);\n this.cardID = _cardID;\n this.courseID = _courseID;\n } catch (e) {\n throw new Error(`[CardLoader] Error loading card: ${JSON.stringify(e)}, ${e}`);\n } finally {\n this.loading = false;\n this.$emit('card-loaded');\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.cardView {\n padding: 15px;\n border-radius: 8px;\n}\n\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.3s ease;\n}\n.component-fade-enter, .component-fade-leave-to\n/* .component-fade-leave-active below version 2.1.8 */ {\n opacity: 0;\n}\n</style>\n","// stores/useAuthStore.ts\nimport { defineStore, setActivePinia } from 'pinia';\nimport { getDataLayer, UserDBInterface } from '@vue-skuilder/db';\nimport { getPinia } from '../plugins/pinia';\n\nexport interface AuthState {\n _user: UserDBInterface | undefined;\n loginAndRegistration: {\n init: boolean;\n loggedIn: boolean;\n regDialogOpen: boolean;\n loginDialogOpen: boolean;\n };\n onLoadComplete: boolean;\n}\n\nexport async function getCurrentUser(): Promise<UserDBInterface | undefined> {\n const store = useAuthStore();\n\n if (!store.onLoadComplete) {\n // Wait for initialization\n let retries = 200;\n const timeout = 50;\n while (!store.onLoadComplete && retries > 0) {\n await new Promise((resolve) => setTimeout(resolve, timeout));\n retries--;\n }\n if (!store.onLoadComplete) {\n throw new Error('User initialization timed out');\n }\n }\n\n return getDataLayer().getUserDB();\n}\n\nexport const useAuthStore = () => {\n // Get the Pinia instance from the plugin\n const pinia = getPinia();\n if (pinia) {\n setActivePinia(pinia);\n }\n\n // Return the store\n return defineStore('auth', {\n state: (): AuthState => ({\n _user: undefined as UserDBInterface | undefined,\n loginAndRegistration: {\n init: false,\n loggedIn: false,\n regDialogOpen: false,\n loginDialogOpen: false,\n },\n onLoadComplete: false,\n }),\n\n actions: {\n async init() {\n try {\n this._user = getDataLayer().getUserDB();\n\n this.loginAndRegistration.loggedIn = this._user ? this._user.isLoggedIn() : false;\n\n this.onLoadComplete = true;\n this.loginAndRegistration.init = true;\n } catch (e) {\n console.error('Failed to initialize auth store:', e);\n // Set onLoadComplete even on error to prevent timeout\n this.loginAndRegistration.loggedIn = false;\n this.onLoadComplete = true;\n this.loginAndRegistration.init = true;\n }\n },\n\n setLoginDialog(open: boolean) {\n this.loginAndRegistration.loginDialogOpen = open;\n },\n\n setRegDialog(open: boolean) {\n this.loginAndRegistration.regDialogOpen = open;\n },\n\n async resetUserData() {\n try {\n if (!this._user) {\n throw new Error('No user available for data reset');\n }\n \n const result = await this._user.resetUserData();\n if (result.status !== 'ok') {\n throw new Error(result.error || 'Reset failed');\n }\n \n console.log('User data reset successfully');\n return result;\n } catch (error) {\n console.error('Failed to reset user data:', error);\n throw error;\n }\n },\n },\n\n getters: {\n currentUser: async () => getCurrentUser(),\n isLoggedIn: (state) => state.loginAndRegistration.loggedIn,\n isInitialized: (state) => state.loginAndRegistration.init,\n status: (state) => {\n return {\n loggedIn: state.loginAndRegistration.loggedIn,\n init: state.loginAndRegistration.init,\n user: state._user,\n };\n },\n },\n })();\n};\n","// stores/useConfigStore.ts\nimport { defineStore, setActivePinia } from 'pinia';\nimport { getCurrentUser } from './useAuthStore';\nimport { UserConfig } from '@vue-skuilder/db';\nimport { getPinia } from '../plugins/pinia';\n\nexport const useConfigStore = () => {\n // Get the Pinia instance from the plugin\n const pinia = getPinia();\n if (pinia) {\n setActivePinia(pinia);\n }\n\n // Return the store\n return defineStore('config', {\n state: () => ({\n config: {\n darkMode: false,\n likesConfetti: false,\n sessionTimeLimit: 5, // Default 5 minutes\n } as UserConfig,\n }),\n\n actions: {\n updateConfig(newConfig: UserConfig) {\n this.config = newConfig;\n },\n async updateDarkMode(darkMode: boolean) {\n this.config.darkMode = darkMode;\n const user = await getCurrentUser();\n if (user) {\n await user.setConfig({\n darkMode,\n });\n }\n },\n\n async updateLikesConfetti(likesConfetti: boolean) {\n this.config.likesConfetti = likesConfetti;\n const user = await getCurrentUser();\n if (user) {\n await user.setConfig({\n likesConfetti,\n });\n }\n },\n\n async updateSessionTimeLimit(sessionTimeLimit: number) {\n this.config.sessionTimeLimit = sessionTimeLimit;\n const user = await getCurrentUser();\n if (user) {\n await user.setConfig({\n sessionTimeLimit,\n });\n }\n },\n\n async hydrate() {\n try {\n const user = await getCurrentUser();\n if (user) {\n const cfg = await user.getConfig();\n console.log(`user config: ${JSON.stringify(cfg)}`);\n this.updateConfig(cfg);\n } else {\n console.log('No user logged in, using default config');\n }\n } catch (e) {\n console.warn('Failed to hydrate config store, using defaults:', e);\n }\n },\n async init() {\n await this.hydrate();\n },\n resetDefaults() {\n this.config = {\n darkMode: false,\n likesConfetti: false,\n sessionTimeLimit: 5,\n };\n },\n },\n })();\n};\n","import { ref, computed } from 'vue';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\nexport interface AuthUIConfig {\n showLoginRegistration: boolean;\n showLogout: boolean;\n showResetData: boolean;\n logoutLabel: string;\n resetLabel: string;\n}\n\nexport function useAuthUI() {\n const isLoading = ref(true);\n const syncStrategyDetected = ref(false);\n const isLocalOnlyMode = ref(false);\n\n const config = computed<AuthUIConfig>(() => {\n if (isLocalOnlyMode.value) {\n return {\n showLoginRegistration: false,\n showLogout: false,\n showResetData: true,\n logoutLabel: '',\n resetLabel: 'Reset User Data',\n };\n } else {\n return {\n showLoginRegistration: true,\n showLogout: true,\n showResetData: false,\n logoutLabel: 'Log out',\n resetLabel: '',\n };\n }\n });\n\n const detectSyncStrategy = async () => {\n try {\n isLoading.value = true;\n const user = await getCurrentUser();\n \n // Access the sync strategy through the user's private syncStrategy property\n // NoOpSyncStrategy (local-only) returns false for canCreateAccount\n // CouchDBSyncStrategy (remote sync) returns true for canCreateAccount\n const userInternal = user as { syncStrategy?: { canCreateAccount?: () => boolean } };\n const canCreateAccount = userInternal.syncStrategy?.canCreateAccount?.();\n \n isLocalOnlyMode.value = !canCreateAccount;\n syncStrategyDetected.value = true;\n } catch (error) {\n console.error('Failed to detect sync strategy:', error);\n // Default to remote sync mode on error\n isLocalOnlyMode.value = false;\n syncStrategyDetected.value = true;\n } finally {\n isLoading.value = false;\n }\n };\n\n return {\n config,\n isLoading,\n syncStrategyDetected,\n isLocalOnlyMode,\n detectSyncStrategy,\n };\n}","<template>\n <v-badge :content=\"items.length\" :model-value=\"hasNewItems\" color=\"accent\" location=\"end top\">\n <v-menu location=\"bottom end\" transition=\"scale-transition\">\n <template #activator=\"{ props }\">\n <v-chip v-bind=\"props\" class=\"ma-2\">\n <v-avatar start class=\"bg-primary\">\n <v-icon>mdi-school</v-icon>\n </v-avatar>\n {{ username }}\n </v-chip>\n </template>\n\n <v-list>\n <v-list-item v-for=\"item in items\" :key=\"item\" @click=\"dismiss(item)\">\n <v-list-item-title>{{ item }}</v-list-item-title>\n </v-list-item>\n\n <v-divider v-if=\"items.length\" />\n\n <v-list-item @click=\"gotoStats\">\n <template #prepend>\n <v-icon>mdi-trending-up</v-icon>\n </template>\n <v-list-item-title>Stats</v-list-item-title>\n </v-list-item>\n\n <v-list-item @click=\"gotoSettings\">\n <template #prepend>\n <v-icon>mdi-cog</v-icon>\n </template>\n <v-list-item-title>Settings</v-list-item-title>\n </v-list-item>\n\n <v-list-item v-if=\"authUIConfig.showLogout\" @click=\"logout\">\n <template #prepend>\n <v-icon>mdi-logout</v-icon>\n </template>\n <v-list-item-title>{{ authUIConfig.logoutLabel }}</v-list-item-title>\n </v-list-item>\n\n <v-list-item v-if=\"authUIConfig.showResetData\" @click=\"showResetDialog = true\">\n <template #prepend>\n <v-icon>mdi-delete-sweep</v-icon>\n </template>\n <v-list-item-title>{{ authUIConfig.resetLabel }}</v-list-item-title>\n </v-list-item>\n </v-list>\n </v-menu>\n </v-badge>\n\n <!-- Reset Confirmation Dialog -->\n <v-dialog v-model=\"showResetDialog\" max-width=\"500px\" persistent>\n <v-card>\n <v-card-title class=\"text-h5 d-flex align-center\">\n <v-icon color=\"warning\" class=\"mr-3\">mdi-alert-circle</v-icon>\n Reset All User Data\n </v-card-title>\n \n <v-card-text>\n <p class=\"mb-4\">This will permanently delete:</p>\n <ul class=\"mb-4\">\n <li>All course progress and history</li>\n <li>Scheduled card reviews</li>\n <li>Course registrations</li>\n <li>User preferences</li>\n </ul>\n <p class=\"mb-4 text-error font-weight-bold\">This cannot be undone.</p>\n \n <v-text-field\n v-model=\"confirmationText\"\n label='Type \"reset\" to confirm'\n outlined\n dense\n @keyup.enter=\"isConfirmationValid && executeReset()\"\n />\n </v-card-text>\n \n <v-card-actions>\n <v-spacer />\n <v-btn text @click=\"resetDialogState\">Cancel</v-btn>\n <v-btn \n color=\"error\" \n :disabled=\"!isConfirmationValid\"\n @click=\"executeReset\"\n >\n Reset All Data\n </v-btn>\n </v-card-actions>\n </v-card>\n </v-dialog>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, computed, onMounted } from 'vue';\nimport { useRouter } from 'vue-router';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { useConfigStore } from '../../stores/useConfigStore';\nimport { useAuthUI } from '../../composables/useAuthUI';\n\n// Define props (even if not used, prevents warnings)\ndefineProps<{\n showLoginButton?: boolean;\n redirectToPath?: string;\n}>();\n\nconst router = useRouter();\nconst authStore = useAuthStore();\nconst configStore = useConfigStore();\nconst authUI = useAuthUI();\n\nconst username = ref('');\nconst items = ref<string[]>([]);\n\n// Reset confirmation dialog state\nconst showResetDialog = ref(false);\nconst confirmationText = ref('');\n\nconst isConfirmationValid = computed(() => confirmationText.value === 'reset');\n\nconst resetDialogState = () => {\n confirmationText.value = '';\n showResetDialog.value = false;\n};\n\nconst hasNewItems = computed(() => items.value.length > 0);\n\nconst authUIConfig = computed(() => {\n const configValue = authUI.config.value;\n const fallback = {\n showLoginRegistration: true,\n showLogout: true,\n showResetData: false,\n logoutLabel: 'Log out',\n resetLabel: '',\n };\n \n return configValue || fallback;\n});\n\nonMounted(async () => {\n const user = await getCurrentUser();\n username.value = user.getUsername();\n \n // Detect sync strategy for conditional UI\n await authUI.detectSyncStrategy();\n});\n\nconst gotoSettings = async () => {\n router.push(`/u/${(await getCurrentUser()).getUsername()}`);\n};\n\nconst gotoStats = async () => {\n router.push(`/u/${(await getCurrentUser()).getUsername()}/stats`);\n};\n\nconst dismiss = (item: string) => {\n const index = items.value.indexOf(item);\n items.value.splice(index, 1);\n};\n\nconst logout = async () => {\n const res = await authStore._user!.logout();\n if (res.ok) {\n authStore.loginAndRegistration = {\n init: true,\n loggedIn: false,\n regDialogOpen: false,\n loginDialogOpen: false,\n };\n\n configStore.resetDefaults();\n router.push('/home');\n }\n};\n\nconst executeReset = async () => {\n try {\n await authStore.resetUserData();\n configStore.resetDefaults();\n \n // Close dialog and navigate to home\n resetDialogState();\n router.push('/home');\n } catch (error) {\n console.error('Failed to reset user data:', error);\n // Could add a snackbar notification here\n }\n};\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n</style>","<template>\n <v-badge :content=\"items.length\" :model-value=\"hasNewItems\" color=\"accent\" location=\"end top\">\n <v-menu location=\"bottom end\" transition=\"scale-transition\">\n <template #activator=\"{ props }\">\n <v-chip v-bind=\"props\" class=\"ma-2\">\n <v-avatar start class=\"bg-primary\">\n <v-icon>mdi-school</v-icon>\n </v-avatar>\n {{ username }}\n </v-chip>\n </template>\n\n <v-list>\n <v-list-item v-for=\"item in items\" :key=\"item\" @click=\"dismiss(item)\">\n <v-list-item-title>{{ item }}</v-list-item-title>\n </v-list-item>\n\n <v-divider v-if=\"items.length\" />\n\n <v-list-item @click=\"gotoStats\">\n <template #prepend>\n <v-icon>mdi-trending-up</v-icon>\n </template>\n <v-list-item-title>Stats</v-list-item-title>\n </v-list-item>\n\n <v-list-item @click=\"gotoSettings\">\n <template #prepend>\n <v-icon>mdi-cog</v-icon>\n </template>\n <v-list-item-title>Settings</v-list-item-title>\n </v-list-item>\n\n <v-list-item v-if=\"authUIConfig.showLogout\" @click=\"logout\">\n <template #prepend>\n <v-icon>mdi-logout</v-icon>\n </template>\n <v-list-item-title>{{ authUIConfig.logoutLabel }}</v-list-item-title>\n </v-list-item>\n\n <v-list-item v-if=\"authUIConfig.showResetData\" @click=\"showResetDialog = true\">\n <template #prepend>\n <v-icon>mdi-delete-sweep</v-icon>\n </template>\n <v-list-item-title>{{ authUIConfig.resetLabel }}</v-list-item-title>\n </v-list-item>\n </v-list>\n </v-menu>\n </v-badge>\n\n <!-- Reset Confirmation Dialog -->\n <v-dialog v-model=\"showResetDialog\" max-width=\"500px\" persistent>\n <v-card>\n <v-card-title class=\"text-h5 d-flex align-center\">\n <v-icon color=\"warning\" class=\"mr-3\">mdi-alert-circle</v-icon>\n Reset All User Data\n </v-card-title>\n \n <v-card-text>\n <p class=\"mb-4\">This will permanently delete:</p>\n <ul class=\"mb-4\">\n <li>All course progress and history</li>\n <li>Scheduled card reviews</li>\n <li>Course registrations</li>\n <li>User preferences</li>\n </ul>\n <p class=\"mb-4 text-error font-weight-bold\">This cannot be undone.</p>\n \n <v-text-field\n v-model=\"confirmationText\"\n label='Type \"reset\" to confirm'\n outlined\n dense\n @keyup.enter=\"isConfirmationValid && executeReset()\"\n />\n </v-card-text>\n \n <v-card-actions>\n <v-spacer />\n <v-btn text @click=\"resetDialogState\">Cancel</v-btn>\n <v-btn \n color=\"error\" \n :disabled=\"!isConfirmationValid\"\n @click=\"executeReset\"\n >\n Reset All Data\n </v-btn>\n </v-card-actions>\n </v-card>\n </v-dialog>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, computed, onMounted } from 'vue';\nimport { useRouter } from 'vue-router';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { useConfigStore } from '../../stores/useConfigStore';\nimport { useAuthUI } from '../../composables/useAuthUI';\n\n// Define props (even if not used, prevents warnings)\ndefineProps<{\n showLoginButton?: boolean;\n redirectToPath?: string;\n}>();\n\nconst router = useRouter();\nconst authStore = useAuthStore();\nconst configStore = useConfigStore();\nconst authUI = useAuthUI();\n\nconst username = ref('');\nconst items = ref<string[]>([]);\n\n// Reset confirmation dialog state\nconst showResetDialog = ref(false);\nconst confirmationText = ref('');\n\nconst isConfirmationValid = computed(() => confirmationText.value === 'reset');\n\nconst resetDialogState = () => {\n confirmationText.value = '';\n showResetDialog.value = false;\n};\n\nconst hasNewItems = computed(() => items.value.length > 0);\n\nconst authUIConfig = computed(() => {\n const configValue = authUI.config.value;\n const fallback = {\n showLoginRegistration: true,\n showLogout: true,\n showResetData: false,\n logoutLabel: 'Log out',\n resetLabel: '',\n };\n \n return configValue || fallback;\n});\n\nonMounted(async () => {\n const user = await getCurrentUser();\n username.value = user.getUsername();\n \n // Detect sync strategy for conditional UI\n await authUI.detectSyncStrategy();\n});\n\nconst gotoSettings = async () => {\n router.push(`/u/${(await getCurrentUser()).getUsername()}`);\n};\n\nconst gotoStats = async () => {\n router.push(`/u/${(await getCurrentUser()).getUsername()}/stats`);\n};\n\nconst dismiss = (item: string) => {\n const index = items.value.indexOf(item);\n items.value.splice(index, 1);\n};\n\nconst logout = async () => {\n const res = await authStore._user!.logout();\n if (res.ok) {\n authStore.loginAndRegistration = {\n init: true,\n loggedIn: false,\n regDialogOpen: false,\n loginDialogOpen: false,\n };\n\n configStore.resetDefaults();\n router.push('/home');\n }\n};\n\nconst executeReset = async () => {\n try {\n await authStore.resetUserData();\n configStore.resetDefaults();\n \n // Close dialog and navigate to home\n resetDialogState();\n router.push('/home');\n } catch (error) {\n console.error('Failed to reset user data:', error);\n // Could add a snackbar notification here\n }\n};\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n</style>","<template>\n <v-card>\n <v-card-title v-if=\"!loginRoute\" class=\"text-h5 bg-grey-lighten-2\">Log In</v-card-title>\n\n <v-card-text>\n <v-form onsubmit=\"return false;\" @submit.prevent=\"login\">\n <v-text-field\n id=\"\"\n v-model=\"username\"\n autofocus\n name=\"username\"\n label=\"Username\"\n prepend-icon=\"mdi-account-circle\"\n ></v-text-field>\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n hover=\"Show password input\"\n label=\"Enter your password\"\n hint=\"\"\n min=\"0\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n\n <v-snackbar v-model=\"badLoginAttempt\" location=\"bottom right\" :timeout=\"errorTimeout\">\n Username or password was incorrect.\n <v-btn color=\"pink\" variant=\"text\" @click=\"badLoginAttempt = false\">Close</v-btn>\n </v-snackbar>\n\n <div class=\"d-flex flex-column align-start\">\n <div class=\"mb-2\">\n <v-btn class=\"mr-2\" type=\"submit\" :loading=\"awaitingResponse\" :color=\"buttonStatus.color\">\n <v-icon start>mdi-lock-open</v-icon>\n Log In\n </v-btn>\n <router-link v-if=\"loginRoute\" to=\"signup\">\n <v-btn variant=\"text\">Create New Account</v-btn>\n </router-link>\n <v-btn v-else variant=\"text\" @click=\"toggle\">Create New Account</v-btn>\n </div>\n\n <slot name=\"forgot-password\">\n <!-- Default: always show forgot password link -->\n <a\n href=\"#\"\n class=\"text-caption text-decoration-none\"\n @click.prevent=\"handleForgotPassword\"\n >\n Forgot password?\n </a>\n </slot>\n </div>\n </v-form>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, computed } from 'vue';\nimport { useRouter, useRoute } from 'vue-router';\nimport { alertUser } from '../SnackbarService';\nimport { log } from '@vue-skuilder/common';\nimport { Status } from '@vue-skuilder/common';\nimport { User } from '@vue-skuilder/db';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { useConfigStore } from '../../stores/useConfigStore';\n\n// Define props\ninterface Props {\n redirectTo?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n redirectTo: '/study'\n});\n\n// Define emits for toggle, cleanup, and forgot password\nconst emit = defineEmits<{\n toggle: [];\n loginSuccess: [redirectPath: string];\n forgotPassword: [];\n}>();\n\nconst router = useRouter();\nconst route = useRoute();\nconst authStore = useAuthStore();\nconst configStore = useConfigStore();\n\nconst username = ref('');\nconst password = ref('');\nconst passwordVisible = ref(false);\nconst awaitingResponse = ref(false);\nconst badLoginAttempt = ref(false);\nconst errorTimeout = ref(7000);\nconst user = ref<User | undefined>(undefined);\n\nconst loginRoute = computed(() => route.name === 'login');\n\nconst buttonStatus = computed(() => ({\n color: badLoginAttempt.value ? 'error' : 'success',\n text: badLoginAttempt.value ? 'Try again' : 'Log In',\n}));\n\nconst initBadLogin = () => {\n badLoginAttempt.value = true;\n\n alertUser({\n text: 'Username or password was incorrect.',\n status: Status.error,\n timeout: errorTimeout.value,\n });\n setTimeout(() => {\n badLoginAttempt.value = false;\n }, errorTimeout.value);\n};\n\nconst login = async () => {\n awaitingResponse.value = true;\n log('Starting login attempt');\n log(`Login attempt for username: ${username.value}`);\n\n try {\n log('Attempting to get User instance');\n // #172 starting point - why is the pre-existing _user being referenced here?\n user.value = await getCurrentUser();\n log('Got User instance, attempting login');\n\n await user.value.login(username.value, password.value);\n log('Login successful');\n\n // load user config\n log('Initializing user config');\n configStore.init();\n log('User config initialized');\n\n // set login state\n log('Setting authentication state');\n authStore.loginAndRegistration.loggedIn = true;\n log(`Authentication state set, redirecting to: ${props.redirectTo}`);\n \n // Emit success event for cleanup\n emit('loginSuccess', props.redirectTo);\n \n router.push(props.redirectTo);\n log('Login and redirect complete');\n } catch (e) {\n // entry #186\n log('Login attempt failed');\n log(`Login error details: ${JSON.stringify(e)}`);\n console.log(`login error: ${JSON.stringify(e)}`);\n // - differentiate response\n // - return better message to UI\n log('Initiating bad login feedback');\n initBadLogin();\n }\n\n log('Resetting awaiting response state');\n awaitingResponse.value = false;\n};\n\n\nconst toggle = () => {\n log('Toggling registration / login forms.');\n emit('toggle');\n};\n\nconst handleForgotPassword = () => {\n log('Forgot password clicked');\n // If on login route, navigate directly\n if (loginRoute.value) {\n router.push('/request-reset');\n } else {\n // Otherwise, emit event for parent to handle (e.g., modal context)\n emit('forgotPassword');\n }\n};\n</script>\n\n<style lang=\"css\" scoped>\n.login {\n max-width: 650px;\n}\n</style>\n","<template>\n <v-card>\n <v-card-title v-if=\"!loginRoute\" class=\"text-h5 bg-grey-lighten-2\">Log In</v-card-title>\n\n <v-card-text>\n <v-form onsubmit=\"return false;\" @submit.prevent=\"login\">\n <v-text-field\n id=\"\"\n v-model=\"username\"\n autofocus\n name=\"username\"\n label=\"Username\"\n prepend-icon=\"mdi-account-circle\"\n ></v-text-field>\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n hover=\"Show password input\"\n label=\"Enter your password\"\n hint=\"\"\n min=\"0\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n\n <v-snackbar v-model=\"badLoginAttempt\" location=\"bottom right\" :timeout=\"errorTimeout\">\n Username or password was incorrect.\n <v-btn color=\"pink\" variant=\"text\" @click=\"badLoginAttempt = false\">Close</v-btn>\n </v-snackbar>\n\n <div class=\"d-flex flex-column align-start\">\n <div class=\"mb-2\">\n <v-btn class=\"mr-2\" type=\"submit\" :loading=\"awaitingResponse\" :color=\"buttonStatus.color\">\n <v-icon start>mdi-lock-open</v-icon>\n Log In\n </v-btn>\n <router-link v-if=\"loginRoute\" to=\"signup\">\n <v-btn variant=\"text\">Create New Account</v-btn>\n </router-link>\n <v-btn v-else variant=\"text\" @click=\"toggle\">Create New Account</v-btn>\n </div>\n\n <slot name=\"forgot-password\">\n <!-- Default: always show forgot password link -->\n <a\n href=\"#\"\n class=\"text-caption text-decoration-none\"\n @click.prevent=\"handleForgotPassword\"\n >\n Forgot password?\n </a>\n </slot>\n </div>\n </v-form>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, computed } from 'vue';\nimport { useRouter, useRoute } from 'vue-router';\nimport { alertUser } from '../SnackbarService';\nimport { log } from '@vue-skuilder/common';\nimport { Status } from '@vue-skuilder/common';\nimport { User } from '@vue-skuilder/db';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { useConfigStore } from '../../stores/useConfigStore';\n\n// Define props\ninterface Props {\n redirectTo?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n redirectTo: '/study'\n});\n\n// Define emits for toggle, cleanup, and forgot password\nconst emit = defineEmits<{\n toggle: [];\n loginSuccess: [redirectPath: string];\n forgotPassword: [];\n}>();\n\nconst router = useRouter();\nconst route = useRoute();\nconst authStore = useAuthStore();\nconst configStore = useConfigStore();\n\nconst username = ref('');\nconst password = ref('');\nconst passwordVisible = ref(false);\nconst awaitingResponse = ref(false);\nconst badLoginAttempt = ref(false);\nconst errorTimeout = ref(7000);\nconst user = ref<User | undefined>(undefined);\n\nconst loginRoute = computed(() => route.name === 'login');\n\nconst buttonStatus = computed(() => ({\n color: badLoginAttempt.value ? 'error' : 'success',\n text: badLoginAttempt.value ? 'Try again' : 'Log In',\n}));\n\nconst initBadLogin = () => {\n badLoginAttempt.value = true;\n\n alertUser({\n text: 'Username or password was incorrect.',\n status: Status.error,\n timeout: errorTimeout.value,\n });\n setTimeout(() => {\n badLoginAttempt.value = false;\n }, errorTimeout.value);\n};\n\nconst login = async () => {\n awaitingResponse.value = true;\n log('Starting login attempt');\n log(`Login attempt for username: ${username.value}`);\n\n try {\n log('Attempting to get User instance');\n // #172 starting point - why is the pre-existing _user being referenced here?\n user.value = await getCurrentUser();\n log('Got User instance, attempting login');\n\n await user.value.login(username.value, password.value);\n log('Login successful');\n\n // load user config\n log('Initializing user config');\n configStore.init();\n log('User config initialized');\n\n // set login state\n log('Setting authentication state');\n authStore.loginAndRegistration.loggedIn = true;\n log(`Authentication state set, redirecting to: ${props.redirectTo}`);\n \n // Emit success event for cleanup\n emit('loginSuccess', props.redirectTo);\n \n router.push(props.redirectTo);\n log('Login and redirect complete');\n } catch (e) {\n // entry #186\n log('Login attempt failed');\n log(`Login error details: ${JSON.stringify(e)}`);\n console.log(`login error: ${JSON.stringify(e)}`);\n // - differentiate response\n // - return better message to UI\n log('Initiating bad login feedback');\n initBadLogin();\n }\n\n log('Resetting awaiting response state');\n awaitingResponse.value = false;\n};\n\n\nconst toggle = () => {\n log('Toggling registration / login forms.');\n emit('toggle');\n};\n\nconst handleForgotPassword = () => {\n log('Forgot password clicked');\n // If on login route, navigate directly\n if (loginRoute.value) {\n router.push('/request-reset');\n } else {\n // Otherwise, emit event for parent to handle (e.g., modal context)\n emit('forgotPassword');\n }\n};\n</script>\n\n<style lang=\"css\" scoped>\n.login {\n max-width: 650px;\n}\n</style>\n","/**\n * Simple password validation utilities.\n *\n * Requirements:\n * - At least 6 characters\n * - At least 2 distinct characters\n */\n\nexport function validatePassword(password: string): string {\n if (!password) return '';\n\n // At least 6 characters\n if (password.length < 6) {\n return 'Password must be at least 6 characters';\n }\n\n // At least 2 distinct characters\n const uniqueChars = new Set(password).size;\n if (uniqueChars < 2) {\n return 'Password must contain at least 2 different characters';\n }\n\n return '';\n}\n\n/**\n * Check if password is valid (no error message)\n */\nexport function isPasswordValid(password: string): boolean {\n return validatePassword(password) === '';\n}\n","<template>\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\"> Create an Account </v-card-title>\n\n <v-card-text>\n <v-form @submit.prevent=\"createUser\">\n <v-text-field\n v-model=\"email\"\n name=\"email\"\n label=\"Email address\"\n type=\"email\"\n prepend-icon=\"mdi-email\"\n :error=\"emailError\"\n :hint=\"emailHint\"\n @blur=\"validateEmail\"\n ></v-text-field>\n <v-text-field\n id=\"\"\n ref=\"userNameTextField\"\n v-model=\"username\"\n name=\"username\"\n label=\"Choose a Username\"\n prepend-icon=\"mdi-account-circle\"\n :error=\"usernameError\"\n :hint=\"usernameHint\"\n @blur=\"validateUsername\"\n ></v-text-field>\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n hover=\"Show password\"\n label=\"Create a password\"\n :hint=\"passwordError\"\n :error=\"!!passwordError\"\n min=\"4\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n <v-text-field\n v-model=\"retypedPassword\"\n prepend-icon=\"mdi-lock\"\n name=\"retypedPassword\"\n hover=\"Show password\"\n label=\"Retype your password\"\n :disabled=\"password === '' || !!passwordError\"\n :hint=\"passwordRetypeError\"\n min=\"4\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n ></v-text-field>\n\n <!-- <v-checkbox label=\"Student\" v-model=\"student\" ></v-checkbox>\n <v-checkbox label=\"Teacher\" v-model=\"teacher\" ></v-checkbox>\n <v-checkbox label=\"Author\" v-model=\"author\" ></v-checkbox> -->\n\n <v-snackbar v-model=\"badLoginAttempt\" location=\"bottom right\" :timeout=\"5000\">\n Username or password was incorrect.\n <v-btn color=\"pink\" variant=\"text\" @click=\"badLoginAttempt = false\"> Close </v-btn>\n </v-snackbar>\n <v-btn\n class=\"mr-2 my-2\"\n type=\"submit\"\n :loading=\"awaitingResponse\"\n :color=\"buttonStatus.color\"\n :disabled=\"!!passwordError || password !== retypedPassword\"\n >\n <v-icon start>mdi-lock-open</v-icon>\n Create Account\n </v-btn>\n <router-link v-if=\"registrationRoute\" to=\"login\">\n <v-btn variant=\"text\">Log In</v-btn>\n </router-link>\n <v-btn v-else variant=\"text\" @click=\"toggle\"> Log In </v-btn>\n </v-form>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { UserDBInterface } from '@vue-skuilder/db';\nimport { alertUser } from '../SnackbarService';\nimport { Status, log } from '@vue-skuilder/common';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { sendVerificationEmail } from '../../services/authAPI';\nimport { validatePassword } from '../../utils/passwordValidation';\n\nexport default defineComponent({\n name: 'UserRegistration',\n\n props: {\n onSignupSuccess: {\n type: Function as PropType<(payload: { username: string }) => void>,\n required: false,\n },\n },\n\n emits: ['toggle', 'signup-success'],\n\n data() {\n return {\n email: '',\n username: '',\n password: '',\n retypedPassword: '',\n passwordVisible: false,\n emailError: false,\n emailHint: '',\n usernameValidationInProgress: false,\n usernameError: false,\n usernameHint: '',\n awaitingResponse: false,\n badLoginAttempt: false,\n userSecret: '',\n secret: 'goons',\n user: null as UserDBInterface | null,\n roles: ['Student', 'Teacher', 'Author'] as string[],\n student: true,\n teacher: false,\n author: false,\n authStore: useAuthStore(),\n };\n },\n\n computed: {\n registrationRoute(): boolean {\n return typeof this.$route.name === 'string' && this.$route.name.toLowerCase() === 'signup';\n },\n buttonStatus() {\n return {\n color: this.badLoginAttempt ? 'error' : 'success',\n text: this.badLoginAttempt ? 'Try again' : 'Log In',\n };\n },\n passwordError(): string {\n return validatePassword(this.password);\n },\n passwordRetypeError(): string {\n console.log('[RTE]');\n if (this.password !== this.retypedPassword) {\n return 'Passwords must match.';\n } else {\n return '';\n }\n },\n },\n\n async created() {\n this.user = await getCurrentUser();\n },\n\n methods: {\n toggle() {\n log('Toggling registration / login forms.');\n this.$emit('toggle');\n },\n\n validateEmail() {\n this.emailError = false;\n // Basic email validation\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (this.email && !emailRegex.test(this.email)) {\n this.emailError = true;\n this.emailHint = 'Please enter a valid email address';\n } else {\n this.emailHint = '';\n }\n },\n\n validateUsername() {\n this.usernameError = false;\n },\n\n async createUser() {\n this.awaitingResponse = true;\n\n // Validate password before proceeding\n if (this.passwordError) {\n alertUser({\n text: this.passwordError,\n status: Status.error,\n });\n this.awaitingResponse = false;\n return;\n }\n\n log(`\nUser creation\n-------------\n\nName: ${this.username}\nStudent: ${this.student}\nTeacher: ${this.teacher}\nAuthor: ${this.author}\n`);\n if (this.password === this.retypedPassword) {\n if (!this.user) {\n console.error('ERROR: No user object available');\n return;\n }\n\n this.user\n .createAccount(this.username, this.password)\n .then(async (resp: any) => {\n if (resp.status === Status.ok) {\n // Account created successfully via PouchDB\n this.authStore.loginAndRegistration.loggedIn = true;\n this.authStore.loginAndRegistration.init = false;\n this.authStore.loginAndRegistration.init = true;\n\n // Save email to user config if provided\n if (this.email) {\n try {\n const currentUser = await getCurrentUser();\n await currentUser.setConfig({ email: this.email });\n\n // Trigger verification email send with current origin\n const origin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const verificationResult = await sendVerificationEmail(this.username, this.email, origin);\n if (verificationResult.ok) {\n alertUser({\n text: 'Account created! Please check your email to verify your account.',\n status: Status.ok,\n });\n } else {\n log(`Warning: Failed to send verification email: ${verificationResult.error}`);\n // Continue anyway - user can still use the account\n }\n } catch (emailError) {\n console.error('Email save/send error:', emailError);\n log(`Warning: Failed to save email or send verification: ${emailError}`);\n // Continue anyway - account was created successfully\n }\n }\n\n // Emit signup success event for parent to handle\n // Use the username we just created rather than calling getCurrentUser() again\n // which can fail if the auth state hasn't fully synchronized\n\n // Call callback prop if provided (preferred method)\n if (this.onSignupSuccess) {\n console.log('[UserRegistration] Calling onSignupSuccess callback');\n this.onSignupSuccess({ username: this.username });\n }\n\n // Also emit for backward compatibility\n this.$emit('signup-success', { username: this.username });\n } else {\n if (resp.error === 'This username is taken!') {\n this.usernameError = true;\n this.usernameHint = 'Try a different name.';\n (this.$refs.userNameTextField as HTMLInputElement).focus();\n alertUser({\n text: `The name ${this.username} is taken!`,\n status: resp.status,\n });\n } else {\n alertUser({\n text: resp.error,\n status: resp.status,\n });\n }\n }\n })\n .catch((e) => {\n console.error('[UserRegistration] .catch() called with error:', e);\n if (e) {\n const errorText = e?.message || e?.error || e?.toString() || 'Account creation failed';\n alertUser({\n text: errorText,\n status: Status.error,\n });\n }\n });\n this.awaitingResponse = false;\n } else {\n alertUser({\n text: 'Passwords do not match.',\n status: Status.error,\n });\n this.awaitingResponse = false;\n }\n },\n },\n});\n</script>\n","<template>\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\"> Create an Account </v-card-title>\n\n <v-card-text>\n <v-form @submit.prevent=\"createUser\">\n <v-text-field\n v-model=\"email\"\n name=\"email\"\n label=\"Email address\"\n type=\"email\"\n prepend-icon=\"mdi-email\"\n :error=\"emailError\"\n :hint=\"emailHint\"\n @blur=\"validateEmail\"\n ></v-text-field>\n <v-text-field\n id=\"\"\n ref=\"userNameTextField\"\n v-model=\"username\"\n name=\"username\"\n label=\"Choose a Username\"\n prepend-icon=\"mdi-account-circle\"\n :error=\"usernameError\"\n :hint=\"usernameHint\"\n @blur=\"validateUsername\"\n ></v-text-field>\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n hover=\"Show password\"\n label=\"Create a password\"\n :hint=\"passwordError\"\n :error=\"!!passwordError\"\n min=\"4\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n <v-text-field\n v-model=\"retypedPassword\"\n prepend-icon=\"mdi-lock\"\n name=\"retypedPassword\"\n hover=\"Show password\"\n label=\"Retype your password\"\n :disabled=\"password === '' || !!passwordError\"\n :hint=\"passwordRetypeError\"\n min=\"4\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n ></v-text-field>\n\n <!-- <v-checkbox label=\"Student\" v-model=\"student\" ></v-checkbox>\n <v-checkbox label=\"Teacher\" v-model=\"teacher\" ></v-checkbox>\n <v-checkbox label=\"Author\" v-model=\"author\" ></v-checkbox> -->\n\n <v-snackbar v-model=\"badLoginAttempt\" location=\"bottom right\" :timeout=\"5000\">\n Username or password was incorrect.\n <v-btn color=\"pink\" variant=\"text\" @click=\"badLoginAttempt = false\"> Close </v-btn>\n </v-snackbar>\n <v-btn\n class=\"mr-2 my-2\"\n type=\"submit\"\n :loading=\"awaitingResponse\"\n :color=\"buttonStatus.color\"\n :disabled=\"!!passwordError || password !== retypedPassword\"\n >\n <v-icon start>mdi-lock-open</v-icon>\n Create Account\n </v-btn>\n <router-link v-if=\"registrationRoute\" to=\"login\">\n <v-btn variant=\"text\">Log In</v-btn>\n </router-link>\n <v-btn v-else variant=\"text\" @click=\"toggle\"> Log In </v-btn>\n </v-form>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { UserDBInterface } from '@vue-skuilder/db';\nimport { alertUser } from '../SnackbarService';\nimport { Status, log } from '@vue-skuilder/common';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { sendVerificationEmail } from '../../services/authAPI';\nimport { validatePassword } from '../../utils/passwordValidation';\n\nexport default defineComponent({\n name: 'UserRegistration',\n\n props: {\n onSignupSuccess: {\n type: Function as PropType<(payload: { username: string }) => void>,\n required: false,\n },\n },\n\n emits: ['toggle', 'signup-success'],\n\n data() {\n return {\n email: '',\n username: '',\n password: '',\n retypedPassword: '',\n passwordVisible: false,\n emailError: false,\n emailHint: '',\n usernameValidationInProgress: false,\n usernameError: false,\n usernameHint: '',\n awaitingResponse: false,\n badLoginAttempt: false,\n userSecret: '',\n secret: 'goons',\n user: null as UserDBInterface | null,\n roles: ['Student', 'Teacher', 'Author'] as string[],\n student: true,\n teacher: false,\n author: false,\n authStore: useAuthStore(),\n };\n },\n\n computed: {\n registrationRoute(): boolean {\n return typeof this.$route.name === 'string' && this.$route.name.toLowerCase() === 'signup';\n },\n buttonStatus() {\n return {\n color: this.badLoginAttempt ? 'error' : 'success',\n text: this.badLoginAttempt ? 'Try again' : 'Log In',\n };\n },\n passwordError(): string {\n return validatePassword(this.password);\n },\n passwordRetypeError(): string {\n console.log('[RTE]');\n if (this.password !== this.retypedPassword) {\n return 'Passwords must match.';\n } else {\n return '';\n }\n },\n },\n\n async created() {\n this.user = await getCurrentUser();\n },\n\n methods: {\n toggle() {\n log('Toggling registration / login forms.');\n this.$emit('toggle');\n },\n\n validateEmail() {\n this.emailError = false;\n // Basic email validation\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (this.email && !emailRegex.test(this.email)) {\n this.emailError = true;\n this.emailHint = 'Please enter a valid email address';\n } else {\n this.emailHint = '';\n }\n },\n\n validateUsername() {\n this.usernameError = false;\n },\n\n async createUser() {\n this.awaitingResponse = true;\n\n // Validate password before proceeding\n if (this.passwordError) {\n alertUser({\n text: this.passwordError,\n status: Status.error,\n });\n this.awaitingResponse = false;\n return;\n }\n\n log(`\nUser creation\n-------------\n\nName: ${this.username}\nStudent: ${this.student}\nTeacher: ${this.teacher}\nAuthor: ${this.author}\n`);\n if (this.password === this.retypedPassword) {\n if (!this.user) {\n console.error('ERROR: No user object available');\n return;\n }\n\n this.user\n .createAccount(this.username, this.password)\n .then(async (resp: any) => {\n if (resp.status === Status.ok) {\n // Account created successfully via PouchDB\n this.authStore.loginAndRegistration.loggedIn = true;\n this.authStore.loginAndRegistration.init = false;\n this.authStore.loginAndRegistration.init = true;\n\n // Save email to user config if provided\n if (this.email) {\n try {\n const currentUser = await getCurrentUser();\n await currentUser.setConfig({ email: this.email });\n\n // Trigger verification email send with current origin\n const origin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const verificationResult = await sendVerificationEmail(this.username, this.email, origin);\n if (verificationResult.ok) {\n alertUser({\n text: 'Account created! Please check your email to verify your account.',\n status: Status.ok,\n });\n } else {\n log(`Warning: Failed to send verification email: ${verificationResult.error}`);\n // Continue anyway - user can still use the account\n }\n } catch (emailError) {\n console.error('Email save/send error:', emailError);\n log(`Warning: Failed to save email or send verification: ${emailError}`);\n // Continue anyway - account was created successfully\n }\n }\n\n // Emit signup success event for parent to handle\n // Use the username we just created rather than calling getCurrentUser() again\n // which can fail if the auth state hasn't fully synchronized\n\n // Call callback prop if provided (preferred method)\n if (this.onSignupSuccess) {\n console.log('[UserRegistration] Calling onSignupSuccess callback');\n this.onSignupSuccess({ username: this.username });\n }\n\n // Also emit for backward compatibility\n this.$emit('signup-success', { username: this.username });\n } else {\n if (resp.error === 'This username is taken!') {\n this.usernameError = true;\n this.usernameHint = 'Try a different name.';\n (this.$refs.userNameTextField as HTMLInputElement).focus();\n alertUser({\n text: `The name ${this.username} is taken!`,\n status: resp.status,\n });\n } else {\n alertUser({\n text: resp.error,\n status: resp.status,\n });\n }\n }\n })\n .catch((e) => {\n console.error('[UserRegistration] .catch() called with error:', e);\n if (e) {\n const errorText = e?.message || e?.error || e?.toString() || 'Account creation failed';\n alertUser({\n text: errorText,\n status: Status.error,\n });\n }\n });\n this.awaitingResponse = false;\n } else {\n alertUser({\n text: 'Passwords do not match.',\n status: Status.error,\n });\n this.awaitingResponse = false;\n }\n },\n },\n});\n</script>\n","<template>\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Reset Password\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <v-form v-if=\"!requestSent\" @submit.prevent=\"handleSubmit\">\n <p class=\"mb-4\">\n Enter your email address and we'll send you a link to reset your password.\n </p>\n\n <v-text-field\n v-model=\"email\"\n name=\"email\"\n label=\"Email address\"\n type=\"email\"\n prepend-icon=\"mdi-email\"\n :error=\"emailError\"\n :hint=\"emailHint\"\n :disabled=\"isSubmitting\"\n @blur=\"validateEmail\"\n ></v-text-field>\n\n <v-btn\n type=\"submit\"\n color=\"primary\"\n class=\"mt-4\"\n :loading=\"isSubmitting\"\n :disabled=\"!email || emailError\"\n block\n >\n <v-icon start>mdi-email-send</v-icon>\n Send Reset Link\n </v-btn>\n </v-form>\n\n <!-- Success message -->\n <div v-else class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-email-check</v-icon>\n <h3 class=\"mb-2\">Check Your Email</h3>\n <p>\n If an account exists with <strong>{{ email }}</strong>, you will receive a password\n reset link shortly.\n </p>\n <p class=\"text-caption mt-4\">Didn't receive an email? Check your spam folder.</p>\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"!requestSent\">\n <slot name=\"back-action\">\n <!-- External apps can provide their own back button -->\n <v-btn variant=\"text\" @click=\"$emit('cancel')\">\n Cancel\n </v-btn>\n </slot>\n <v-spacer></v-spacer>\n </v-card-actions>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { requestPasswordReset } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'RequestPasswordReset',\n\n emits: ['cancel', 'success'],\n\n data() {\n return {\n email: '',\n emailError: false,\n emailHint: '',\n isSubmitting: false,\n requestSent: false,\n };\n },\n\n methods: {\n validateEmail() {\n this.emailError = false;\n this.emailHint = '';\n\n if (!this.email) return;\n\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(this.email)) {\n this.emailError = true;\n this.emailHint = 'Please enter a valid email address';\n }\n },\n\n async handleSubmit() {\n this.validateEmail();\n\n if (this.emailError || !this.email) {\n return;\n }\n\n this.isSubmitting = true;\n\n try {\n // Pass current origin for correct reset link in email\n const origin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const result = await requestPasswordReset(this.email, origin);\n\n if (result.ok) {\n this.requestSent = true;\n this.$emit('success', this.email);\n } else {\n alertUser({\n text: result.error || 'Failed to send reset email',\n status: Status.error,\n });\n }\n } catch (error) {\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n } finally {\n this.isSubmitting = false;\n }\n },\n },\n});\n</script>\n","<template>\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Reset Password\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <v-form v-if=\"!requestSent\" @submit.prevent=\"handleSubmit\">\n <p class=\"mb-4\">\n Enter your email address and we'll send you a link to reset your password.\n </p>\n\n <v-text-field\n v-model=\"email\"\n name=\"email\"\n label=\"Email address\"\n type=\"email\"\n prepend-icon=\"mdi-email\"\n :error=\"emailError\"\n :hint=\"emailHint\"\n :disabled=\"isSubmitting\"\n @blur=\"validateEmail\"\n ></v-text-field>\n\n <v-btn\n type=\"submit\"\n color=\"primary\"\n class=\"mt-4\"\n :loading=\"isSubmitting\"\n :disabled=\"!email || emailError\"\n block\n >\n <v-icon start>mdi-email-send</v-icon>\n Send Reset Link\n </v-btn>\n </v-form>\n\n <!-- Success message -->\n <div v-else class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-email-check</v-icon>\n <h3 class=\"mb-2\">Check Your Email</h3>\n <p>\n If an account exists with <strong>{{ email }}</strong>, you will receive a password\n reset link shortly.\n </p>\n <p class=\"text-caption mt-4\">Didn't receive an email? Check your spam folder.</p>\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"!requestSent\">\n <slot name=\"back-action\">\n <!-- External apps can provide their own back button -->\n <v-btn variant=\"text\" @click=\"$emit('cancel')\">\n Cancel\n </v-btn>\n </slot>\n <v-spacer></v-spacer>\n </v-card-actions>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { requestPasswordReset } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'RequestPasswordReset',\n\n emits: ['cancel', 'success'],\n\n data() {\n return {\n email: '',\n emailError: false,\n emailHint: '',\n isSubmitting: false,\n requestSent: false,\n };\n },\n\n methods: {\n validateEmail() {\n this.emailError = false;\n this.emailHint = '';\n\n if (!this.email) return;\n\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(this.email)) {\n this.emailError = true;\n this.emailHint = 'Please enter a valid email address';\n }\n },\n\n async handleSubmit() {\n this.validateEmail();\n\n if (this.emailError || !this.email) {\n return;\n }\n\n this.isSubmitting = true;\n\n try {\n // Pass current origin for correct reset link in email\n const origin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const result = await requestPasswordReset(this.email, origin);\n\n if (result.ok) {\n this.requestSent = true;\n this.$emit('success', this.email);\n } else {\n alertUser({\n text: result.error || 'Failed to send reset email',\n status: Status.error,\n });\n }\n } catch (error) {\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n } finally {\n this.isSubmitting = false;\n }\n },\n },\n});\n</script>\n","<template>\n <div v-if=\"userReady && display\">\n <transition name=\"component-fade\" mode=\"out-in\">\n <div v-if=\"guestMode\" key=\"login-buttons\">\n <!-- Registration button: only if showLoginRegistration is true -->\n <v-dialog v-model=\"regDialog\" width=\"500px\" v-if=\"authUIConfig.showLoginRegistration\">\n <template #activator=\"{ props }\">\n <v-btn class=\"mr-2\" size=\"small\" color=\"success\" v-bind=\"props\">Sign Up</v-btn>\n </template>\n <UserRegistration\n @toggle=\"toggle\"\n @signup-success=\"handleSignupSuccess\"\n :on-signup-success=\"handleSignupSuccess\"\n />\n </v-dialog>\n \n <!-- Login button: always show in guest mode -->\n <v-dialog v-model=\"loginDialog\" width=\"500px\">\n <template #activator=\"{ props }\">\n <v-btn size=\"small\" color=\"success\" v-bind=\"props\">Log In</v-btn>\n </template>\n <UserLogin @toggle=\"toggle\" @forgot-password=\"openResetDialog\" />\n </v-dialog>\n\n <!-- Password reset dialog: opened from login via \"Forgot password?\" -->\n <v-dialog v-model=\"resetDialog\" width=\"500px\">\n <RequestPasswordReset @cancel=\"closeResetDialog\" @success=\"closeResetDialog\" />\n </v-dialog>\n </div>\n <div v-else key=\"user-chip\">\n <user-chip />\n </div>\n </transition>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, onMounted, ref } from 'vue';\nimport { useRoute } from 'vue-router';\nimport UserLogin from './UserLogin.vue';\nimport UserRegistration from './UserRegistration.vue';\nimport RequestPasswordReset from './RequestPasswordReset.vue';\nimport UserChip from './UserChip.vue';\nimport { useAuthStore } from '../../stores/useAuthStore';\nimport { useAuthUI } from '../../composables/useAuthUI';\nimport { GuestUsername } from '@vue-skuilder/db';\n\n// Define props\nconst props = defineProps<{\n showLoginButton?: boolean;\n redirectToPath?: string;\n showRegistration?: boolean;\n onSignupSuccess?: (payload: { username: string }) => void;\n}>();\n\n// Define emits (keeping for backward compatibility if needed)\nconst emit = defineEmits<{\n 'signup-success': [payload: { username: string }]\n}>();\n\nconst route = useRoute();\nconst authStore = useAuthStore();\nconst authUI = useAuthUI();\n\n// Initialize auth UI detection\nonMounted(async () => {\n await authUI.detectSyncStrategy();\n});\n\nconst display = computed(() => {\n if (!route.name || typeof route.name !== 'string') {\n return true;\n }\n const routeName = route.name.toLowerCase();\n return !(routeName === 'login' || routeName === 'signup');\n});\n\nconst userReady = computed(() => authStore.onLoadComplete);\n\nconst guestMode = computed(() => {\n if (authStore._user) {\n return authStore._user.getUsername().startsWith(GuestUsername);\n }\n return !authStore.loginAndRegistration.loggedIn;\n});\n\nconst authUIConfig = computed(() => {\n const baseConfig = authUI.config.value || {\n showLoginRegistration: true,\n showLogout: true, \n showResetData: false,\n logoutLabel: 'Log out',\n resetLabel: '',\n };\n \n // Apply prop overrides\n return {\n ...baseConfig,\n ...(props.showRegistration !== undefined && { showLoginRegistration: props.showRegistration })\n };\n});\n\nconst regDialog = computed({\n get: () => authStore.loginAndRegistration.regDialogOpen,\n set: (value: boolean) => {\n authStore.loginAndRegistration.regDialogOpen = value;\n },\n});\n\nconst loginDialog = computed({\n get: () => authStore.loginAndRegistration.loginDialogOpen,\n set: (value: boolean) => {\n authStore.loginAndRegistration.loginDialogOpen = value;\n },\n});\n\n// Password reset dialog state (local, not in auth store)\nconst resetDialog = ref(false);\n\nconst toggle = () => {\n if (regDialog.value && loginDialog.value) {\n throw new Error('Registration / Login dialogs both activated.');\n } else if (regDialog.value === loginDialog.value) {\n throw new Error('Registration / Login dialogs toggled while both were dormant.');\n } else {\n regDialog.value = !regDialog.value;\n loginDialog.value = !loginDialog.value;\n }\n};\n\nconst openResetDialog = () => {\n loginDialog.value = false; // Close login dialog\n resetDialog.value = true; // Open reset dialog\n};\n\nconst closeResetDialog = () => {\n resetDialog.value = false;\n};\n\nconst handleSignupSuccess = (payload: { username: string }) => {\n // Close the registration dialog\n regDialog.value = false;\n\n // Call the callback prop if provided (preferred method)\n if (props.onSignupSuccess) {\n props.onSignupSuccess(payload);\n }\n\n // Also emit event for backward compatibility\n emit('signup-success', payload);\n};\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n</style>\n","<template>\n <div v-if=\"userReady && display\">\n <transition name=\"component-fade\" mode=\"out-in\">\n <div v-if=\"guestMode\" key=\"login-buttons\">\n <!-- Registration button: only if showLoginRegistration is true -->\n <v-dialog v-model=\"regDialog\" width=\"500px\" v-if=\"authUIConfig.showLoginRegistration\">\n <template #activator=\"{ props }\">\n <v-btn class=\"mr-2\" size=\"small\" color=\"success\" v-bind=\"props\">Sign Up</v-btn>\n </template>\n <UserRegistration\n @toggle=\"toggle\"\n @signup-success=\"handleSignupSuccess\"\n :on-signup-success=\"handleSignupSuccess\"\n />\n </v-dialog>\n \n <!-- Login button: always show in guest mode -->\n <v-dialog v-model=\"loginDialog\" width=\"500px\">\n <template #activator=\"{ props }\">\n <v-btn size=\"small\" color=\"success\" v-bind=\"props\">Log In</v-btn>\n </template>\n <UserLogin @toggle=\"toggle\" @forgot-password=\"openResetDialog\" />\n </v-dialog>\n\n <!-- Password reset dialog: opened from login via \"Forgot password?\" -->\n <v-dialog v-model=\"resetDialog\" width=\"500px\">\n <RequestPasswordReset @cancel=\"closeResetDialog\" @success=\"closeResetDialog\" />\n </v-dialog>\n </div>\n <div v-else key=\"user-chip\">\n <user-chip />\n </div>\n </transition>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, onMounted, ref } from 'vue';\nimport { useRoute } from 'vue-router';\nimport UserLogin from './UserLogin.vue';\nimport UserRegistration from './UserRegistration.vue';\nimport RequestPasswordReset from './RequestPasswordReset.vue';\nimport UserChip from './UserChip.vue';\nimport { useAuthStore } from '../../stores/useAuthStore';\nimport { useAuthUI } from '../../composables/useAuthUI';\nimport { GuestUsername } from '@vue-skuilder/db';\n\n// Define props\nconst props = defineProps<{\n showLoginButton?: boolean;\n redirectToPath?: string;\n showRegistration?: boolean;\n onSignupSuccess?: (payload: { username: string }) => void;\n}>();\n\n// Define emits (keeping for backward compatibility if needed)\nconst emit = defineEmits<{\n 'signup-success': [payload: { username: string }]\n}>();\n\nconst route = useRoute();\nconst authStore = useAuthStore();\nconst authUI = useAuthUI();\n\n// Initialize auth UI detection\nonMounted(async () => {\n await authUI.detectSyncStrategy();\n});\n\nconst display = computed(() => {\n if (!route.name || typeof route.name !== 'string') {\n return true;\n }\n const routeName = route.name.toLowerCase();\n return !(routeName === 'login' || routeName === 'signup');\n});\n\nconst userReady = computed(() => authStore.onLoadComplete);\n\nconst guestMode = computed(() => {\n if (authStore._user) {\n return authStore._user.getUsername().startsWith(GuestUsername);\n }\n return !authStore.loginAndRegistration.loggedIn;\n});\n\nconst authUIConfig = computed(() => {\n const baseConfig = authUI.config.value || {\n showLoginRegistration: true,\n showLogout: true, \n showResetData: false,\n logoutLabel: 'Log out',\n resetLabel: '',\n };\n \n // Apply prop overrides\n return {\n ...baseConfig,\n ...(props.showRegistration !== undefined && { showLoginRegistration: props.showRegistration })\n };\n});\n\nconst regDialog = computed({\n get: () => authStore.loginAndRegistration.regDialogOpen,\n set: (value: boolean) => {\n authStore.loginAndRegistration.regDialogOpen = value;\n },\n});\n\nconst loginDialog = computed({\n get: () => authStore.loginAndRegistration.loginDialogOpen,\n set: (value: boolean) => {\n authStore.loginAndRegistration.loginDialogOpen = value;\n },\n});\n\n// Password reset dialog state (local, not in auth store)\nconst resetDialog = ref(false);\n\nconst toggle = () => {\n if (regDialog.value && loginDialog.value) {\n throw new Error('Registration / Login dialogs both activated.');\n } else if (regDialog.value === loginDialog.value) {\n throw new Error('Registration / Login dialogs toggled while both were dormant.');\n } else {\n regDialog.value = !regDialog.value;\n loginDialog.value = !loginDialog.value;\n }\n};\n\nconst openResetDialog = () => {\n loginDialog.value = false; // Close login dialog\n resetDialog.value = true; // Open reset dialog\n};\n\nconst closeResetDialog = () => {\n resetDialog.value = false;\n};\n\nconst handleSignupSuccess = (payload: { username: string }) => {\n // Close the registration dialog\n regDialog.value = false;\n\n // Call the callback prop if provided (preferred method)\n if (props.onSignupSuccess) {\n props.onSignupSuccess(payload);\n }\n\n // Also emit event for backward compatibility\n emit('signup-success', payload);\n};\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n</style>\n","<template>\n <v-container class=\"fill-height\" fluid>\n <v-row align=\"center\" justify=\"center\">\n <v-col cols=\"12\" sm=\"8\" md=\"6\">\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Email Verification\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <!-- Loading state -->\n <div v-if=\"isVerifying\" class=\"text-center\">\n <v-progress-circular\n indeterminate\n color=\"primary\"\n size=\"64\"\n class=\"mb-4\"\n ></v-progress-circular>\n <p>Verifying your email...</p>\n </div>\n\n <!-- Success state -->\n <div v-else-if=\"verificationStatus === 'success'\" class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-check-circle</v-icon>\n <h3 class=\"mb-2\">Email Verified!</h3>\n <p>Your account has been successfully verified.</p>\n <p v-if=\"username\">Welcome, {{ username }}!</p>\n </div>\n\n <!-- Error state -->\n <div v-else-if=\"verificationStatus === 'error'\" class=\"text-center\">\n <v-icon color=\"error\" size=\"64\" class=\"mb-4\">mdi-alert-circle</v-icon>\n <h3 class=\"mb-2\">Verification Failed</h3>\n <p>{{ errorMessage }}</p>\n </div>\n\n <!-- No token state -->\n <div v-else class=\"text-center\">\n <v-icon color=\"warning\" size=\"64\" class=\"mb-4\">mdi-help-circle</v-icon>\n <h3 class=\"mb-2\">No Verification Token</h3>\n <p>Please use the link from your verification email.</p>\n </div>\n </v-card-text>\n\n <v-card-actions>\n <v-spacer></v-spacer>\n <slot name=\"actions\" :status=\"verificationStatus\" :username=\"username\">\n <!-- Default action - external apps can override with their own navigation -->\n <v-btn\n v-if=\"verificationStatus === 'success'\"\n color=\"primary\"\n @click=\"$emit('verified', username)\"\n >\n Continue\n </v-btn>\n <v-btn\n v-else-if=\"verificationStatus === 'error'\"\n variant=\"text\"\n @click=\"$emit('error', errorMessage)\"\n >\n Close\n </v-btn>\n </slot>\n </v-card-actions>\n </v-card>\n </v-col>\n </v-row>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { verifyEmail } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'VerifyEmail',\n\n emits: ['verified', 'error'],\n\n props: {\n /**\n * Verification token. If not provided, will try to read from URL query params.\n */\n token: {\n type: String,\n default: null,\n },\n },\n\n data() {\n return {\n isVerifying: false,\n verificationStatus: null as 'success' | 'error' | null,\n errorMessage: '',\n username: '',\n };\n },\n\n async mounted() {\n await this.performVerification();\n },\n\n methods: {\n async performVerification() {\n // Get token from prop or URL query params\n const token = this.token || this.getTokenFromURL();\n\n if (!token) {\n this.verificationStatus = 'error';\n this.errorMessage = 'No verification token provided';\n return;\n }\n\n this.isVerifying = true;\n\n try {\n const result = await verifyEmail(token);\n\n if (result.ok) {\n this.verificationStatus = 'success';\n this.username = result.username || '';\n alertUser({\n text: 'Email verified successfully!',\n status: Status.ok,\n });\n } else {\n this.verificationStatus = 'error';\n this.errorMessage = result.error || 'Verification failed';\n alertUser({\n text: result.error || 'Verification failed',\n status: Status.error,\n });\n }\n } catch (error) {\n this.verificationStatus = 'error';\n this.errorMessage = 'An unexpected error occurred';\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n } finally {\n this.isVerifying = false;\n }\n },\n\n getTokenFromURL(): string | null {\n if (typeof window === 'undefined') return null;\n\n const params = new URLSearchParams(window.location.search);\n return params.get('token');\n },\n },\n});\n</script>\n","<template>\n <v-container class=\"fill-height\" fluid>\n <v-row align=\"center\" justify=\"center\">\n <v-col cols=\"12\" sm=\"8\" md=\"6\">\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Email Verification\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <!-- Loading state -->\n <div v-if=\"isVerifying\" class=\"text-center\">\n <v-progress-circular\n indeterminate\n color=\"primary\"\n size=\"64\"\n class=\"mb-4\"\n ></v-progress-circular>\n <p>Verifying your email...</p>\n </div>\n\n <!-- Success state -->\n <div v-else-if=\"verificationStatus === 'success'\" class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-check-circle</v-icon>\n <h3 class=\"mb-2\">Email Verified!</h3>\n <p>Your account has been successfully verified.</p>\n <p v-if=\"username\">Welcome, {{ username }}!</p>\n </div>\n\n <!-- Error state -->\n <div v-else-if=\"verificationStatus === 'error'\" class=\"text-center\">\n <v-icon color=\"error\" size=\"64\" class=\"mb-4\">mdi-alert-circle</v-icon>\n <h3 class=\"mb-2\">Verification Failed</h3>\n <p>{{ errorMessage }}</p>\n </div>\n\n <!-- No token state -->\n <div v-else class=\"text-center\">\n <v-icon color=\"warning\" size=\"64\" class=\"mb-4\">mdi-help-circle</v-icon>\n <h3 class=\"mb-2\">No Verification Token</h3>\n <p>Please use the link from your verification email.</p>\n </div>\n </v-card-text>\n\n <v-card-actions>\n <v-spacer></v-spacer>\n <slot name=\"actions\" :status=\"verificationStatus\" :username=\"username\">\n <!-- Default action - external apps can override with their own navigation -->\n <v-btn\n v-if=\"verificationStatus === 'success'\"\n color=\"primary\"\n @click=\"$emit('verified', username)\"\n >\n Continue\n </v-btn>\n <v-btn\n v-else-if=\"verificationStatus === 'error'\"\n variant=\"text\"\n @click=\"$emit('error', errorMessage)\"\n >\n Close\n </v-btn>\n </slot>\n </v-card-actions>\n </v-card>\n </v-col>\n </v-row>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { verifyEmail } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'VerifyEmail',\n\n emits: ['verified', 'error'],\n\n props: {\n /**\n * Verification token. If not provided, will try to read from URL query params.\n */\n token: {\n type: String,\n default: null,\n },\n },\n\n data() {\n return {\n isVerifying: false,\n verificationStatus: null as 'success' | 'error' | null,\n errorMessage: '',\n username: '',\n };\n },\n\n async mounted() {\n await this.performVerification();\n },\n\n methods: {\n async performVerification() {\n // Get token from prop or URL query params\n const token = this.token || this.getTokenFromURL();\n\n if (!token) {\n this.verificationStatus = 'error';\n this.errorMessage = 'No verification token provided';\n return;\n }\n\n this.isVerifying = true;\n\n try {\n const result = await verifyEmail(token);\n\n if (result.ok) {\n this.verificationStatus = 'success';\n this.username = result.username || '';\n alertUser({\n text: 'Email verified successfully!',\n status: Status.ok,\n });\n } else {\n this.verificationStatus = 'error';\n this.errorMessage = result.error || 'Verification failed';\n alertUser({\n text: result.error || 'Verification failed',\n status: Status.error,\n });\n }\n } catch (error) {\n this.verificationStatus = 'error';\n this.errorMessage = 'An unexpected error occurred';\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n } finally {\n this.isVerifying = false;\n }\n },\n\n getTokenFromURL(): string | null {\n if (typeof window === 'undefined') return null;\n\n const params = new URLSearchParams(window.location.search);\n return params.get('token');\n },\n },\n});\n</script>\n","<template>\n <v-container class=\"fill-height\" fluid>\n <v-row align=\"center\" justify=\"center\">\n <v-col cols=\"12\" sm=\"8\" md=\"6\">\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Set New Password\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <v-form v-if=\"!isComplete\" @submit.prevent=\"handleSubmit\">\n <p class=\"mb-4\">Enter your new password below.</p>\n\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n label=\"New password\"\n min=\"4\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n :disabled=\"isSubmitting\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n\n <v-text-field\n v-model=\"confirmPassword\"\n prepend-icon=\"mdi-lock\"\n name=\"confirmPassword\"\n label=\"Confirm new password\"\n min=\"4\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n :error=\"confirmPasswordError\"\n :hint=\"confirmPasswordHint\"\n :disabled=\"isSubmitting\"\n @blur=\"validateConfirmPassword\"\n ></v-text-field>\n\n <v-btn\n type=\"submit\"\n color=\"primary\"\n class=\"mt-4\"\n :loading=\"isSubmitting\"\n :disabled=\"!canSubmit\"\n block\n >\n <v-icon start>mdi-lock-reset</v-icon>\n Reset Password\n </v-btn>\n </v-form>\n\n <!-- Success state -->\n <div v-else class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-check-circle</v-icon>\n <h3 class=\"mb-2\">Password Reset Successful!</h3>\n <p>Your password has been updated. You can now log in with your new password.</p>\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"isComplete\">\n <v-spacer></v-spacer>\n <slot name=\"success-action\">\n <!-- External apps can provide their own navigation -->\n <v-btn color=\"primary\" @click=\"$emit('complete')\">\n Continue to Login\n </v-btn>\n </slot>\n </v-card-actions>\n </v-card>\n </v-col>\n </v-row>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { resetPassword } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'ResetPassword',\n\n emits: ['complete', 'error'],\n\n props: {\n /**\n * Reset token. If not provided, will try to read from URL query params.\n */\n token: {\n type: String,\n default: null,\n },\n },\n\n data() {\n return {\n password: '',\n confirmPassword: '',\n passwordVisible: false,\n confirmPasswordError: false,\n confirmPasswordHint: '',\n isSubmitting: false,\n isComplete: false,\n };\n },\n\n computed: {\n canSubmit(): boolean {\n return (\n this.password.length >= 4 &&\n this.confirmPassword.length >= 4 &&\n this.password === this.confirmPassword &&\n !this.confirmPasswordError\n );\n },\n },\n\n methods: {\n validateConfirmPassword() {\n this.confirmPasswordError = false;\n this.confirmPasswordHint = '';\n\n if (!this.confirmPassword) return;\n\n if (this.password !== this.confirmPassword) {\n this.confirmPasswordError = true;\n this.confirmPasswordHint = 'Passwords do not match';\n }\n },\n\n async handleSubmit() {\n this.validateConfirmPassword();\n\n if (!this.canSubmit) {\n return;\n }\n\n // Get token from prop or URL query params\n const token = this.token || this.getTokenFromURL();\n\n if (!token) {\n alertUser({\n text: 'No reset token provided. Please use the link from your email.',\n status: Status.error,\n });\n this.$emit('error', 'No token');\n return;\n }\n\n this.isSubmitting = true;\n\n try {\n const result = await resetPassword(token, this.password);\n\n if (result.ok) {\n this.isComplete = true;\n alertUser({\n text: 'Password reset successfully!',\n status: Status.ok,\n });\n } else {\n alertUser({\n text: result.error || 'Failed to reset password',\n status: Status.error,\n });\n this.$emit('error', result.error);\n }\n } catch (error) {\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n this.$emit('error', 'Unexpected error');\n } finally {\n this.isSubmitting = false;\n }\n },\n\n getTokenFromURL(): string | null {\n if (typeof window === 'undefined') return null;\n\n const params = new URLSearchParams(window.location.search);\n return params.get('token');\n },\n },\n});\n</script>\n","<template>\n <v-container class=\"fill-height\" fluid>\n <v-row align=\"center\" justify=\"center\">\n <v-col cols=\"12\" sm=\"8\" md=\"6\">\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Set New Password\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <v-form v-if=\"!isComplete\" @submit.prevent=\"handleSubmit\">\n <p class=\"mb-4\">Enter your new password below.</p>\n\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n label=\"New password\"\n min=\"4\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n :disabled=\"isSubmitting\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n\n <v-text-field\n v-model=\"confirmPassword\"\n prepend-icon=\"mdi-lock\"\n name=\"confirmPassword\"\n label=\"Confirm new password\"\n min=\"4\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n :error=\"confirmPasswordError\"\n :hint=\"confirmPasswordHint\"\n :disabled=\"isSubmitting\"\n @blur=\"validateConfirmPassword\"\n ></v-text-field>\n\n <v-btn\n type=\"submit\"\n color=\"primary\"\n class=\"mt-4\"\n :loading=\"isSubmitting\"\n :disabled=\"!canSubmit\"\n block\n >\n <v-icon start>mdi-lock-reset</v-icon>\n Reset Password\n </v-btn>\n </v-form>\n\n <!-- Success state -->\n <div v-else class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-check-circle</v-icon>\n <h3 class=\"mb-2\">Password Reset Successful!</h3>\n <p>Your password has been updated. You can now log in with your new password.</p>\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"isComplete\">\n <v-spacer></v-spacer>\n <slot name=\"success-action\">\n <!-- External apps can provide their own navigation -->\n <v-btn color=\"primary\" @click=\"$emit('complete')\">\n Continue to Login\n </v-btn>\n </slot>\n </v-card-actions>\n </v-card>\n </v-col>\n </v-row>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { resetPassword } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'ResetPassword',\n\n emits: ['complete', 'error'],\n\n props: {\n /**\n * Reset token. If not provided, will try to read from URL query params.\n */\n token: {\n type: String,\n default: null,\n },\n },\n\n data() {\n return {\n password: '',\n confirmPassword: '',\n passwordVisible: false,\n confirmPasswordError: false,\n confirmPasswordHint: '',\n isSubmitting: false,\n isComplete: false,\n };\n },\n\n computed: {\n canSubmit(): boolean {\n return (\n this.password.length >= 4 &&\n this.confirmPassword.length >= 4 &&\n this.password === this.confirmPassword &&\n !this.confirmPasswordError\n );\n },\n },\n\n methods: {\n validateConfirmPassword() {\n this.confirmPasswordError = false;\n this.confirmPasswordHint = '';\n\n if (!this.confirmPassword) return;\n\n if (this.password !== this.confirmPassword) {\n this.confirmPasswordError = true;\n this.confirmPasswordHint = 'Passwords do not match';\n }\n },\n\n async handleSubmit() {\n this.validateConfirmPassword();\n\n if (!this.canSubmit) {\n return;\n }\n\n // Get token from prop or URL query params\n const token = this.token || this.getTokenFromURL();\n\n if (!token) {\n alertUser({\n text: 'No reset token provided. Please use the link from your email.',\n status: Status.error,\n });\n this.$emit('error', 'No token');\n return;\n }\n\n this.isSubmitting = true;\n\n try {\n const result = await resetPassword(token, this.password);\n\n if (result.ok) {\n this.isComplete = true;\n alertUser({\n text: 'Password reset successfully!',\n status: Status.ok,\n });\n } else {\n alertUser({\n text: result.error || 'Failed to reset password',\n status: Status.error,\n });\n this.$emit('error', result.error);\n }\n } catch (error) {\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n this.$emit('error', 'Unexpected error');\n } finally {\n this.isSubmitting = false;\n }\n },\n\n getTokenFromURL(): string | null {\n if (typeof window === 'undefined') return null;\n\n const params = new URLSearchParams(window.location.search);\n return params.get('token');\n },\n },\n});\n</script>\n","<template>\n <div data-cy=\"tags-input\">\n <vue-tags-input\n v-model=\"tag\"\n :tags=\"tags\"\n :autocomplete-items=\"autoCompleteSuggestions\"\n :separators=\"separators\"\n :add-on-key=\"separators\"\n @tags-changed=\"tagsChanged\"\n >\n <template #autocomplete-item=\"props\">\n <div class=\"autocomplete-item\" :class=\"{ 'is-active': props.selected }\">\n <span class=\"tag-name\">{{ props.item.text }}</span>\n <span v-if=\"props.item.data && props.item.data.snippet\" class=\"tag-snippet\">\n - {{ props.item.data.snippet }}\n </span>\n </div>\n </template>\n </vue-tags-input>\n\n <v-btn v-if=\"!hideSubmit\" color=\"success\" :loading=\"loading\" @click=\"submit\">Save Changes</v-btn>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface } from '@vue-skuilder/db';\n// @ts-expect-error - suppress TS error for VueTagsInput - no types available\nimport { VueTagsInput } from '@vojtechlanka/vue-tags-input';\n\nexport interface TagObject {\n text: string;\n style?: string;\n classes?: string;\n data?: {\n snippet: string;\n };\n}\n\nexport interface TagsInputInstance {\n tags: Array<{\n text: string;\n style?: string;\n classes?: string;\n data?: {\n snippet: string;\n };\n }>;\n submit: () => Promise<void>;\n updateAvailableCourseTags: () => Promise<void>;\n}\n\nexport default defineComponent({\n name: 'SkTagsInput',\n\n components: {\n VueTagsInput,\n },\n\n props: {\n courseID: {\n type: String,\n required: true,\n default: '',\n },\n cardID: {\n type: String,\n required: false,\n default: '',\n },\n hideSubmit: {\n type: Boolean,\n required: false,\n default: false,\n },\n },\n\n data() {\n return {\n loading: true,\n tag: '',\n tags: [] as TagObject[],\n initialTags: [] as string[],\n availableCourseTags: [] as Tag[],\n separators: [';', ',', ' '] as string[],\n courseDB: null as CourseDBInterface | null,\n };\n },\n\n computed: {\n autoCompleteSuggestions(): TagObject[] {\n return this.availableCourseTags\n .filter((availableTag) => {\n return availableTag.name.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;\n })\n .map((availableTag) => {\n return {\n text: availableTag.name,\n data: {\n snippet: availableTag.snippet,\n },\n };\n });\n },\n },\n\n watch: {\n async cardID() {\n await this.getAppliedTags();\n },\n async courseID() {\n this.courseDB = getDataLayer().getCourseDB(this.courseID);\n await this.updateAvailableCourseTags();\n },\n },\n\n async created() {\n this.courseDB = getDataLayer().getCourseDB(this.courseID);\n\n await this.updateAvailableCourseTags();\n await this.getAppliedTags();\n },\n\n methods: {\n tagsChanged(newTags: TagObject[]) {\n console.log(`[TagsInput] Tags changing: ${JSON.stringify(newTags)}`);\n this.tags = newTags;\n },\n\n async getAppliedTags() {\n this.initialTags = [];\n this.tags = [];\n try {\n const appliedDocsFindResult = await this.courseDB!.getAppliedTags(this.cardID);\n appliedDocsFindResult.rows.forEach((row) => {\n console.log(`[TagsInput] The following tag is applied:\n\\t${JSON.stringify(row)}`);\n this.tags.push({\n text: row!.value.name,\n style: '',\n classes: '',\n });\n });\n this.initialTags = this.tags.map((tag) => tag.text);\n } catch (e) {\n console.error(`Error in init-getAppliedTags: ${JSON.stringify(e)}, ${e}`);\n } finally {\n this.loading = false;\n }\n },\n\n async updateAvailableCourseTags() {\n try {\n this.availableCourseTags = (await this.courseDB!.getCourseTagStubs()).rows.map((row) => {\n // console.log(`[TagsInput] available tag: ${JSON.stringify(row)}`);\n return row.doc! as Tag;\n });\n } catch (e) {\n console.error(`Error in init-availableCourseTags: ${JSON.stringify(e)}`);\n }\n },\n\n async submit() {\n console.log(`[TagsInput] tagsInput is submitting...`);\n this.loading = true;\n\n try {\n await Promise.all(\n this.tags.map(async (currentTag) => {\n if (!this.initialTags.includes(currentTag.text)) {\n try {\n await this.courseDB!.addTagToCard(this.cardID, currentTag.text);\n console.log(`[TagsInput] Successfully added tag: ${currentTag.text}`);\n } catch (error) {\n console.error(`Failed to add tag ${currentTag.text}:`, error);\n }\n }\n })\n );\n } catch (e) {\n console.error(`Exception adding tags: ${JSON.stringify(e)}`);\n }\n\n try {\n await Promise.all(\n this.initialTags.map(async (initialTag) => {\n if (\n this.tags.filter((tag) => {\n return tag.text === initialTag;\n }).length === 0\n ) {\n try {\n await this.courseDB!.removeTagFromCard(this.cardID, initialTag);\n console.log(`[TagsInput] Successfully removed tag: ${initialTag}`);\n } catch (error) {\n console.error(`Failed to remove tag ${initialTag}:`, error);\n }\n }\n })\n );\n } catch (e) {\n console.error(`Exception removing tags: ${JSON.stringify(e)}`);\n }\n this.loading = false;\n },\n },\n});\n</script>\n\n<style scoped>\n.vue-tags-input {\n max-width: 100%;\n}\n\n.autocomplete-item {\n display: flex;\n align-items: center;\n padding: 5px;\n}\n\n.autocomplete-item.is-active {\n background-color: #e8e8e8;\n}\n\n.autocomplete-item.is-active > .tag-name {\n background-color: #5c6bc0;\n color: white;\n}\n\n.tag-name {\n background-color: #e0e0e0;\n color: #333;\n padding: 2px 6px;\n border-radius: 3px;\n font-weight: bold;\n margin-right: 5px;\n}\n\n.tag-snippet {\n color: #666;\n font-size: 0.9em;\n}\n\n.autocomplete-item.is-active .tag-snippet {\n color: #333; /* Ensure snippet text is visible when item is active */\n}\n</style>\n","<template>\n <div data-cy=\"tags-input\">\n <vue-tags-input\n v-model=\"tag\"\n :tags=\"tags\"\n :autocomplete-items=\"autoCompleteSuggestions\"\n :separators=\"separators\"\n :add-on-key=\"separators\"\n @tags-changed=\"tagsChanged\"\n >\n <template #autocomplete-item=\"props\">\n <div class=\"autocomplete-item\" :class=\"{ 'is-active': props.selected }\">\n <span class=\"tag-name\">{{ props.item.text }}</span>\n <span v-if=\"props.item.data && props.item.data.snippet\" class=\"tag-snippet\">\n - {{ props.item.data.snippet }}\n </span>\n </div>\n </template>\n </vue-tags-input>\n\n <v-btn v-if=\"!hideSubmit\" color=\"success\" :loading=\"loading\" @click=\"submit\">Save Changes</v-btn>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface } from '@vue-skuilder/db';\n// @ts-expect-error - suppress TS error for VueTagsInput - no types available\nimport { VueTagsInput } from '@vojtechlanka/vue-tags-input';\n\nexport interface TagObject {\n text: string;\n style?: string;\n classes?: string;\n data?: {\n snippet: string;\n };\n}\n\nexport interface TagsInputInstance {\n tags: Array<{\n text: string;\n style?: string;\n classes?: string;\n data?: {\n snippet: string;\n };\n }>;\n submit: () => Promise<void>;\n updateAvailableCourseTags: () => Promise<void>;\n}\n\nexport default defineComponent({\n name: 'SkTagsInput',\n\n components: {\n VueTagsInput,\n },\n\n props: {\n courseID: {\n type: String,\n required: true,\n default: '',\n },\n cardID: {\n type: String,\n required: false,\n default: '',\n },\n hideSubmit: {\n type: Boolean,\n required: false,\n default: false,\n },\n },\n\n data() {\n return {\n loading: true,\n tag: '',\n tags: [] as TagObject[],\n initialTags: [] as string[],\n availableCourseTags: [] as Tag[],\n separators: [';', ',', ' '] as string[],\n courseDB: null as CourseDBInterface | null,\n };\n },\n\n computed: {\n autoCompleteSuggestions(): TagObject[] {\n return this.availableCourseTags\n .filter((availableTag) => {\n return availableTag.name.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;\n })\n .map((availableTag) => {\n return {\n text: availableTag.name,\n data: {\n snippet: availableTag.snippet,\n },\n };\n });\n },\n },\n\n watch: {\n async cardID() {\n await this.getAppliedTags();\n },\n async courseID() {\n this.courseDB = getDataLayer().getCourseDB(this.courseID);\n await this.updateAvailableCourseTags();\n },\n },\n\n async created() {\n this.courseDB = getDataLayer().getCourseDB(this.courseID);\n\n await this.updateAvailableCourseTags();\n await this.getAppliedTags();\n },\n\n methods: {\n tagsChanged(newTags: TagObject[]) {\n console.log(`[TagsInput] Tags changing: ${JSON.stringify(newTags)}`);\n this.tags = newTags;\n },\n\n async getAppliedTags() {\n this.initialTags = [];\n this.tags = [];\n try {\n const appliedDocsFindResult = await this.courseDB!.getAppliedTags(this.cardID);\n appliedDocsFindResult.rows.forEach((row) => {\n console.log(`[TagsInput] The following tag is applied:\n\\t${JSON.stringify(row)}`);\n this.tags.push({\n text: row!.value.name,\n style: '',\n classes: '',\n });\n });\n this.initialTags = this.tags.map((tag) => tag.text);\n } catch (e) {\n console.error(`Error in init-getAppliedTags: ${JSON.stringify(e)}, ${e}`);\n } finally {\n this.loading = false;\n }\n },\n\n async updateAvailableCourseTags() {\n try {\n this.availableCourseTags = (await this.courseDB!.getCourseTagStubs()).rows.map((row) => {\n // console.log(`[TagsInput] available tag: ${JSON.stringify(row)}`);\n return row.doc! as Tag;\n });\n } catch (e) {\n console.error(`Error in init-availableCourseTags: ${JSON.stringify(e)}`);\n }\n },\n\n async submit() {\n console.log(`[TagsInput] tagsInput is submitting...`);\n this.loading = true;\n\n try {\n await Promise.all(\n this.tags.map(async (currentTag) => {\n if (!this.initialTags.includes(currentTag.text)) {\n try {\n await this.courseDB!.addTagToCard(this.cardID, currentTag.text);\n console.log(`[TagsInput] Successfully added tag: ${currentTag.text}`);\n } catch (error) {\n console.error(`Failed to add tag ${currentTag.text}:`, error);\n }\n }\n })\n );\n } catch (e) {\n console.error(`Exception adding tags: ${JSON.stringify(e)}`);\n }\n\n try {\n await Promise.all(\n this.initialTags.map(async (initialTag) => {\n if (\n this.tags.filter((tag) => {\n return tag.text === initialTag;\n }).length === 0\n ) {\n try {\n await this.courseDB!.removeTagFromCard(this.cardID, initialTag);\n console.log(`[TagsInput] Successfully removed tag: ${initialTag}`);\n } catch (error) {\n console.error(`Failed to remove tag ${initialTag}:`, error);\n }\n }\n })\n );\n } catch (e) {\n console.error(`Exception removing tags: ${JSON.stringify(e)}`);\n }\n this.loading = false;\n },\n },\n});\n</script>\n\n<style scoped>\n.vue-tags-input {\n max-width: 100%;\n}\n\n.autocomplete-item {\n display: flex;\n align-items: center;\n padding: 5px;\n}\n\n.autocomplete-item.is-active {\n background-color: #e8e8e8;\n}\n\n.autocomplete-item.is-active > .tag-name {\n background-color: #5c6bc0;\n color: white;\n}\n\n.tag-name {\n background-color: #e0e0e0;\n color: #333;\n padding: 2px 6px;\n border-radius: 3px;\n font-weight: bold;\n margin-right: 5px;\n}\n\n.tag-snippet {\n color: #666;\n font-size: 0.9em;\n}\n\n.autocomplete-item.is-active .tag-snippet {\n color: #333; /* Ensure snippet text is visible when item is active */\n}\n</style>\n","<template>\n <v-card>\n <div v-if=\"updatePending\" class=\"d-flex justify-center align-center pa-6\">\n <v-progress-circular indeterminate color=\"primary\" />\n </div>\n <div v-else>\n <paginating-toolbar\n title=\"Exercises\"\n :page=\"page\"\n :pages=\"pages\"\n :subtitle=\"`(${questionCount})`\"\n @first=\"first\"\n @prev=\"prev\"\n @next=\"next\"\n @last=\"last\"\n @set-page=\"(n) => setPage(n)\"\n />\n\n <v-list>\n <template v-for=\"c in cards\" :key=\"c.card.cardID\">\n <v-list-item\n :class=\"{\n 'bg-blue-grey-lighten-5': c.isOpen,\n 'elevation-4': c.isOpen,\n }\"\n density=\"compact\"\n data-cy=\"course-card\"\n >\n <template #prepend>\n <div>\n <v-list-item-title :class=\"{ 'text-blue-grey-darken-1': c.isOpen }\" class=\"font-weight-medium\">\n {{ cardPreview[c.card.cardID] }}\n </v-list-item-title>\n <v-list-item-subtitle>\n ELO: {{ cardElos[c.card.cardID]?.global?.score || '(unknown)' }}\n </v-list-item-subtitle>\n </div>\n </template>\n\n <template #append>\n <v-speed-dial\n v-model=\"c.isOpen\"\n location=\"left center\"\n transition=\"slide-x-transition\"\n style=\"display: flex; flex-direction: row-reverse\"\n persistent\n >\n <template #activator=\"{ props }\">\n <v-btn\n v-bind=\"props\"\n :icon=\"c.isOpen ? 'mdi-close' : 'mdi-plus'\"\n size=\"small\"\n variant=\"text\"\n @click=\"clearSelections(c.card.cardID)\"\n />\n </template>\n\n <v-btn\n v-if=\"editMode === 'full'\"\n key=\"tags\"\n icon\n size=\"small\"\n :variant=\"internalEditMode !== 'tags' ? 'outlined' : 'elevated'\"\n :color=\"internalEditMode === 'tags' ? 'teal' : 'teal-darken-3'\"\n @click.stop=\"internalEditMode = 'tags'\"\n >\n <v-icon>mdi-bookmark</v-icon>\n </v-btn>\n\n <v-btn\n v-if=\"editMode === 'full'\"\n key=\"flag\"\n icon\n size=\"small\"\n :variant=\"internalEditMode !== 'flag' ? 'outlined' : 'elevated'\"\n :color=\"internalEditMode === 'flag' ? 'error' : 'error-darken-3'\"\n @click.stop=\"internalEditMode = 'flag'\"\n >\n <v-icon>mdi-flag</v-icon>\n </v-btn>\n </v-speed-dial>\n </template>\n </v-list-item>\n\n <div v-if=\"c.isOpen\" class=\"px-4 py-2 bg-blue-grey-lighten-5\">\n <card-loader :qualified_id=\"c.card\" :view-lookup=\"viewLookup\" class=\"elevation-1\" />\n\n <!-- Tags display for readonly mode -->\n <div v-if=\"editMode === 'readonly' && cardTags[c.card.cardID]\" class=\"mt-4\">\n <v-chip-group>\n <v-chip\n v-for=\"tag in cardTags[c.card.cardID]\"\n :key=\"tag.name\"\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n >\n {{ tag.name }}\n </v-chip>\n </v-chip-group>\n </div>\n\n <tags-input\n v-show=\"internalEditMode === 'tags' && editMode === 'full'\"\n :course-i-d=\"courseId\"\n :card-i-d=\"c.card.cardID\"\n class=\"mt-4\"\n />\n\n <div v-show=\"internalEditMode === 'flag' && editMode === 'full'\" class=\"mt-4\">\n <v-btn color=\"error\" variant=\"outlined\" @click=\"c.delBtn = true\"> Delete this card </v-btn>\n <span v-if=\"c.delBtn\" class=\"ml-4\">\n <span class=\"mr-2\">Are you sure?</span>\n <v-btn color=\"error\" variant=\"elevated\" @click=\"deleteCard(c.card.cardID)\"> Confirm </v-btn>\n </span>\n </div>\n </div>\n </template>\n </v-list>\n\n <paginating-toolbar\n class=\"elevation-0\"\n :page=\"page\"\n :pages=\"pages\"\n @first=\"first\"\n @prev=\"prev\"\n @next=\"next\"\n @last=\"last\"\n @set-page=\"(n) => setPage(n)\"\n />\n </div>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { displayableDataToViewData, Status, CourseElo } from '@vue-skuilder/common';\nimport {\n getDataLayer,\n CourseDBInterface,\n CardData,\n DisplayableData,\n Tag,\n TagStub,\n QualifiedCardID,\n} from '@vue-skuilder/db';\n// local imports\nimport TagsInput from './TagsInput.vue';\nimport PaginatingToolbar from './PaginatingToolbar.vue';\nimport { ViewComponent } from '../composables/Displayable';\nimport CardLoader from './cardRendering/CardLoader.vue';\nimport { alertUser } from './SnackbarService';\n\n// Legacy isConstructor function removed - no longer needed for Vue 3 components\n\nexport default defineComponent({\n name: 'CourseCardBrowser',\n\n components: {\n CardLoader,\n TagsInput,\n PaginatingToolbar,\n },\n\n props: {\n courseId: {\n type: String,\n required: true,\n },\n tagId: {\n type: String,\n required: false,\n default: '',\n },\n viewLookupFunction: {\n type: Function,\n required: true,\n default: () => {\n console.warn('No viewLookupFunction provided to CourseCardBrowser');\n return null;\n },\n },\n editMode: {\n type: String as PropType<'none' | 'readonly' | 'full'>,\n required: false,\n default: 'full',\n },\n },\n\n data() {\n return {\n courseDB: null as CourseDBInterface | null,\n page: 1,\n pages: [] as number[],\n cards: [] as { card: QualifiedCardID; isOpen: boolean; delBtn: boolean }[],\n cardData: {} as { [card: string]: string[] },\n cardPreview: {} as { [card: string]: string },\n cardElos: {} as Record<string, CourseElo>,\n cardTags: {} as Record<string, TagStub[]>,\n internalEditMode: 'none' as 'tags' | 'flag' | 'none',\n delBtn: false,\n updatePending: true,\n userIsRegistered: false,\n questionCount: 0,\n tags: [] as Tag[],\n viewLookup: this.viewLookupFunction,\n };\n },\n\n async created() {\n try {\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n\n if (this.tagId) {\n this.questionCount = (await this.courseDB.getTag(this.tagId)).taggedCards.length;\n } else {\n this.questionCount = (await this.courseDB!.getCourseInfo()).cardCount;\n }\n\n for (let i = 1; (i - 1) * 25 < this.questionCount; i++) {\n this.pages.push(i);\n }\n\n await this.populateTableData();\n } catch (error) {\n console.error('Error initializing CourseCardBrowser:', error);\n } finally {\n this.updatePending = false;\n }\n },\n\n methods: {\n idQualify(id: string): string {\n const delimiters = id.includes('-');\n if (delimiters) {\n return id;\n } else {\n return `${this.courseId}-${id}`;\n }\n },\n\n idToQualifiedObject(id: string): QualifiedCardID {\n const delimiters = id.includes('-');\n if (delimiters) {\n const parts = id.split('-');\n return {\n courseID: parts[0],\n cardID: parts[1],\n };\n } else {\n return {\n courseID: this.courseId,\n cardID: id,\n };\n }\n },\n\n first() {\n this.page = 1;\n this.populateTableData();\n },\n prev() {\n this.page--;\n this.populateTableData();\n },\n next() {\n this.page++;\n this.populateTableData();\n },\n last() {\n this.page = this.pages.length;\n this.populateTableData();\n },\n setPage(n: number) {\n this.page = n;\n this.populateTableData();\n },\n async loadCardTags(cardIds: string[]) {\n try {\n // Use the proper API method to get tags for each card\n await Promise.all(\n cardIds.map(async (cardId) => {\n const appliedTags = await this.courseDB!.getAppliedTags(cardId);\n\n // Convert to TagStub format\n this.cardTags[cardId] = appliedTags.rows.map((row) => ({\n name: row.value.name,\n snippet: row.value.snippet,\n count: row.value.count,\n }));\n })\n );\n } catch (error) {\n console.error('Error loading card tags:', error);\n }\n },\n clearSelections(exception: string = '') {\n this.cards.forEach((card) => {\n if (card.card.cardID !== exception) {\n card.isOpen = false;\n }\n });\n this.internalEditMode = 'none';\n this.delBtn = false;\n },\n async deleteCard(cID: string) {\n console.log(`Deleting card ${cID}`);\n const res = await this.courseDB!.removeCard(cID);\n if (res.ok) {\n this.cards = this.cards.filter((card) => card.card.cardID != cID);\n this.clearSelections();\n } else {\n console.error(`Failed to delete card:\\n\\n${JSON.stringify(res)}`);\n alertUser({\n text: 'Failed to delete card',\n status: Status.error,\n });\n }\n },\n async populateTableData() {\n this.updatePending = true;\n if (this.tagId) {\n const tag = await this.courseDB!.getTag(this.tagId);\n this.cards = tag.taggedCards.map((c) => {\n return { card: { cardID: c, courseID: this.courseId }, isOpen: false, delBtn: false };\n });\n } else {\n this.cards = (await this.courseDB!.getCardsByELO(0, 25)).map((c) => {\n return {\n card: c,\n isOpen: false,\n delBtn: false,\n };\n });\n }\n\n const toRemove: string[] = [];\n const hydratedCardData = (\n await this.courseDB!.getCourseDocs<CardData>(\n this.cards.map((c) => c.card.cardID),\n {\n include_docs: true,\n }\n )\n ).rows\n .filter((r) => {\n if (r.doc) {\n return true;\n } else {\n console.error(`Card ${r.id}.doc not found.\\ncard: ${JSON.stringify(r)}`);\n // toRemove.push(r.id);\n // if (this.tagId) {\n // this.courseDB!.removeTagFromCard(r.id, this.tagId);\n // }\n return false;\n }\n })\n .map((r) => r.doc!);\n\n this.cards = this.cards.filter((c) => !toRemove.includes(c.card.cardID));\n\n hydratedCardData.forEach((c) => {\n if (c && c.id_displayable_data) {\n this.cardData[c._id] = c.id_displayable_data;\n }\n });\n\n try {\n await Promise.all(\n this.cards.map(async (c) => {\n const _cardID: string = c.card.cardID;\n\n const tmpCardData = hydratedCardData.find((c) => c._id == _cardID);\n if (!tmpCardData || !tmpCardData.id_displayable_data) {\n console.error(`No valid data found for card ${_cardID}`);\n return;\n }\n const tmpView: ViewComponent = this.viewLookupFunction(\n tmpCardData.id_view || 'default.question.BlanksCard.FillInView'\n );\n\n const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {\n return this.courseDB!.getCourseDoc<DisplayableData>(id, {\n attachments: false,\n binary: true,\n });\n });\n\n const allDocs = await Promise.all(tmpDataDocs);\n await Promise.all(\n allDocs.map((doc) => {\n const tmpData = [];\n tmpData.unshift(displayableDataToViewData(doc));\n\n // Vue 3: Use component name for preview (legacy constructor code removed)\n this.cardPreview[c.card.cardID] = tmpView.name ? tmpView.name : 'Unknown';\n })\n );\n })\n );\n\n // Load ELO data for all cards\n const cardIds = this.cards.map((c) => c.card.cardID);\n const eloData = await this.courseDB!.getCardEloData(cardIds); // general case lookup\n\n // Store ELO data indexed by card ID\n cardIds.forEach((cardId, index) => {\n this.cardElos[cardId] = eloData[index];\n });\n\n // Load tags for each card\n await this.loadCardTags(cardIds);\n } catch (error) {\n console.error('Error populating table data:', error);\n } finally {\n this.updatePending = false;\n this.$forceUpdate();\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter, .component-fade-leave-to\n/* .component-fade-leave-active below version 2.1.8 */ {\n opacity: 0;\n}\n\n.component-scale-enter-active,\n.component-scale-leave-active {\n max-height: auto;\n transform: scale(1, 1);\n transform-origin: top;\n transition:\n transform 0.3s ease,\n max-height 0.3s ease;\n}\n.component-scale-enter,\n.component-fade-leave-to {\n max-height: 0px;\n transform: scale(1, 0);\n overflow: hidden;\n}\n</style>\n","<template>\n <v-card>\n <div v-if=\"updatePending\" class=\"d-flex justify-center align-center pa-6\">\n <v-progress-circular indeterminate color=\"primary\" />\n </div>\n <div v-else>\n <paginating-toolbar\n title=\"Exercises\"\n :page=\"page\"\n :pages=\"pages\"\n :subtitle=\"`(${questionCount})`\"\n @first=\"first\"\n @prev=\"prev\"\n @next=\"next\"\n @last=\"last\"\n @set-page=\"(n) => setPage(n)\"\n />\n\n <v-list>\n <template v-for=\"c in cards\" :key=\"c.card.cardID\">\n <v-list-item\n :class=\"{\n 'bg-blue-grey-lighten-5': c.isOpen,\n 'elevation-4': c.isOpen,\n }\"\n density=\"compact\"\n data-cy=\"course-card\"\n >\n <template #prepend>\n <div>\n <v-list-item-title :class=\"{ 'text-blue-grey-darken-1': c.isOpen }\" class=\"font-weight-medium\">\n {{ cardPreview[c.card.cardID] }}\n </v-list-item-title>\n <v-list-item-subtitle>\n ELO: {{ cardElos[c.card.cardID]?.global?.score || '(unknown)' }}\n </v-list-item-subtitle>\n </div>\n </template>\n\n <template #append>\n <v-speed-dial\n v-model=\"c.isOpen\"\n location=\"left center\"\n transition=\"slide-x-transition\"\n style=\"display: flex; flex-direction: row-reverse\"\n persistent\n >\n <template #activator=\"{ props }\">\n <v-btn\n v-bind=\"props\"\n :icon=\"c.isOpen ? 'mdi-close' : 'mdi-plus'\"\n size=\"small\"\n variant=\"text\"\n @click=\"clearSelections(c.card.cardID)\"\n />\n </template>\n\n <v-btn\n v-if=\"editMode === 'full'\"\n key=\"tags\"\n icon\n size=\"small\"\n :variant=\"internalEditMode !== 'tags' ? 'outlined' : 'elevated'\"\n :color=\"internalEditMode === 'tags' ? 'teal' : 'teal-darken-3'\"\n @click.stop=\"internalEditMode = 'tags'\"\n >\n <v-icon>mdi-bookmark</v-icon>\n </v-btn>\n\n <v-btn\n v-if=\"editMode === 'full'\"\n key=\"flag\"\n icon\n size=\"small\"\n :variant=\"internalEditMode !== 'flag' ? 'outlined' : 'elevated'\"\n :color=\"internalEditMode === 'flag' ? 'error' : 'error-darken-3'\"\n @click.stop=\"internalEditMode = 'flag'\"\n >\n <v-icon>mdi-flag</v-icon>\n </v-btn>\n </v-speed-dial>\n </template>\n </v-list-item>\n\n <div v-if=\"c.isOpen\" class=\"px-4 py-2 bg-blue-grey-lighten-5\">\n <card-loader :qualified_id=\"c.card\" :view-lookup=\"viewLookup\" class=\"elevation-1\" />\n\n <!-- Tags display for readonly mode -->\n <div v-if=\"editMode === 'readonly' && cardTags[c.card.cardID]\" class=\"mt-4\">\n <v-chip-group>\n <v-chip\n v-for=\"tag in cardTags[c.card.cardID]\"\n :key=\"tag.name\"\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n >\n {{ tag.name }}\n </v-chip>\n </v-chip-group>\n </div>\n\n <tags-input\n v-show=\"internalEditMode === 'tags' && editMode === 'full'\"\n :course-i-d=\"courseId\"\n :card-i-d=\"c.card.cardID\"\n class=\"mt-4\"\n />\n\n <div v-show=\"internalEditMode === 'flag' && editMode === 'full'\" class=\"mt-4\">\n <v-btn color=\"error\" variant=\"outlined\" @click=\"c.delBtn = true\"> Delete this card </v-btn>\n <span v-if=\"c.delBtn\" class=\"ml-4\">\n <span class=\"mr-2\">Are you sure?</span>\n <v-btn color=\"error\" variant=\"elevated\" @click=\"deleteCard(c.card.cardID)\"> Confirm </v-btn>\n </span>\n </div>\n </div>\n </template>\n </v-list>\n\n <paginating-toolbar\n class=\"elevation-0\"\n :page=\"page\"\n :pages=\"pages\"\n @first=\"first\"\n @prev=\"prev\"\n @next=\"next\"\n @last=\"last\"\n @set-page=\"(n) => setPage(n)\"\n />\n </div>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { displayableDataToViewData, Status, CourseElo } from '@vue-skuilder/common';\nimport {\n getDataLayer,\n CourseDBInterface,\n CardData,\n DisplayableData,\n Tag,\n TagStub,\n QualifiedCardID,\n} from '@vue-skuilder/db';\n// local imports\nimport TagsInput from './TagsInput.vue';\nimport PaginatingToolbar from './PaginatingToolbar.vue';\nimport { ViewComponent } from '../composables/Displayable';\nimport CardLoader from './cardRendering/CardLoader.vue';\nimport { alertUser } from './SnackbarService';\n\n// Legacy isConstructor function removed - no longer needed for Vue 3 components\n\nexport default defineComponent({\n name: 'CourseCardBrowser',\n\n components: {\n CardLoader,\n TagsInput,\n PaginatingToolbar,\n },\n\n props: {\n courseId: {\n type: String,\n required: true,\n },\n tagId: {\n type: String,\n required: false,\n default: '',\n },\n viewLookupFunction: {\n type: Function,\n required: true,\n default: () => {\n console.warn('No viewLookupFunction provided to CourseCardBrowser');\n return null;\n },\n },\n editMode: {\n type: String as PropType<'none' | 'readonly' | 'full'>,\n required: false,\n default: 'full',\n },\n },\n\n data() {\n return {\n courseDB: null as CourseDBInterface | null,\n page: 1,\n pages: [] as number[],\n cards: [] as { card: QualifiedCardID; isOpen: boolean; delBtn: boolean }[],\n cardData: {} as { [card: string]: string[] },\n cardPreview: {} as { [card: string]: string },\n cardElos: {} as Record<string, CourseElo>,\n cardTags: {} as Record<string, TagStub[]>,\n internalEditMode: 'none' as 'tags' | 'flag' | 'none',\n delBtn: false,\n updatePending: true,\n userIsRegistered: false,\n questionCount: 0,\n tags: [] as Tag[],\n viewLookup: this.viewLookupFunction,\n };\n },\n\n async created() {\n try {\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n\n if (this.tagId) {\n this.questionCount = (await this.courseDB.getTag(this.tagId)).taggedCards.length;\n } else {\n this.questionCount = (await this.courseDB!.getCourseInfo()).cardCount;\n }\n\n for (let i = 1; (i - 1) * 25 < this.questionCount; i++) {\n this.pages.push(i);\n }\n\n await this.populateTableData();\n } catch (error) {\n console.error('Error initializing CourseCardBrowser:', error);\n } finally {\n this.updatePending = false;\n }\n },\n\n methods: {\n idQualify(id: string): string {\n const delimiters = id.includes('-');\n if (delimiters) {\n return id;\n } else {\n return `${this.courseId}-${id}`;\n }\n },\n\n idToQualifiedObject(id: string): QualifiedCardID {\n const delimiters = id.includes('-');\n if (delimiters) {\n const parts = id.split('-');\n return {\n courseID: parts[0],\n cardID: parts[1],\n };\n } else {\n return {\n courseID: this.courseId,\n cardID: id,\n };\n }\n },\n\n first() {\n this.page = 1;\n this.populateTableData();\n },\n prev() {\n this.page--;\n this.populateTableData();\n },\n next() {\n this.page++;\n this.populateTableData();\n },\n last() {\n this.page = this.pages.length;\n this.populateTableData();\n },\n setPage(n: number) {\n this.page = n;\n this.populateTableData();\n },\n async loadCardTags(cardIds: string[]) {\n try {\n // Use the proper API method to get tags for each card\n await Promise.all(\n cardIds.map(async (cardId) => {\n const appliedTags = await this.courseDB!.getAppliedTags(cardId);\n\n // Convert to TagStub format\n this.cardTags[cardId] = appliedTags.rows.map((row) => ({\n name: row.value.name,\n snippet: row.value.snippet,\n count: row.value.count,\n }));\n })\n );\n } catch (error) {\n console.error('Error loading card tags:', error);\n }\n },\n clearSelections(exception: string = '') {\n this.cards.forEach((card) => {\n if (card.card.cardID !== exception) {\n card.isOpen = false;\n }\n });\n this.internalEditMode = 'none';\n this.delBtn = false;\n },\n async deleteCard(cID: string) {\n console.log(`Deleting card ${cID}`);\n const res = await this.courseDB!.removeCard(cID);\n if (res.ok) {\n this.cards = this.cards.filter((card) => card.card.cardID != cID);\n this.clearSelections();\n } else {\n console.error(`Failed to delete card:\\n\\n${JSON.stringify(res)}`);\n alertUser({\n text: 'Failed to delete card',\n status: Status.error,\n });\n }\n },\n async populateTableData() {\n this.updatePending = true;\n if (this.tagId) {\n const tag = await this.courseDB!.getTag(this.tagId);\n this.cards = tag.taggedCards.map((c) => {\n return { card: { cardID: c, courseID: this.courseId }, isOpen: false, delBtn: false };\n });\n } else {\n this.cards = (await this.courseDB!.getCardsByELO(0, 25)).map((c) => {\n return {\n card: c,\n isOpen: false,\n delBtn: false,\n };\n });\n }\n\n const toRemove: string[] = [];\n const hydratedCardData = (\n await this.courseDB!.getCourseDocs<CardData>(\n this.cards.map((c) => c.card.cardID),\n {\n include_docs: true,\n }\n )\n ).rows\n .filter((r) => {\n if (r.doc) {\n return true;\n } else {\n console.error(`Card ${r.id}.doc not found.\\ncard: ${JSON.stringify(r)}`);\n // toRemove.push(r.id);\n // if (this.tagId) {\n // this.courseDB!.removeTagFromCard(r.id, this.tagId);\n // }\n return false;\n }\n })\n .map((r) => r.doc!);\n\n this.cards = this.cards.filter((c) => !toRemove.includes(c.card.cardID));\n\n hydratedCardData.forEach((c) => {\n if (c && c.id_displayable_data) {\n this.cardData[c._id] = c.id_displayable_data;\n }\n });\n\n try {\n await Promise.all(\n this.cards.map(async (c) => {\n const _cardID: string = c.card.cardID;\n\n const tmpCardData = hydratedCardData.find((c) => c._id == _cardID);\n if (!tmpCardData || !tmpCardData.id_displayable_data) {\n console.error(`No valid data found for card ${_cardID}`);\n return;\n }\n const tmpView: ViewComponent = this.viewLookupFunction(\n tmpCardData.id_view || 'default.question.BlanksCard.FillInView'\n );\n\n const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {\n return this.courseDB!.getCourseDoc<DisplayableData>(id, {\n attachments: false,\n binary: true,\n });\n });\n\n const allDocs = await Promise.all(tmpDataDocs);\n await Promise.all(\n allDocs.map((doc) => {\n const tmpData = [];\n tmpData.unshift(displayableDataToViewData(doc));\n\n // Vue 3: Use component name for preview (legacy constructor code removed)\n this.cardPreview[c.card.cardID] = tmpView.name ? tmpView.name : 'Unknown';\n })\n );\n })\n );\n\n // Load ELO data for all cards\n const cardIds = this.cards.map((c) => c.card.cardID);\n const eloData = await this.courseDB!.getCardEloData(cardIds); // general case lookup\n\n // Store ELO data indexed by card ID\n cardIds.forEach((cardId, index) => {\n this.cardElos[cardId] = eloData[index];\n });\n\n // Load tags for each card\n await this.loadCardTags(cardIds);\n } catch (error) {\n console.error('Error populating table data:', error);\n } finally {\n this.updatePending = false;\n this.$forceUpdate();\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter, .component-fade-leave-to\n/* .component-fade-leave-active below version 2.1.8 */ {\n opacity: 0;\n}\n\n.component-scale-enter-active,\n.component-scale-leave-active {\n max-height: auto;\n transform: scale(1, 1);\n transform-origin: top;\n transition:\n transform 0.3s ease,\n max-height 0.3s ease;\n}\n.component-scale-enter,\n.component-fade-leave-to {\n max-height: 0px;\n transform: scale(1, 0);\n overflow: hidden;\n}\n</style>\n","<template>\n <div v-if=\"!updatePending\">\n <slot name=\"header\" :course-config=\"courseConfig\" :course-id=\"courseId\">\n <h1 class=\"text-h4 mb-2\">{{ courseConfig.name }}</h1>\n </slot>\n\n <p class=\"text-body-2\">\n {{ courseConfig.description }}\n </p>\n\n <slot\n name=\"actions\"\n :user-is-registered=\"userIsRegistered\"\n :course-id=\"courseId\"\n :edit-mode=\"editMode\"\n :register=\"register\"\n :drop=\"drop\"\n >\n <!-- Default fallback content if no actions slot provided -->\n <transition name=\"component-fade\" mode=\"out-in\">\n <div v-if=\"userIsRegistered\">\n <v-btn color=\"success\" data-cy=\"focused-study-session-btn\" class=\"me-2\">Start ad study session</v-btn>\n <v-btn v-if=\"editMode === 'full'\" data-cy=\"add-content-btn\" color=\"indigo-lighten-1\" class=\"me-2\">\n <v-icon start>mdi-plus</v-icon>\n Add content\n </v-btn>\n <v-btn\n v-if=\"editMode === 'full'\"\n color=\"green-darken-2\"\n title=\"Rank course content for difficulty\"\n class=\"me-2\"\n >\n <v-icon start>mdi-format-list-numbered</v-icon>\n Arrange\n </v-btn>\n <v-btn v-if=\"editMode === 'full'\" color=\"error\" size=\"small\" variant=\"outlined\" @click=\"drop\">\n Drop this course\n </v-btn>\n </div>\n <div v-else>\n <v-btn data-cy=\"register-btn\" color=\"primary\" class=\"me-2\" @click=\"register\">Register</v-btn>\n <v-btn variant=\"outlined\" color=\"primary\" class=\"me-2\">Start a trial study session</v-btn>\n </div>\n </transition>\n </slot>\n\n <slot name=\"additional-content\"></slot>\n \n <v-card class=\"my-2\">\n <v-toolbar density=\"compact\">\n <v-toolbar-title>Tags</v-toolbar-title>\n <v-toolbar-items>\n <v-btn variant=\"text\">({{ tags.length }})</v-btn>\n </v-toolbar-items>\n </v-toolbar>\n <v-card-text>\n <span v-for=\"(tag, i) in tags\" :key=\"i\">\n <slot name=\"tag-link\" :tag=\"tag\" :course-id=\"courseId\">\n <v-chip variant=\"tonal\" class=\"me-2 mb-2\">\n {{ tag.name }}\n </v-chip>\n </slot>\n </span>\n </v-card-text>\n </v-card>\n\n <course-card-browser\n class=\"my-3\"\n :course-id=\"courseId\"\n :view-lookup-function=\"viewLookupFunction\"\n :edit-mode=\"editMode\"\n />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\n// import { MidiConfig } from '@vue-skuilder/courseware'; // Removed to break circular dependency\nimport CourseCardBrowser from './CourseCardBrowser.vue';\nimport { log } from '@vue-skuilder/common';\nimport { CourseDBInterface, Tag, UserDBInterface, getDataLayer } from '@vue-skuilder/db';\nimport { CourseConfig } from '@vue-skuilder/common';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\nexport default defineComponent({\n name: 'CourseInformation',\n\n components: {\n // MidiConfig, // Removed to break circular dependency\n CourseCardBrowser,\n },\n\n props: {\n courseId: {\n type: String as PropType<string>,\n required: true,\n },\n viewLookupFunction: {\n type: Function,\n required: false,\n default: (x: unknown) => {\n console.warn('No viewLookupFunction provided to CourseInformation');\n return null;\n },\n },\n editMode: {\n type: String as PropType<'none' | 'readonly' | 'full'>,\n required: false,\n default: 'full',\n },\n },\n\n data() {\n return {\n courseDB: null as CourseDBInterface | null,\n nameRules: [\n (value: string): string | boolean => {\n const max = 30;\n return value.length > max ? `Course name must be ${max} characters or less` : true;\n },\n ],\n updatePending: true,\n courseConfig: {} as CourseConfig,\n userIsRegistered: false,\n tags: [] as Tag[],\n user: null as UserDBInterface | null,\n };\n },\n\n computed: {\n // isPianoCourse removed - piano-specific logic should be in wrapper component\n },\n\n async created() {\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n this.user = await getCurrentUser();\n\n const userCourses = await this.user.getCourseRegistrationsDoc();\n\n // Admin users always have edit access (for studio mode)\n if (this.user.getUsername() === 'admin') {\n this.userIsRegistered = true;\n } else {\n this.userIsRegistered =\n userCourses.courses.filter((c) => {\n return c.courseID === this.courseId && (c.status === 'active' || c.status === undefined);\n }).length === 1;\n }\n\n this.courseConfig = (await this.courseDB!.getCourseConfig())!;\n this.tags = (await this.courseDB!.getCourseTagStubs()).rows.map((r) => r.doc!);\n this.updatePending = false;\n },\n\n methods: {\n async register() {\n log(`Registering for ${this.courseId}`);\n const res = await this.user!.registerForCourse(this.courseId);\n if (res.ok) {\n this.userIsRegistered = true;\n }\n },\n\n async drop() {\n log(`Dropping course ${this.courseId}`);\n const res = await this.user!.dropCourse(this.courseId);\n if (res.ok) {\n this.userIsRegistered = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n\n.component-scale-enter-active,\n.component-scale-leave-active {\n max-height: auto;\n transform: scale(1, 1);\n transform-origin: top;\n transition:\n transform 0.3s ease,\n max-height 0.3s ease;\n}\n.component-scale-enter,\n.component-fade-leave-to {\n max-height: 0px;\n transform: scale(1, 0);\n overflow: hidden;\n}\n</style>\n","<template>\n <div v-if=\"!updatePending\">\n <slot name=\"header\" :course-config=\"courseConfig\" :course-id=\"courseId\">\n <h1 class=\"text-h4 mb-2\">{{ courseConfig.name }}</h1>\n </slot>\n\n <p class=\"text-body-2\">\n {{ courseConfig.description }}\n </p>\n\n <slot\n name=\"actions\"\n :user-is-registered=\"userIsRegistered\"\n :course-id=\"courseId\"\n :edit-mode=\"editMode\"\n :register=\"register\"\n :drop=\"drop\"\n >\n <!-- Default fallback content if no actions slot provided -->\n <transition name=\"component-fade\" mode=\"out-in\">\n <div v-if=\"userIsRegistered\">\n <v-btn color=\"success\" data-cy=\"focused-study-session-btn\" class=\"me-2\">Start ad study session</v-btn>\n <v-btn v-if=\"editMode === 'full'\" data-cy=\"add-content-btn\" color=\"indigo-lighten-1\" class=\"me-2\">\n <v-icon start>mdi-plus</v-icon>\n Add content\n </v-btn>\n <v-btn\n v-if=\"editMode === 'full'\"\n color=\"green-darken-2\"\n title=\"Rank course content for difficulty\"\n class=\"me-2\"\n >\n <v-icon start>mdi-format-list-numbered</v-icon>\n Arrange\n </v-btn>\n <v-btn v-if=\"editMode === 'full'\" color=\"error\" size=\"small\" variant=\"outlined\" @click=\"drop\">\n Drop this course\n </v-btn>\n </div>\n <div v-else>\n <v-btn data-cy=\"register-btn\" color=\"primary\" class=\"me-2\" @click=\"register\">Register</v-btn>\n <v-btn variant=\"outlined\" color=\"primary\" class=\"me-2\">Start a trial study session</v-btn>\n </div>\n </transition>\n </slot>\n\n <slot name=\"additional-content\"></slot>\n \n <v-card class=\"my-2\">\n <v-toolbar density=\"compact\">\n <v-toolbar-title>Tags</v-toolbar-title>\n <v-toolbar-items>\n <v-btn variant=\"text\">({{ tags.length }})</v-btn>\n </v-toolbar-items>\n </v-toolbar>\n <v-card-text>\n <span v-for=\"(tag, i) in tags\" :key=\"i\">\n <slot name=\"tag-link\" :tag=\"tag\" :course-id=\"courseId\">\n <v-chip variant=\"tonal\" class=\"me-2 mb-2\">\n {{ tag.name }}\n </v-chip>\n </slot>\n </span>\n </v-card-text>\n </v-card>\n\n <course-card-browser\n class=\"my-3\"\n :course-id=\"courseId\"\n :view-lookup-function=\"viewLookupFunction\"\n :edit-mode=\"editMode\"\n />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\n// import { MidiConfig } from '@vue-skuilder/courseware'; // Removed to break circular dependency\nimport CourseCardBrowser from './CourseCardBrowser.vue';\nimport { log } from '@vue-skuilder/common';\nimport { CourseDBInterface, Tag, UserDBInterface, getDataLayer } from '@vue-skuilder/db';\nimport { CourseConfig } from '@vue-skuilder/common';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\nexport default defineComponent({\n name: 'CourseInformation',\n\n components: {\n // MidiConfig, // Removed to break circular dependency\n CourseCardBrowser,\n },\n\n props: {\n courseId: {\n type: String as PropType<string>,\n required: true,\n },\n viewLookupFunction: {\n type: Function,\n required: false,\n default: (x: unknown) => {\n console.warn('No viewLookupFunction provided to CourseInformation');\n return null;\n },\n },\n editMode: {\n type: String as PropType<'none' | 'readonly' | 'full'>,\n required: false,\n default: 'full',\n },\n },\n\n data() {\n return {\n courseDB: null as CourseDBInterface | null,\n nameRules: [\n (value: string): string | boolean => {\n const max = 30;\n return value.length > max ? `Course name must be ${max} characters or less` : true;\n },\n ],\n updatePending: true,\n courseConfig: {} as CourseConfig,\n userIsRegistered: false,\n tags: [] as Tag[],\n user: null as UserDBInterface | null,\n };\n },\n\n computed: {\n // isPianoCourse removed - piano-specific logic should be in wrapper component\n },\n\n async created() {\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n this.user = await getCurrentUser();\n\n const userCourses = await this.user.getCourseRegistrationsDoc();\n\n // Admin users always have edit access (for studio mode)\n if (this.user.getUsername() === 'admin') {\n this.userIsRegistered = true;\n } else {\n this.userIsRegistered =\n userCourses.courses.filter((c) => {\n return c.courseID === this.courseId && (c.status === 'active' || c.status === undefined);\n }).length === 1;\n }\n\n this.courseConfig = (await this.courseDB!.getCourseConfig())!;\n this.tags = (await this.courseDB!.getCourseTagStubs()).rows.map((r) => r.doc!);\n this.updatePending = false;\n },\n\n methods: {\n async register() {\n log(`Registering for ${this.courseId}`);\n const res = await this.user!.registerForCourse(this.courseId);\n if (res.ok) {\n this.userIsRegistered = true;\n }\n },\n\n async drop() {\n log(`Dropping course ${this.courseId}`);\n const res = await this.user!.dropCourse(this.courseId);\n if (res.ok) {\n this.userIsRegistered = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n\n.component-scale-enter-active,\n.component-scale-leave-active {\n max-height: auto;\n transform: scale(1, 1);\n transform-origin: top;\n transition:\n transform 0.3s ease,\n max-height 0.3s ease;\n}\n.component-scale-enter,\n.component-fade-leave-to {\n max-height: 0px;\n transform: scale(1, 0);\n overflow: hidden;\n}\n</style>\n","<template>\n <v-row column align=\"center\" justify=\"center\">\n <CardViewer :view=\"views[viewIndex]\" :data=\"data\" :course_id=\"'[browsing]'\" :card_id=\"'[browsing]'\" />\n <br /><br />\n <div v-if=\"!suppressSpinner\" class=\"text-subtitle-1 pa-2\">\n <v-btn v-if=\"spinner\" icon variant=\"outlined\" color=\"primary\" @click=\"decrementView\">\n <v-icon>mdi-chevron-left</v-icon>\n </v-btn>\n {{ views[viewIndex].name }}\n <v-btn v-if=\"spinner\" icon variant=\"outlined\" color=\"primary\" @click=\"incrementView\">\n <v-icon>mdi-chevron-right</v-icon>\n </v-btn>\n </div>\n </v-row>\n</template>\n\n<script lang=\"ts\">\nimport { ViewData } from '@vue-skuilder/common';\nimport { defineComponent, PropType } from 'vue';\nimport { ViewComponent } from '../composables/Displayable';\nimport CardViewer from './cardRendering/CardViewer.vue';\nimport { useCardPreviewModeStore } from '../stores/useCardPreviewModeStore';\n\nexport default defineComponent({\n name: 'CardBrowser',\n\n components: {\n CardViewer,\n },\n\n props: {\n views: {\n type: Array as PropType<Array<ViewComponent>>,\n required: true,\n },\n data: {\n type: Array as PropType<ViewData[]>,\n required: true,\n },\n suppressSpinner: {\n type: Boolean,\n default: false,\n },\n },\n\n data() {\n return {\n viewIndex: 0,\n previewMode: useCardPreviewModeStore(),\n };\n },\n\n computed: {\n spinner(): boolean {\n return this.views.length > 1;\n },\n },\n\n created() {\n console.log(`[CardBrowser] Card browser created. Cards now in 'prewviewMode'`);\n this.previewMode.setPreviewMode(true);\n },\n\n unmounted() {\n console.log(`[CardBrowser] Card browser unmounted. Cards no longer in 'prewviewMode'`);\n this.previewMode.setPreviewMode(false);\n },\n\n methods: {\n incrementView() {\n this.viewIndex++;\n this.viewIndex = (this.viewIndex + this.views.length) % this.views.length;\n },\n\n decrementView() {\n this.viewIndex--;\n this.viewIndex = (this.viewIndex + this.views.length) % this.views.length;\n },\n },\n});\n</script>\n","<template>\n <v-row column align=\"center\" justify=\"center\">\n <CardViewer :view=\"views[viewIndex]\" :data=\"data\" :course_id=\"'[browsing]'\" :card_id=\"'[browsing]'\" />\n <br /><br />\n <div v-if=\"!suppressSpinner\" class=\"text-subtitle-1 pa-2\">\n <v-btn v-if=\"spinner\" icon variant=\"outlined\" color=\"primary\" @click=\"decrementView\">\n <v-icon>mdi-chevron-left</v-icon>\n </v-btn>\n {{ views[viewIndex].name }}\n <v-btn v-if=\"spinner\" icon variant=\"outlined\" color=\"primary\" @click=\"incrementView\">\n <v-icon>mdi-chevron-right</v-icon>\n </v-btn>\n </div>\n </v-row>\n</template>\n\n<script lang=\"ts\">\nimport { ViewData } from '@vue-skuilder/common';\nimport { defineComponent, PropType } from 'vue';\nimport { ViewComponent } from '../composables/Displayable';\nimport CardViewer from './cardRendering/CardViewer.vue';\nimport { useCardPreviewModeStore } from '../stores/useCardPreviewModeStore';\n\nexport default defineComponent({\n name: 'CardBrowser',\n\n components: {\n CardViewer,\n },\n\n props: {\n views: {\n type: Array as PropType<Array<ViewComponent>>,\n required: true,\n },\n data: {\n type: Array as PropType<ViewData[]>,\n required: true,\n },\n suppressSpinner: {\n type: Boolean,\n default: false,\n },\n },\n\n data() {\n return {\n viewIndex: 0,\n previewMode: useCardPreviewModeStore(),\n };\n },\n\n computed: {\n spinner(): boolean {\n return this.views.length > 1;\n },\n },\n\n created() {\n console.log(`[CardBrowser] Card browser created. Cards now in 'prewviewMode'`);\n this.previewMode.setPreviewMode(true);\n },\n\n unmounted() {\n console.log(`[CardBrowser] Card browser unmounted. Cards no longer in 'prewviewMode'`);\n this.previewMode.setPreviewMode(false);\n },\n\n methods: {\n incrementView() {\n this.viewIndex++;\n this.viewIndex = (this.viewIndex + this.views.length) % this.views.length;\n },\n\n decrementView() {\n this.viewIndex--;\n this.viewIndex = (this.viewIndex + this.views.length) % this.views.length;\n },\n },\n});\n</script>\n","<template>\n <div class=\"course-tag-filter-widget\">\n <v-autocomplete\n v-model=\"localFilter.include\"\n :items=\"availableTags\"\n :loading=\"loading\"\n :disabled=\"loading\"\n label=\"Include tags\"\n placeholder=\"Type to search tags...\"\n hint=\"Cards must have at least one of these tags\"\n persistent-hint\n multiple\n chips\n closable-chips\n clearable\n density=\"compact\"\n variant=\"outlined\"\n class=\"mb-2\"\n :custom-filter=\"fuzzyFilter\"\n no-data-text=\"No matching tags found\"\n >\n <template #chip=\"{ props, item }\">\n <v-chip\n v-bind=\"props\"\n color=\"primary\"\n variant=\"tonal\"\n size=\"small\"\n >\n {{ item.raw }}\n </v-chip>\n </template>\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\" :title=\"item.raw\">\n <template #subtitle v-if=\"tagSnippets[item.raw]\">\n {{ tagSnippets[item.raw] }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <v-autocomplete\n v-model=\"localFilter.exclude\"\n :items=\"availableTagsForExclude\"\n :loading=\"loading\"\n :disabled=\"loading\"\n label=\"Exclude tags\"\n placeholder=\"Type to search tags...\"\n hint=\"Cards with these tags will be excluded\"\n persistent-hint\n multiple\n chips\n closable-chips\n clearable\n density=\"compact\"\n variant=\"outlined\"\n :custom-filter=\"fuzzyFilter\"\n no-data-text=\"No matching tags found\"\n >\n <template #chip=\"{ props, item }\">\n <v-chip\n v-bind=\"props\"\n color=\"error\"\n variant=\"tonal\"\n size=\"small\"\n >\n {{ item.raw }}\n </v-chip>\n </template>\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\" :title=\"item.raw\">\n <template #subtitle v-if=\"tagSnippets[item.raw]\">\n {{ tagSnippets[item.raw] }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <div v-if=\"hasActiveFilter\" class=\"filter-summary text-caption mt-2\">\n <v-icon size=\"small\" color=\"info\" class=\"mr-1\">mdi-filter</v-icon>\n <span v-if=\"localFilter.include.length\">\n Including: {{ localFilter.include.join(', ') }}\n </span>\n <span v-if=\"localFilter.include.length && localFilter.exclude.length\"> · </span>\n <span v-if=\"localFilter.exclude.length\">\n Excluding: {{ localFilter.exclude.join(', ') }}\n </span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, ref, computed, watch, onMounted } from 'vue';\nimport { TagFilter, emptyTagFilter, hasActiveFilter as checkActiveFilter } from '@vue-skuilder/common';\nimport { getDataLayer, Tag } from '@vue-skuilder/db';\n\nexport default defineComponent({\n name: 'CourseTagFilterWidget',\n\n props: {\n courseId: {\n type: String,\n required: true,\n },\n modelValue: {\n type: Object as PropType<TagFilter | undefined>,\n default: undefined,\n },\n },\n\n emits: ['update:modelValue'],\n\n setup(props, { emit }) {\n const loading = ref(true);\n const availableTags = ref<string[]>([]);\n const tagSnippets = ref<Record<string, string>>({});\n\n // Local copy of the filter for editing\n const localFilter = ref<TagFilter>(\n props.modelValue ? { ...props.modelValue } : emptyTagFilter()\n );\n\n // Computed: tags available for exclude (not already in include)\n const availableTagsForExclude = computed(() => {\n return availableTags.value.filter(\n (tag) => !localFilter.value.include.includes(tag)\n );\n });\n\n // Computed: whether filter has any active constraints\n const hasActiveFilter = computed(() => {\n return checkActiveFilter(localFilter.value);\n });\n\n // Fuzzy filter function for autocomplete\n const fuzzyFilter = (itemText: string, queryText: string): boolean => {\n if (!queryText) return true;\n\n const query = queryText.toLowerCase();\n const text = itemText.toLowerCase();\n\n // Simple fuzzy match: all query characters appear in order\n let queryIndex = 0;\n for (let i = 0; i < text.length && queryIndex < query.length; i++) {\n if (text[i] === query[queryIndex]) {\n queryIndex++;\n }\n }\n return queryIndex === query.length;\n };\n\n // Watch for external changes to modelValue\n watch(\n () => props.modelValue,\n (newValue) => {\n if (newValue) {\n localFilter.value = { ...newValue };\n } else {\n localFilter.value = emptyTagFilter();\n }\n },\n { deep: true }\n );\n\n // Emit changes when local filter changes\n watch(\n localFilter,\n (newFilter) => {\n emit('update:modelValue', { ...newFilter });\n },\n { deep: true }\n );\n\n // Watch for courseId changes and reload tags\n watch(\n () => props.courseId,\n async (newCourseId) => {\n if (newCourseId) {\n await loadTags();\n }\n }\n );\n\n // Load tags for the course\n async function loadTags() {\n loading.value = true;\n try {\n const courseDB = getDataLayer().getCourseDB(props.courseId);\n const response = await courseDB.getCourseTagStubs();\n\n availableTags.value = [];\n tagSnippets.value = {};\n\n for (const row of response.rows) {\n if (row.doc) {\n const tag = row.doc as Tag;\n availableTags.value.push(tag.name);\n if (tag.snippet) {\n tagSnippets.value[tag.name] = tag.snippet;\n }\n }\n }\n\n // Sort alphabetically\n availableTags.value.sort((a, b) => a.localeCompare(b));\n } catch (error) {\n console.error('[CourseTagFilterWidget] Failed to load tags:', error);\n availableTags.value = [];\n tagSnippets.value = {};\n } finally {\n loading.value = false;\n }\n }\n\n onMounted(() => {\n if (props.courseId) {\n loadTags();\n }\n });\n\n return {\n loading,\n availableTags,\n availableTagsForExclude,\n tagSnippets,\n localFilter,\n hasActiveFilter,\n fuzzyFilter,\n };\n },\n});\n</script>\n\n<style scoped>\n.course-tag-filter-widget {\n width: 100%;\n}\n\n.filter-summary {\n color: rgba(var(--v-theme-on-surface), 0.7);\n}\n</style>\n","<template>\n <div class=\"course-tag-filter-widget\">\n <v-autocomplete\n v-model=\"localFilter.include\"\n :items=\"availableTags\"\n :loading=\"loading\"\n :disabled=\"loading\"\n label=\"Include tags\"\n placeholder=\"Type to search tags...\"\n hint=\"Cards must have at least one of these tags\"\n persistent-hint\n multiple\n chips\n closable-chips\n clearable\n density=\"compact\"\n variant=\"outlined\"\n class=\"mb-2\"\n :custom-filter=\"fuzzyFilter\"\n no-data-text=\"No matching tags found\"\n >\n <template #chip=\"{ props, item }\">\n <v-chip\n v-bind=\"props\"\n color=\"primary\"\n variant=\"tonal\"\n size=\"small\"\n >\n {{ item.raw }}\n </v-chip>\n </template>\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\" :title=\"item.raw\">\n <template #subtitle v-if=\"tagSnippets[item.raw]\">\n {{ tagSnippets[item.raw] }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <v-autocomplete\n v-model=\"localFilter.exclude\"\n :items=\"availableTagsForExclude\"\n :loading=\"loading\"\n :disabled=\"loading\"\n label=\"Exclude tags\"\n placeholder=\"Type to search tags...\"\n hint=\"Cards with these tags will be excluded\"\n persistent-hint\n multiple\n chips\n closable-chips\n clearable\n density=\"compact\"\n variant=\"outlined\"\n :custom-filter=\"fuzzyFilter\"\n no-data-text=\"No matching tags found\"\n >\n <template #chip=\"{ props, item }\">\n <v-chip\n v-bind=\"props\"\n color=\"error\"\n variant=\"tonal\"\n size=\"small\"\n >\n {{ item.raw }}\n </v-chip>\n </template>\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\" :title=\"item.raw\">\n <template #subtitle v-if=\"tagSnippets[item.raw]\">\n {{ tagSnippets[item.raw] }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <div v-if=\"hasActiveFilter\" class=\"filter-summary text-caption mt-2\">\n <v-icon size=\"small\" color=\"info\" class=\"mr-1\">mdi-filter</v-icon>\n <span v-if=\"localFilter.include.length\">\n Including: {{ localFilter.include.join(', ') }}\n </span>\n <span v-if=\"localFilter.include.length && localFilter.exclude.length\"> · </span>\n <span v-if=\"localFilter.exclude.length\">\n Excluding: {{ localFilter.exclude.join(', ') }}\n </span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, ref, computed, watch, onMounted } from 'vue';\nimport { TagFilter, emptyTagFilter, hasActiveFilter as checkActiveFilter } from '@vue-skuilder/common';\nimport { getDataLayer, Tag } from '@vue-skuilder/db';\n\nexport default defineComponent({\n name: 'CourseTagFilterWidget',\n\n props: {\n courseId: {\n type: String,\n required: true,\n },\n modelValue: {\n type: Object as PropType<TagFilter | undefined>,\n default: undefined,\n },\n },\n\n emits: ['update:modelValue'],\n\n setup(props, { emit }) {\n const loading = ref(true);\n const availableTags = ref<string[]>([]);\n const tagSnippets = ref<Record<string, string>>({});\n\n // Local copy of the filter for editing\n const localFilter = ref<TagFilter>(\n props.modelValue ? { ...props.modelValue } : emptyTagFilter()\n );\n\n // Computed: tags available for exclude (not already in include)\n const availableTagsForExclude = computed(() => {\n return availableTags.value.filter(\n (tag) => !localFilter.value.include.includes(tag)\n );\n });\n\n // Computed: whether filter has any active constraints\n const hasActiveFilter = computed(() => {\n return checkActiveFilter(localFilter.value);\n });\n\n // Fuzzy filter function for autocomplete\n const fuzzyFilter = (itemText: string, queryText: string): boolean => {\n if (!queryText) return true;\n\n const query = queryText.toLowerCase();\n const text = itemText.toLowerCase();\n\n // Simple fuzzy match: all query characters appear in order\n let queryIndex = 0;\n for (let i = 0; i < text.length && queryIndex < query.length; i++) {\n if (text[i] === query[queryIndex]) {\n queryIndex++;\n }\n }\n return queryIndex === query.length;\n };\n\n // Watch for external changes to modelValue\n watch(\n () => props.modelValue,\n (newValue) => {\n if (newValue) {\n localFilter.value = { ...newValue };\n } else {\n localFilter.value = emptyTagFilter();\n }\n },\n { deep: true }\n );\n\n // Emit changes when local filter changes\n watch(\n localFilter,\n (newFilter) => {\n emit('update:modelValue', { ...newFilter });\n },\n { deep: true }\n );\n\n // Watch for courseId changes and reload tags\n watch(\n () => props.courseId,\n async (newCourseId) => {\n if (newCourseId) {\n await loadTags();\n }\n }\n );\n\n // Load tags for the course\n async function loadTags() {\n loading.value = true;\n try {\n const courseDB = getDataLayer().getCourseDB(props.courseId);\n const response = await courseDB.getCourseTagStubs();\n\n availableTags.value = [];\n tagSnippets.value = {};\n\n for (const row of response.rows) {\n if (row.doc) {\n const tag = row.doc as Tag;\n availableTags.value.push(tag.name);\n if (tag.snippet) {\n tagSnippets.value[tag.name] = tag.snippet;\n }\n }\n }\n\n // Sort alphabetically\n availableTags.value.sort((a, b) => a.localeCompare(b));\n } catch (error) {\n console.error('[CourseTagFilterWidget] Failed to load tags:', error);\n availableTags.value = [];\n tagSnippets.value = {};\n } finally {\n loading.value = false;\n }\n }\n\n onMounted(() => {\n if (props.courseId) {\n loadTags();\n }\n });\n\n return {\n loading,\n availableTags,\n availableTagsForExclude,\n tagSnippets,\n localFilter,\n hasActiveFilter,\n fuzzyFilter,\n };\n },\n});\n</script>\n\n<style scoped>\n.course-tag-filter-widget {\n width: 100%;\n}\n\n.filter-summary {\n color: rgba(var(--v-theme-on-surface), 0.7);\n}\n</style>\n","<template>\n <v-card :loading=\"loading\">\n <v-card-title>\n <v-icon start>mdi-tune</v-icon>\n Learning Preferences\n </v-card-title>\n\n <v-card-subtitle v-if=\"courseId\">\n Customize how content is presented in this course\n </v-card-subtitle>\n\n <v-card-text>\n <v-alert v-if=\"!courseId\" type=\"info\" variant=\"tonal\" class=\"mb-4\">\n Select a course to configure your learning preferences.\n </v-alert>\n\n <div v-else-if=\"!loading\">\n <!-- Tag Preferences Section -->\n <div class=\"mb-6\">\n <h3 class=\"text-subtitle-1 font-weight-bold mb-2\">\n <v-icon start size=\"small\">mdi-tune-variant</v-icon>\n Tag Preferences\n </h3>\n <p class=\"text-body-2 text-medium-emphasis mb-3\">\n Adjust how much you want to see cards with specific tags. 0 = exclude, 1 = neutral, higher = prefer more.\n </p>\n\n <!-- Tag autocomplete -->\n <v-autocomplete\n v-model=\"tagToAdd\"\n :items=\"availableTagsToAdd\"\n item-title=\"name\"\n item-value=\"name\"\n label=\"Add tag preference\"\n placeholder=\"Search tags...\"\n density=\"compact\"\n variant=\"outlined\"\n clearable\n hide-details\n class=\"mb-4\"\n @update:model-value=\"addTag\"\n >\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\">\n <template #subtitle>\n {{ item.raw.snippet }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <!-- Tag slider list -->\n <v-list v-if=\"tagNames.length > 0\" class=\"mt-4\">\n <v-list-item\n v-for=\"tagName in tagNames\"\n :key=\"tagName\"\n class=\"px-0\"\n >\n <template #default>\n <div class=\"d-flex align-center ga-3 w-100\">\n <!-- Tag name -->\n <v-chip\n size=\"small\"\n variant=\"tonal\"\n class=\"flex-shrink-0\"\n style=\"min-width: 120px;\"\n >\n {{ tagName }}\n </v-chip>\n\n <!-- Slider -->\n <v-slider\n :model-value=\"preferences.boost[tagName]\"\n :min=\"sliderConfigResolved.min\"\n :max=\"globalSliderMax\"\n :step=\"0.01\"\n hide-details\n class=\"flex-grow-1\"\n thumb-label\n @update:model-value=\"(val: number) => updateBoost(tagName, val)\"\n >\n <template #thumb-label=\"{ modelValue }\">\n {{ formatMultiplier(modelValue) }}\n </template>\n </v-slider>\n\n <!-- Current value display -->\n <span class=\"text-body-2 flex-shrink-0\" style=\"min-width: 60px; text-align: right;\">\n {{ formatMultiplier(preferences.boost[tagName]) }}\n </span>\n\n <!-- Expand range button (always visible) -->\n <v-btn\n icon=\"mdi-plus\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n :disabled=\"globalSliderMax >= sliderConfigResolved.absoluteMax\"\n @click=\"expandSliderRange(tagName)\"\n />\n\n <!-- Delete button -->\n <v-btn\n icon=\"mdi-delete\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n @click=\"removeTag(tagName)\"\n />\n </div>\n </template>\n </v-list-item>\n </v-list>\n\n <v-alert v-else type=\"info\" variant=\"tonal\" class=\"mt-4\">\n No tag preferences configured yet. Add tags above to get started.\n </v-alert>\n </div>\n\n <!-- Status / Feedback -->\n <v-alert v-if=\"saveError\" type=\"error\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveError = ''\">\n {{ saveError }}\n </v-alert>\n\n <v-alert v-if=\"saveSuccess\" type=\"success\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveSuccess = false\">\n Preferences saved successfully\n </v-alert>\n </div>\n\n <div v-else class=\"d-flex justify-center py-8\">\n <v-progress-circular indeterminate color=\"primary\" />\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"courseId && !loading\">\n <v-spacer />\n <v-btn\n variant=\"text\"\n :disabled=\"!hasChanges\"\n @click=\"resetToSaved\"\n >\n Reset\n </v-btn>\n <v-btn\n color=\"primary\"\n variant=\"flat\"\n :loading=\"saving\"\n :disabled=\"!hasChanges\"\n @click=\"savePreferences\"\n >\n Save Preferences\n </v-btn>\n </v-card-actions>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface, UserDBInterface } from '@vue-skuilder/db';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\n/**\n * User's tag preference state, matching the backend schema.\n */\ninterface UserTagPreferenceState {\n boost: Record<string, number>;\n updatedAt: string;\n}\n\n/**\n * Slider configuration for tag preferences.\n */\ninterface SliderConfig {\n min?: number; // default: 0\n startingMax?: number; // default: 2\n absoluteMax?: number; // default: 10\n}\n\nconst DEFAULT_SLIDER_CONFIG: Required<SliderConfig> = {\n min: 0,\n startingMax: 2,\n absoluteMax: 10,\n};\n\nconst STRATEGY_KEY = 'UserTagPreferenceFilter';\n\nexport default defineComponent({\n name: 'UserTagPreferences',\n\n props: {\n /**\n * Course ID to configure preferences for.\n * If not provided, component shows a prompt to select a course.\n */\n courseId: {\n type: String as PropType<string>,\n required: false,\n default: '',\n },\n\n /**\n * Slider configuration (min, startingMax, absoluteMax).\n * All fields optional, defaults: { min: 0, startingMax: 2, absoluteMax: 10 }\n */\n sliderConfig: {\n type: Object as PropType<SliderConfig>,\n required: false,\n default: () => ({}),\n },\n },\n\n emits: ['preferences-saved', 'preferences-changed'],\n\n data() {\n return {\n loading: true,\n saving: false,\n saveError: '',\n saveSuccess: false,\n\n // Current working state\n preferences: {\n boost: {} as Record<string, number>,\n },\n\n // Saved state for comparison\n savedPreferences: {\n boost: {} as Record<string, number>,\n },\n\n // Per-tag current max range (for dynamic expansion)\n tagMaxRanges: {} as Record<string, number>,\n\n // Available tags from course\n availableTags: [] as Tag[],\n\n // Autocomplete model\n tagToAdd: null as string | null,\n\n // DB references\n courseDB: null as CourseDBInterface | null,\n user: null as UserDBInterface | null,\n };\n },\n\n computed: {\n /**\n * Resolved slider config with defaults\n */\n sliderConfigResolved(): Required<SliderConfig> {\n return {\n min: this.sliderConfig.min ?? DEFAULT_SLIDER_CONFIG.min,\n startingMax: this.sliderConfig.startingMax ?? DEFAULT_SLIDER_CONFIG.startingMax,\n absoluteMax: this.sliderConfig.absoluteMax ?? DEFAULT_SLIDER_CONFIG.absoluteMax,\n };\n },\n\n /**\n * List of tag names that have preferences (sorted alphabetically)\n */\n tagNames(): string[] {\n return Object.keys(this.preferences.boost).sort();\n },\n\n /**\n * Global max for all sliders (highest tagMaxRanges value)\n * Ensures all sliders have the same visual scale\n */\n globalSliderMax(): number {\n const maxValues = Object.values(this.tagMaxRanges);\n if (maxValues.length === 0) {\n return this.sliderConfigResolved.startingMax;\n }\n return Math.max(...maxValues);\n },\n\n /**\n * Tags available to add (not already in preferences)\n */\n availableTagsToAdd(): Tag[] {\n const usedTags = new Set(Object.keys(this.preferences.boost));\n return this.availableTags.filter((t) => !usedTags.has(t.name));\n },\n\n /**\n * Check if current preferences differ from saved state\n */\n hasChanges(): boolean {\n const currentTags = Object.keys(this.preferences.boost).sort();\n const savedTags = Object.keys(this.savedPreferences.boost).sort();\n\n if (currentTags.length !== savedTags.length) {\n return true;\n }\n\n if (currentTags.join(',') !== savedTags.join(',')) {\n return true;\n }\n\n return currentTags.some(\n (tag) => this.preferences.boost[tag] !== this.savedPreferences.boost[tag]\n );\n },\n },\n\n watch: {\n courseId: {\n immediate: true,\n async handler(newCourseId: string) {\n if (newCourseId) {\n await this.loadPreferences();\n } else {\n this.loading = false;\n }\n },\n },\n },\n\n methods: {\n /**\n * Load preferences from strategy state and available tags from course\n */\n async loadPreferences() {\n this.loading = true;\n this.saveError = '';\n\n try {\n // Get user and course DB\n this.user = (await getCurrentUser()) ?? null;\n if (!this.user) {\n throw new Error('User not available');\n }\n\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n\n // Load available tags\n const tagStubs = await this.courseDB.getCourseTagStubs();\n this.availableTags = tagStubs.rows.map((r) => r.doc!).filter(Boolean);\n\n // Load saved preferences\n const state = await this.user.getStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY\n );\n\n if (state) {\n this.preferences.boost = { ...state.boost };\n } else {\n // No preferences yet - start fresh\n this.preferences.boost = {};\n }\n\n // Store saved state for comparison\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n // Initialize tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n // If saved value exceeds startingMax, expand range to accommodate it\n // (capped at absoluteMax)\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n } catch (e) {\n console.error('Failed to load preferences:', e);\n this.saveError = 'Failed to load preferences. Please try again.';\n } finally {\n this.loading = false;\n }\n },\n\n /**\n * Add a tag to preferences with default multiplier of 1.0\n */\n addTag(tagName: string | null) {\n if (tagName && !(tagName in this.preferences.boost)) {\n this.preferences.boost[tagName] = 1.0;\n this.tagMaxRanges[tagName] = this.sliderConfigResolved.startingMax;\n this.$emit('preferences-changed', this.preferences);\n }\n // Clear the autocomplete\n this.$nextTick(() => {\n this.tagToAdd = null;\n });\n },\n\n /**\n * Remove a tag from preferences\n */\n removeTag(tagName: string) {\n delete this.preferences.boost[tagName];\n delete this.tagMaxRanges[tagName];\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Update boost multiplier for a tag\n */\n updateBoost(tagName: string, value: number) {\n this.preferences.boost[tagName] = value;\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Expand the global slider range by 1 and move the triggering tag's slider to new max\n */\n expandSliderRange(tagName: string) {\n const currentGlobalMax = this.globalSliderMax;\n if (currentGlobalMax < this.sliderConfigResolved.absoluteMax) {\n const newGlobalMax = currentGlobalMax + 1;\n\n // Expand all tag ranges to the new global max\n // (ensures all sliders stay synchronized)\n Object.keys(this.tagMaxRanges).forEach((tag) => {\n this.tagMaxRanges[tag] = Math.max(this.tagMaxRanges[tag], newGlobalMax);\n });\n\n // Move the triggering tag's slider to new max\n this.preferences.boost[tagName] = newGlobalMax;\n this.$emit('preferences-changed', this.preferences);\n }\n },\n\n /**\n * Format multiplier for display\n */\n formatMultiplier(value: number): string {\n if (value === 0) {\n return '0x';\n }\n if (value < 0.1) {\n return value.toFixed(2) + 'x';\n }\n return value.toFixed(1) + 'x';\n },\n\n /**\n * Reset to last saved state\n */\n resetToSaved() {\n this.preferences.boost = { ...this.savedPreferences.boost };\n this.saveSuccess = false;\n this.saveError = '';\n\n // Reset tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n },\n\n /**\n * Save preferences to strategy state\n */\n async savePreferences() {\n if (!this.user || !this.courseId) {\n this.saveError = 'Unable to save - user or course not available';\n return;\n }\n\n this.saving = true;\n this.saveError = '';\n this.saveSuccess = false;\n\n try {\n const state: UserTagPreferenceState = {\n boost: { ...this.preferences.boost },\n updatedAt: new Date().toISOString(),\n };\n\n await this.user.putStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY,\n state\n );\n\n // Update saved state\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n this.saveSuccess = true;\n this.$emit('preferences-saved', state);\n } catch (e) {\n console.error('Failed to save preferences:', e);\n this.saveError = 'Failed to save preferences. Please try again.';\n } finally {\n this.saving = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n/* Additional styles if needed */\n</style>\n","<template>\n <v-card :loading=\"loading\">\n <v-card-title>\n <v-icon start>mdi-tune</v-icon>\n Learning Preferences\n </v-card-title>\n\n <v-card-subtitle v-if=\"courseId\">\n Customize how content is presented in this course\n </v-card-subtitle>\n\n <v-card-text>\n <v-alert v-if=\"!courseId\" type=\"info\" variant=\"tonal\" class=\"mb-4\">\n Select a course to configure your learning preferences.\n </v-alert>\n\n <div v-else-if=\"!loading\">\n <!-- Tag Preferences Section -->\n <div class=\"mb-6\">\n <h3 class=\"text-subtitle-1 font-weight-bold mb-2\">\n <v-icon start size=\"small\">mdi-tune-variant</v-icon>\n Tag Preferences\n </h3>\n <p class=\"text-body-2 text-medium-emphasis mb-3\">\n Adjust how much you want to see cards with specific tags. 0 = exclude, 1 = neutral, higher = prefer more.\n </p>\n\n <!-- Tag autocomplete -->\n <v-autocomplete\n v-model=\"tagToAdd\"\n :items=\"availableTagsToAdd\"\n item-title=\"name\"\n item-value=\"name\"\n label=\"Add tag preference\"\n placeholder=\"Search tags...\"\n density=\"compact\"\n variant=\"outlined\"\n clearable\n hide-details\n class=\"mb-4\"\n @update:model-value=\"addTag\"\n >\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\">\n <template #subtitle>\n {{ item.raw.snippet }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <!-- Tag slider list -->\n <v-list v-if=\"tagNames.length > 0\" class=\"mt-4\">\n <v-list-item\n v-for=\"tagName in tagNames\"\n :key=\"tagName\"\n class=\"px-0\"\n >\n <template #default>\n <div class=\"d-flex align-center ga-3 w-100\">\n <!-- Tag name -->\n <v-chip\n size=\"small\"\n variant=\"tonal\"\n class=\"flex-shrink-0\"\n style=\"min-width: 120px;\"\n >\n {{ tagName }}\n </v-chip>\n\n <!-- Slider -->\n <v-slider\n :model-value=\"preferences.boost[tagName]\"\n :min=\"sliderConfigResolved.min\"\n :max=\"globalSliderMax\"\n :step=\"0.01\"\n hide-details\n class=\"flex-grow-1\"\n thumb-label\n @update:model-value=\"(val: number) => updateBoost(tagName, val)\"\n >\n <template #thumb-label=\"{ modelValue }\">\n {{ formatMultiplier(modelValue) }}\n </template>\n </v-slider>\n\n <!-- Current value display -->\n <span class=\"text-body-2 flex-shrink-0\" style=\"min-width: 60px; text-align: right;\">\n {{ formatMultiplier(preferences.boost[tagName]) }}\n </span>\n\n <!-- Expand range button (always visible) -->\n <v-btn\n icon=\"mdi-plus\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n :disabled=\"globalSliderMax >= sliderConfigResolved.absoluteMax\"\n @click=\"expandSliderRange(tagName)\"\n />\n\n <!-- Delete button -->\n <v-btn\n icon=\"mdi-delete\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n @click=\"removeTag(tagName)\"\n />\n </div>\n </template>\n </v-list-item>\n </v-list>\n\n <v-alert v-else type=\"info\" variant=\"tonal\" class=\"mt-4\">\n No tag preferences configured yet. Add tags above to get started.\n </v-alert>\n </div>\n\n <!-- Status / Feedback -->\n <v-alert v-if=\"saveError\" type=\"error\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveError = ''\">\n {{ saveError }}\n </v-alert>\n\n <v-alert v-if=\"saveSuccess\" type=\"success\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveSuccess = false\">\n Preferences saved successfully\n </v-alert>\n </div>\n\n <div v-else class=\"d-flex justify-center py-8\">\n <v-progress-circular indeterminate color=\"primary\" />\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"courseId && !loading\">\n <v-spacer />\n <v-btn\n variant=\"text\"\n :disabled=\"!hasChanges\"\n @click=\"resetToSaved\"\n >\n Reset\n </v-btn>\n <v-btn\n color=\"primary\"\n variant=\"flat\"\n :loading=\"saving\"\n :disabled=\"!hasChanges\"\n @click=\"savePreferences\"\n >\n Save Preferences\n </v-btn>\n </v-card-actions>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface, UserDBInterface } from '@vue-skuilder/db';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\n/**\n * User's tag preference state, matching the backend schema.\n */\ninterface UserTagPreferenceState {\n boost: Record<string, number>;\n updatedAt: string;\n}\n\n/**\n * Slider configuration for tag preferences.\n */\ninterface SliderConfig {\n min?: number; // default: 0\n startingMax?: number; // default: 2\n absoluteMax?: number; // default: 10\n}\n\nconst DEFAULT_SLIDER_CONFIG: Required<SliderConfig> = {\n min: 0,\n startingMax: 2,\n absoluteMax: 10,\n};\n\nconst STRATEGY_KEY = 'UserTagPreferenceFilter';\n\nexport default defineComponent({\n name: 'UserTagPreferences',\n\n props: {\n /**\n * Course ID to configure preferences for.\n * If not provided, component shows a prompt to select a course.\n */\n courseId: {\n type: String as PropType<string>,\n required: false,\n default: '',\n },\n\n /**\n * Slider configuration (min, startingMax, absoluteMax).\n * All fields optional, defaults: { min: 0, startingMax: 2, absoluteMax: 10 }\n */\n sliderConfig: {\n type: Object as PropType<SliderConfig>,\n required: false,\n default: () => ({}),\n },\n },\n\n emits: ['preferences-saved', 'preferences-changed'],\n\n data() {\n return {\n loading: true,\n saving: false,\n saveError: '',\n saveSuccess: false,\n\n // Current working state\n preferences: {\n boost: {} as Record<string, number>,\n },\n\n // Saved state for comparison\n savedPreferences: {\n boost: {} as Record<string, number>,\n },\n\n // Per-tag current max range (for dynamic expansion)\n tagMaxRanges: {} as Record<string, number>,\n\n // Available tags from course\n availableTags: [] as Tag[],\n\n // Autocomplete model\n tagToAdd: null as string | null,\n\n // DB references\n courseDB: null as CourseDBInterface | null,\n user: null as UserDBInterface | null,\n };\n },\n\n computed: {\n /**\n * Resolved slider config with defaults\n */\n sliderConfigResolved(): Required<SliderConfig> {\n return {\n min: this.sliderConfig.min ?? DEFAULT_SLIDER_CONFIG.min,\n startingMax: this.sliderConfig.startingMax ?? DEFAULT_SLIDER_CONFIG.startingMax,\n absoluteMax: this.sliderConfig.absoluteMax ?? DEFAULT_SLIDER_CONFIG.absoluteMax,\n };\n },\n\n /**\n * List of tag names that have preferences (sorted alphabetically)\n */\n tagNames(): string[] {\n return Object.keys(this.preferences.boost).sort();\n },\n\n /**\n * Global max for all sliders (highest tagMaxRanges value)\n * Ensures all sliders have the same visual scale\n */\n globalSliderMax(): number {\n const maxValues = Object.values(this.tagMaxRanges);\n if (maxValues.length === 0) {\n return this.sliderConfigResolved.startingMax;\n }\n return Math.max(...maxValues);\n },\n\n /**\n * Tags available to add (not already in preferences)\n */\n availableTagsToAdd(): Tag[] {\n const usedTags = new Set(Object.keys(this.preferences.boost));\n return this.availableTags.filter((t) => !usedTags.has(t.name));\n },\n\n /**\n * Check if current preferences differ from saved state\n */\n hasChanges(): boolean {\n const currentTags = Object.keys(this.preferences.boost).sort();\n const savedTags = Object.keys(this.savedPreferences.boost).sort();\n\n if (currentTags.length !== savedTags.length) {\n return true;\n }\n\n if (currentTags.join(',') !== savedTags.join(',')) {\n return true;\n }\n\n return currentTags.some(\n (tag) => this.preferences.boost[tag] !== this.savedPreferences.boost[tag]\n );\n },\n },\n\n watch: {\n courseId: {\n immediate: true,\n async handler(newCourseId: string) {\n if (newCourseId) {\n await this.loadPreferences();\n } else {\n this.loading = false;\n }\n },\n },\n },\n\n methods: {\n /**\n * Load preferences from strategy state and available tags from course\n */\n async loadPreferences() {\n this.loading = true;\n this.saveError = '';\n\n try {\n // Get user and course DB\n this.user = (await getCurrentUser()) ?? null;\n if (!this.user) {\n throw new Error('User not available');\n }\n\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n\n // Load available tags\n const tagStubs = await this.courseDB.getCourseTagStubs();\n this.availableTags = tagStubs.rows.map((r) => r.doc!).filter(Boolean);\n\n // Load saved preferences\n const state = await this.user.getStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY\n );\n\n if (state) {\n this.preferences.boost = { ...state.boost };\n } else {\n // No preferences yet - start fresh\n this.preferences.boost = {};\n }\n\n // Store saved state for comparison\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n // Initialize tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n // If saved value exceeds startingMax, expand range to accommodate it\n // (capped at absoluteMax)\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n } catch (e) {\n console.error('Failed to load preferences:', e);\n this.saveError = 'Failed to load preferences. Please try again.';\n } finally {\n this.loading = false;\n }\n },\n\n /**\n * Add a tag to preferences with default multiplier of 1.0\n */\n addTag(tagName: string | null) {\n if (tagName && !(tagName in this.preferences.boost)) {\n this.preferences.boost[tagName] = 1.0;\n this.tagMaxRanges[tagName] = this.sliderConfigResolved.startingMax;\n this.$emit('preferences-changed', this.preferences);\n }\n // Clear the autocomplete\n this.$nextTick(() => {\n this.tagToAdd = null;\n });\n },\n\n /**\n * Remove a tag from preferences\n */\n removeTag(tagName: string) {\n delete this.preferences.boost[tagName];\n delete this.tagMaxRanges[tagName];\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Update boost multiplier for a tag\n */\n updateBoost(tagName: string, value: number) {\n this.preferences.boost[tagName] = value;\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Expand the global slider range by 1 and move the triggering tag's slider to new max\n */\n expandSliderRange(tagName: string) {\n const currentGlobalMax = this.globalSliderMax;\n if (currentGlobalMax < this.sliderConfigResolved.absoluteMax) {\n const newGlobalMax = currentGlobalMax + 1;\n\n // Expand all tag ranges to the new global max\n // (ensures all sliders stay synchronized)\n Object.keys(this.tagMaxRanges).forEach((tag) => {\n this.tagMaxRanges[tag] = Math.max(this.tagMaxRanges[tag], newGlobalMax);\n });\n\n // Move the triggering tag's slider to new max\n this.preferences.boost[tagName] = newGlobalMax;\n this.$emit('preferences-changed', this.preferences);\n }\n },\n\n /**\n * Format multiplier for display\n */\n formatMultiplier(value: number): string {\n if (value === 0) {\n return '0x';\n }\n if (value < 0.1) {\n return value.toFixed(2) + 'x';\n }\n return value.toFixed(1) + 'x';\n },\n\n /**\n * Reset to last saved state\n */\n resetToSaved() {\n this.preferences.boost = { ...this.savedPreferences.boost };\n this.saveSuccess = false;\n this.saveError = '';\n\n // Reset tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n },\n\n /**\n * Save preferences to strategy state\n */\n async savePreferences() {\n if (!this.user || !this.courseId) {\n this.saveError = 'Unable to save - user or course not available';\n return;\n }\n\n this.saving = true;\n this.saveError = '';\n this.saveSuccess = false;\n\n try {\n const state: UserTagPreferenceState = {\n boost: { ...this.preferences.boost },\n updatedAt: new Date().toISOString(),\n };\n\n await this.user.putStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY,\n state\n );\n\n // Update saved state\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n this.saveSuccess = true;\n this.$emit('preferences-saved', state);\n } catch (e) {\n console.error('Failed to save preferences:', e);\n this.saveError = 'Failed to save preferences. Please try again.';\n } finally {\n this.saving = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n/* Additional styles if needed */\n</style>\n"],"x_google_ignoreList":[26],"mappings":"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"}
1
+ {"version":3,"file":"common-ui.es.js","names":[],"sources":["../src/components/HeatMap.vue","../src/components/HeatMap.vue","../src/components/SkMouseTrap.vue","../src/components/SkMouseTrap.vue","../src/components/SkMouseTrapToolTip.vue","../src/components/SkMouseTrapToolTip.vue","../src/components/SnackbarService.ts","../src/components/SnackbarService.vue","../src/components/SnackbarService.vue","../src/components/PaginatingToolbar.vue","../src/components/PaginatingToolbar.vue","../src/components/CardSearch.vue","../src/components/CardSearch.vue","../src/components/CardSearchResults.vue","../src/components/CardSearchResults.vue","../src/components/CardHistoryViewer.vue","../src/components/CardHistoryViewer.vue","../src/composables/Displayable.ts","../src/services/authAPI.ts","../src/composables/useEntitlements.ts","../src/components/StudySessionTimer.vue","../src/components/StudySessionTimer.vue","../src/components/cardRendering/CardViewer.vue","../src/components/cardRendering/CardViewer.vue","../src/components/SessionControllerDebug.vue","../src/components/SessionControllerDebug.vue","../../../node_modules/canvas-confetti/dist/confetti.module.mjs","../src/components/StudySession.vue","../src/components/StudySession.vue","../src/components/studentInputs/MultipleChoiceOption.vue","../src/components/studentInputs/MultipleChoiceOption.vue","../src/components/studentInputs/RadioMultipleChoice.vue","../src/components/studentInputs/RadioMultipleChoice.vue","../src/components/studentInputs/TrueFalse.vue","../src/components/studentInputs/TrueFalse.vue","../src/components/studentInputs/UserInputNumber.vue","../src/components/studentInputs/UserInputNumber.vue","../src/components/cardRendering/CardLoader.vue","../src/components/cardRendering/CardLoader.vue","../src/stores/useAuthStore.ts","../src/stores/useConfigStore.ts","../src/composables/useAuthUI.ts","../src/components/auth/UserChip.vue","../src/components/auth/UserChip.vue","../src/components/auth/UserLogin.vue","../src/components/auth/UserLogin.vue","../src/utils/passwordValidation.ts","../src/components/auth/UserRegistration.vue","../src/components/auth/UserRegistration.vue","../src/components/auth/RequestPasswordReset.vue","../src/components/auth/RequestPasswordReset.vue","../src/components/auth/UserLoginAndRegistrationContainer.vue","../src/components/auth/UserLoginAndRegistrationContainer.vue","../src/components/auth/VerifyEmail.vue","../src/components/auth/VerifyEmail.vue","../src/components/auth/ResetPassword.vue","../src/components/auth/ResetPassword.vue","../src/components/TagsInput.vue","../src/components/TagsInput.vue","../src/components/CourseCardBrowser.vue","../src/components/CourseCardBrowser.vue","../src/components/CourseInformation.vue","../src/components/CourseInformation.vue","../src/components/CardBrowser.vue","../src/components/CardBrowser.vue","../src/components/CourseTagFilterWidget.vue","../src/components/CourseTagFilterWidget.vue","../src/components/UserTagPreferences.vue","../src/components/UserTagPreferences.vue"],"sourcesContent":["<template>\n <div>\n <svg :width=\"width\" :height=\"height\">\n <g\n v-for=\"(week, weekIndex) in weeks\"\n :key=\"weekIndex\"\n :transform=\"`translate(${weekIndex * (cellSize + cellMargin)}, 0)`\"\n >\n <rect\n v-for=\"(day, dayIndex) in week\"\n :key=\"day.date\"\n :x=\"0\"\n :y=\"dayIndex * (cellSize + cellMargin)\"\n :width=\"cellSize\"\n :height=\"cellSize\"\n :fill=\"getColor(day.count)\"\n @mouseover=\"showTooltip(day, $event)\"\n @mouseout=\"hideTooltip\"\n />\n </g>\n </svg>\n <div v-if=\"tooltipData\" class=\"tooltip\" :style=\"tooltipStyle\">\n {{ tooltipData.count }} review{{ tooltipData.count !== 1 ? 's' : '' }} on {{ toDateString(tooltipData.date) }}\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport moment from 'moment';\nimport { DayData, Color, ActivityRecord } from './HeatMap.types';\n\nexport default defineComponent({\n name: 'HeatMap',\n\n props: {\n // Accept activity records directly as a prop\n activityRecords: {\n type: Array as PropType<ActivityRecord[]>,\n default: () => [],\n },\n // Accept a function that can retrieve activity records\n activityRecordsGetter: {\n type: Function as PropType<() => Promise<ActivityRecord[]>>,\n default: null,\n },\n // Customize colors\n inactiveColor: {\n type: Object as PropType<Color>,\n default: () => ({ h: 0, s: 0, l: 0.9 }),\n },\n activeColor: {\n type: Object as PropType<Color>,\n default: () => ({ h: 155, s: 1, l: 0.5 }),\n },\n // Customize size\n cellSize: {\n type: Number,\n default: 12,\n },\n cellMargin: {\n type: Number,\n default: 3,\n },\n // Enable/disable seasonal colors\n enableSeasonalColors: {\n type: Boolean,\n default: true,\n },\n },\n\n data() {\n return {\n isLoading: false,\n localActivityRecords: [] as ActivityRecord[],\n heatmapData: {} as { [key: string]: number },\n weeks: [] as DayData[][],\n tooltipData: null as DayData | null,\n tooltipStyle: {} as { [key: string]: string },\n maxInRange: 0,\n };\n },\n\n computed: {\n width(): number {\n return 53 * (this.cellSize + this.cellMargin);\n },\n height(): number {\n return 7 * (this.cellSize + this.cellMargin);\n },\n effectiveActivityRecords(): ActivityRecord[] {\n const useLocal = Array.isArray(this.localActivityRecords) && this.localActivityRecords.length > 0;\n const records = useLocal ? this.localActivityRecords : this.activityRecords || [];\n console.log('Using effectiveActivityRecords, count:', records.length, 'source:', useLocal ? 'local' : 'prop');\n return records;\n },\n },\n\n watch: {\n activityRecords: {\n handler() {\n this.processRecords();\n this.createWeeksData();\n },\n immediate: true,\n },\n },\n\n async created() {\n if (this.activityRecordsGetter) {\n try {\n this.isLoading = true;\n console.log('Fetching activity records using getter...');\n \n // Ensure the getter is called safely with proper error handling\n let result = await this.activityRecordsGetter();\n \n // Handle the result - ensure it's an array of activity records\n if (Array.isArray(result)) {\n // Filter out records with invalid timestamps before processing\n this.localActivityRecords = result.filter(record => {\n if (!record || !record.timeStamp) return false;\n \n // Basic validation check for timestamps\n try {\n const m = moment(record.timeStamp);\n return m.isValid() && m.year() > 2000 && m.year() < 2100;\n } catch (e) {\n return false;\n }\n });\n \n console.log(`Received ${result.length} records, ${this.localActivityRecords.length} valid after filtering`);\n \n // Process the loaded records\n this.processRecords();\n this.createWeeksData();\n } else {\n console.error('Activity records getter did not return an array:', result);\n this.localActivityRecords = [];\n }\n } catch (error) {\n console.error('Error fetching activity records:', error);\n this.localActivityRecords = [];\n } finally {\n this.isLoading = false;\n }\n } else {\n console.log('No activityRecordsGetter provided, using direct activityRecords prop');\n }\n },\n\n methods: {\n toDateString(d: string): string {\n const m = moment(d);\n return moment.months()[m.month()] + ' ' + m.date();\n },\n\n processRecords() {\n const records = this.effectiveActivityRecords || [];\n console.log(`Processing ${records.length} records`);\n\n const data: { [key: string]: number } = {};\n\n if (records.length === 0) {\n console.log('No records to process');\n this.heatmapData = data;\n return;\n }\n\n // Sample logging of a few records to understand structure without flooding console\n const uniqueDates = new Set<string>();\n const dateDistribution: Record<string, number> = {};\n let validCount = 0;\n let invalidCount = 0;\n \n for (let i = 0; i < records.length; i++) {\n const record = records[i];\n \n if (!record || typeof record !== 'object' || !record.timeStamp) {\n invalidCount++;\n continue;\n }\n \n try {\n // Attempt to normalize the timestamp\n let normalizedDate: string;\n \n if (typeof record.timeStamp === 'string') {\n // For ISO strings, parse directly with moment\n normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');\n } else if (typeof record.timeStamp === 'number') {\n // For numeric timestamps, use Date constructor then moment\n normalizedDate = moment(new Date(record.timeStamp)).format('YYYY-MM-DD');\n } else if (typeof record.timeStamp === 'object') {\n // For objects (like Moment), try toString() or direct parsing\n if (typeof record.timeStamp.format === 'function') {\n // It's likely a Moment object\n normalizedDate = record.timeStamp.format('YYYY-MM-DD');\n } else if (record.timeStamp instanceof Date) {\n normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');\n } else {\n // Try to parse it as a string representation\n normalizedDate = moment(String(record.timeStamp)).format('YYYY-MM-DD');\n }\n } else {\n // Unhandled type\n invalidCount++;\n continue;\n }\n \n // Verify the date is valid before using it\n if (moment(normalizedDate, 'YYYY-MM-DD', true).isValid()) {\n data[normalizedDate] = (data[normalizedDate] || 0) + 1;\n uniqueDates.add(normalizedDate);\n \n // Track distribution by month for debugging\n const month = normalizedDate.substring(0, 7); // YYYY-MM\n dateDistribution[month] = (dateDistribution[month] || 0) + 1;\n \n validCount++;\n } else {\n invalidCount++;\n }\n } catch (e) {\n invalidCount++;\n }\n }\n \n // Log summary statistics\n console.log(`Processed ${validCount} valid dates, ${invalidCount} invalid dates`);\n console.log(`Found ${uniqueDates.size} unique dates`);\n console.log('Date distribution by month:', dateDistribution);\n\n this.heatmapData = data;\n },\n\n createWeeksData() {\n // Reset weeks and max count\n this.weeks = [];\n this.maxInRange = 0;\n \n const end = moment();\n const start = end.clone().subtract(52, 'weeks');\n const day = start.clone().startOf('week');\n \n console.log('Creating weeks data from', start.format('YYYY-MM-DD'), 'to', end.format('YYYY-MM-DD'));\n \n // Ensure we have data to display\n if (Object.keys(this.heatmapData).length === 0) {\n console.log('No heatmap data available to display');\n }\n\n // For debugging, log some sample dates from the heatmap data\n const sampleDates = Object.keys(this.heatmapData).slice(0, 5);\n console.log('Sample dates in heatmap data:', sampleDates);\n \n // Build the week data structure\n while (day.isSameOrBefore(end)) {\n const weekData: DayData[] = [];\n for (let i = 0; i < 7; i++) {\n const date = day.format('YYYY-MM-DD');\n const count = this.heatmapData[date] || 0;\n const dayData: DayData = {\n date,\n count,\n };\n weekData.push(dayData);\n if (dayData.count > this.maxInRange) {\n this.maxInRange = dayData.count;\n }\n\n day.add(1, 'day');\n }\n this.weeks.push(weekData);\n }\n \n console.log('Weeks data created, maxInRange:', this.maxInRange);\n \n // Calculate summary stats for display\n let totalDaysWithActivity = 0;\n let totalActivity = 0;\n \n Object.values(this.heatmapData).forEach(count => {\n totalDaysWithActivity++;\n totalActivity += count;\n });\n \n console.log(`Activity summary: ${totalActivity} activities across ${totalDaysWithActivity} days`);\n },\n\n getColor(count: number): string {\n if (this.maxInRange === 0) return this.hslToString(this.inactiveColor);\n\n const t = count === 0 ? 0 : Math.min((2 * count) / this.maxInRange, 1);\n\n let seasonalColor: Color = this.activeColor;\n\n if (this.enableSeasonalColors) {\n const now = moment();\n if (now.month() === 11 && now.date() >= 5) {\n // Christmas colors\n seasonalColor = Math.random() > 0.5 ? { h: 350, s: 0.8, l: 0.5 } : { h: 135, s: 0.8, l: 0.4 };\n } else if (now.month() === 9 && now.date() >= 25) {\n // Halloween colors\n seasonalColor =\n Math.random() > 0.5\n ? { h: 0, s: 0, l: 0 }\n : Math.random() > 0.5\n ? { h: 30, s: 1, l: 0.5 }\n : { h: 270, s: 1, l: 0.5 };\n }\n }\n\n const h = seasonalColor.h;\n const s = this.interpolate(this.inactiveColor.s, seasonalColor.s, t);\n const l = this.interpolate(this.inactiveColor.l, seasonalColor.l, t);\n\n return this.hslToString({ h, s, l });\n },\n\n interpolate(start: number, end: number, t: number): number {\n return start + (end - start) * t;\n },\n\n hslToString(color: Color): string {\n return `hsl(${color.h}, ${color.s * 100}%, ${color.l * 100}%)`;\n },\n\n showTooltip(day: DayData, event: MouseEvent) {\n this.tooltipData = day;\n this.tooltipStyle = {\n position: 'absolute',\n left: `${event.pageX + 10}px`,\n top: `${event.pageY + 10}px`,\n };\n },\n\n hideTooltip() {\n this.tooltipData = null;\n },\n },\n});\n</script>\n\n<style scoped>\n.tooltip {\n background-color: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 5px;\n border-radius: 3px;\n font-size: 12px;\n}\n</style>\n","<template>\n <div>\n <svg :width=\"width\" :height=\"height\">\n <g\n v-for=\"(week, weekIndex) in weeks\"\n :key=\"weekIndex\"\n :transform=\"`translate(${weekIndex * (cellSize + cellMargin)}, 0)`\"\n >\n <rect\n v-for=\"(day, dayIndex) in week\"\n :key=\"day.date\"\n :x=\"0\"\n :y=\"dayIndex * (cellSize + cellMargin)\"\n :width=\"cellSize\"\n :height=\"cellSize\"\n :fill=\"getColor(day.count)\"\n @mouseover=\"showTooltip(day, $event)\"\n @mouseout=\"hideTooltip\"\n />\n </g>\n </svg>\n <div v-if=\"tooltipData\" class=\"tooltip\" :style=\"tooltipStyle\">\n {{ tooltipData.count }} review{{ tooltipData.count !== 1 ? 's' : '' }} on {{ toDateString(tooltipData.date) }}\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport moment from 'moment';\nimport { DayData, Color, ActivityRecord } from './HeatMap.types';\n\nexport default defineComponent({\n name: 'HeatMap',\n\n props: {\n // Accept activity records directly as a prop\n activityRecords: {\n type: Array as PropType<ActivityRecord[]>,\n default: () => [],\n },\n // Accept a function that can retrieve activity records\n activityRecordsGetter: {\n type: Function as PropType<() => Promise<ActivityRecord[]>>,\n default: null,\n },\n // Customize colors\n inactiveColor: {\n type: Object as PropType<Color>,\n default: () => ({ h: 0, s: 0, l: 0.9 }),\n },\n activeColor: {\n type: Object as PropType<Color>,\n default: () => ({ h: 155, s: 1, l: 0.5 }),\n },\n // Customize size\n cellSize: {\n type: Number,\n default: 12,\n },\n cellMargin: {\n type: Number,\n default: 3,\n },\n // Enable/disable seasonal colors\n enableSeasonalColors: {\n type: Boolean,\n default: true,\n },\n },\n\n data() {\n return {\n isLoading: false,\n localActivityRecords: [] as ActivityRecord[],\n heatmapData: {} as { [key: string]: number },\n weeks: [] as DayData[][],\n tooltipData: null as DayData | null,\n tooltipStyle: {} as { [key: string]: string },\n maxInRange: 0,\n };\n },\n\n computed: {\n width(): number {\n return 53 * (this.cellSize + this.cellMargin);\n },\n height(): number {\n return 7 * (this.cellSize + this.cellMargin);\n },\n effectiveActivityRecords(): ActivityRecord[] {\n const useLocal = Array.isArray(this.localActivityRecords) && this.localActivityRecords.length > 0;\n const records = useLocal ? this.localActivityRecords : this.activityRecords || [];\n console.log('Using effectiveActivityRecords, count:', records.length, 'source:', useLocal ? 'local' : 'prop');\n return records;\n },\n },\n\n watch: {\n activityRecords: {\n handler() {\n this.processRecords();\n this.createWeeksData();\n },\n immediate: true,\n },\n },\n\n async created() {\n if (this.activityRecordsGetter) {\n try {\n this.isLoading = true;\n console.log('Fetching activity records using getter...');\n \n // Ensure the getter is called safely with proper error handling\n let result = await this.activityRecordsGetter();\n \n // Handle the result - ensure it's an array of activity records\n if (Array.isArray(result)) {\n // Filter out records with invalid timestamps before processing\n this.localActivityRecords = result.filter(record => {\n if (!record || !record.timeStamp) return false;\n \n // Basic validation check for timestamps\n try {\n const m = moment(record.timeStamp);\n return m.isValid() && m.year() > 2000 && m.year() < 2100;\n } catch (e) {\n return false;\n }\n });\n \n console.log(`Received ${result.length} records, ${this.localActivityRecords.length} valid after filtering`);\n \n // Process the loaded records\n this.processRecords();\n this.createWeeksData();\n } else {\n console.error('Activity records getter did not return an array:', result);\n this.localActivityRecords = [];\n }\n } catch (error) {\n console.error('Error fetching activity records:', error);\n this.localActivityRecords = [];\n } finally {\n this.isLoading = false;\n }\n } else {\n console.log('No activityRecordsGetter provided, using direct activityRecords prop');\n }\n },\n\n methods: {\n toDateString(d: string): string {\n const m = moment(d);\n return moment.months()[m.month()] + ' ' + m.date();\n },\n\n processRecords() {\n const records = this.effectiveActivityRecords || [];\n console.log(`Processing ${records.length} records`);\n\n const data: { [key: string]: number } = {};\n\n if (records.length === 0) {\n console.log('No records to process');\n this.heatmapData = data;\n return;\n }\n\n // Sample logging of a few records to understand structure without flooding console\n const uniqueDates = new Set<string>();\n const dateDistribution: Record<string, number> = {};\n let validCount = 0;\n let invalidCount = 0;\n \n for (let i = 0; i < records.length; i++) {\n const record = records[i];\n \n if (!record || typeof record !== 'object' || !record.timeStamp) {\n invalidCount++;\n continue;\n }\n \n try {\n // Attempt to normalize the timestamp\n let normalizedDate: string;\n \n if (typeof record.timeStamp === 'string') {\n // For ISO strings, parse directly with moment\n normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');\n } else if (typeof record.timeStamp === 'number') {\n // For numeric timestamps, use Date constructor then moment\n normalizedDate = moment(new Date(record.timeStamp)).format('YYYY-MM-DD');\n } else if (typeof record.timeStamp === 'object') {\n // For objects (like Moment), try toString() or direct parsing\n if (typeof record.timeStamp.format === 'function') {\n // It's likely a Moment object\n normalizedDate = record.timeStamp.format('YYYY-MM-DD');\n } else if (record.timeStamp instanceof Date) {\n normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');\n } else {\n // Try to parse it as a string representation\n normalizedDate = moment(String(record.timeStamp)).format('YYYY-MM-DD');\n }\n } else {\n // Unhandled type\n invalidCount++;\n continue;\n }\n \n // Verify the date is valid before using it\n if (moment(normalizedDate, 'YYYY-MM-DD', true).isValid()) {\n data[normalizedDate] = (data[normalizedDate] || 0) + 1;\n uniqueDates.add(normalizedDate);\n \n // Track distribution by month for debugging\n const month = normalizedDate.substring(0, 7); // YYYY-MM\n dateDistribution[month] = (dateDistribution[month] || 0) + 1;\n \n validCount++;\n } else {\n invalidCount++;\n }\n } catch (e) {\n invalidCount++;\n }\n }\n \n // Log summary statistics\n console.log(`Processed ${validCount} valid dates, ${invalidCount} invalid dates`);\n console.log(`Found ${uniqueDates.size} unique dates`);\n console.log('Date distribution by month:', dateDistribution);\n\n this.heatmapData = data;\n },\n\n createWeeksData() {\n // Reset weeks and max count\n this.weeks = [];\n this.maxInRange = 0;\n \n const end = moment();\n const start = end.clone().subtract(52, 'weeks');\n const day = start.clone().startOf('week');\n \n console.log('Creating weeks data from', start.format('YYYY-MM-DD'), 'to', end.format('YYYY-MM-DD'));\n \n // Ensure we have data to display\n if (Object.keys(this.heatmapData).length === 0) {\n console.log('No heatmap data available to display');\n }\n\n // For debugging, log some sample dates from the heatmap data\n const sampleDates = Object.keys(this.heatmapData).slice(0, 5);\n console.log('Sample dates in heatmap data:', sampleDates);\n \n // Build the week data structure\n while (day.isSameOrBefore(end)) {\n const weekData: DayData[] = [];\n for (let i = 0; i < 7; i++) {\n const date = day.format('YYYY-MM-DD');\n const count = this.heatmapData[date] || 0;\n const dayData: DayData = {\n date,\n count,\n };\n weekData.push(dayData);\n if (dayData.count > this.maxInRange) {\n this.maxInRange = dayData.count;\n }\n\n day.add(1, 'day');\n }\n this.weeks.push(weekData);\n }\n \n console.log('Weeks data created, maxInRange:', this.maxInRange);\n \n // Calculate summary stats for display\n let totalDaysWithActivity = 0;\n let totalActivity = 0;\n \n Object.values(this.heatmapData).forEach(count => {\n totalDaysWithActivity++;\n totalActivity += count;\n });\n \n console.log(`Activity summary: ${totalActivity} activities across ${totalDaysWithActivity} days`);\n },\n\n getColor(count: number): string {\n if (this.maxInRange === 0) return this.hslToString(this.inactiveColor);\n\n const t = count === 0 ? 0 : Math.min((2 * count) / this.maxInRange, 1);\n\n let seasonalColor: Color = this.activeColor;\n\n if (this.enableSeasonalColors) {\n const now = moment();\n if (now.month() === 11 && now.date() >= 5) {\n // Christmas colors\n seasonalColor = Math.random() > 0.5 ? { h: 350, s: 0.8, l: 0.5 } : { h: 135, s: 0.8, l: 0.4 };\n } else if (now.month() === 9 && now.date() >= 25) {\n // Halloween colors\n seasonalColor =\n Math.random() > 0.5\n ? { h: 0, s: 0, l: 0 }\n : Math.random() > 0.5\n ? { h: 30, s: 1, l: 0.5 }\n : { h: 270, s: 1, l: 0.5 };\n }\n }\n\n const h = seasonalColor.h;\n const s = this.interpolate(this.inactiveColor.s, seasonalColor.s, t);\n const l = this.interpolate(this.inactiveColor.l, seasonalColor.l, t);\n\n return this.hslToString({ h, s, l });\n },\n\n interpolate(start: number, end: number, t: number): number {\n return start + (end - start) * t;\n },\n\n hslToString(color: Color): string {\n return `hsl(${color.h}, ${color.s * 100}%, ${color.l * 100}%)`;\n },\n\n showTooltip(day: DayData, event: MouseEvent) {\n this.tooltipData = day;\n this.tooltipStyle = {\n position: 'absolute',\n left: `${event.pageX + 10}px`,\n top: `${event.pageY + 10}px`,\n };\n },\n\n hideTooltip() {\n this.tooltipData = null;\n },\n },\n});\n</script>\n\n<style scoped>\n.tooltip {\n background-color: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 5px;\n border-radius: 3px;\n font-size: 12px;\n}\n</style>\n","<template>\n <v-dialog v-if=\"display\" max-width=\"500px\" transition=\"dialog-transition\">\n <template #activator=\"{ props }\">\n <v-btn icon color=\"primary\" v-bind=\"props\">\n <v-icon>mdi-keyboard</v-icon>\n </v-btn>\n </template>\n\n <v-card>\n <v-toolbar color=\"secondary\" dark dense>\n <v-toolbar-title class=\"text-subtitle-1\">Shortcut keys:</v-toolbar-title>\n </v-toolbar>\n <v-list dense>\n <v-list-item\n v-for=\"hk in commands\"\n :key=\"Array.isArray(hk.hotkey) ? hk.hotkey.join(',') : hk.hotkey\"\n class=\"py-1\"\n >\n <v-btn variant=\"outlined\" color=\"primary\" class=\"text-white\" size=\"small\">\n {{ Array.isArray(hk.hotkey) ? hk.hotkey[0] : hk.hotkey }}\n </v-btn>\n <v-spacer></v-spacer>\n <span class=\"text-caption ml-2\">{{ hk.command }}</span>\n </v-list-item>\n </v-list>\n </v-card>\n </v-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';\nimport { SkMouseTrapProps } from './SkMouseTrap.types';\n\nexport default defineComponent({\n name: 'SkMouseTrap',\n\n props: {\n refreshInterval: {\n type: Number as PropType<SkMouseTrapProps['refreshInterval']>,\n default: 500,\n },\n },\n\n data() {\n return {\n commands: [] as HotKeyMetaData[],\n display: false,\n intervalId: null as number | null,\n };\n },\n\n created() {\n this.intervalId = window.setInterval(this.refreshState, this.refreshInterval);\n },\n\n beforeUnmount() {\n if (this.intervalId !== null) {\n clearInterval(this.intervalId);\n }\n },\n\n methods: {\n refreshState() {\n this.commands = SkldrMouseTrap.commands;\n this.display = this.commands.length > 0;\n },\n },\n});\n</script>\n","<template>\n <v-dialog v-if=\"display\" max-width=\"500px\" transition=\"dialog-transition\">\n <template #activator=\"{ props }\">\n <v-btn icon color=\"primary\" v-bind=\"props\">\n <v-icon>mdi-keyboard</v-icon>\n </v-btn>\n </template>\n\n <v-card>\n <v-toolbar color=\"secondary\" dark dense>\n <v-toolbar-title class=\"text-subtitle-1\">Shortcut keys:</v-toolbar-title>\n </v-toolbar>\n <v-list dense>\n <v-list-item\n v-for=\"hk in commands\"\n :key=\"Array.isArray(hk.hotkey) ? hk.hotkey.join(',') : hk.hotkey\"\n class=\"py-1\"\n >\n <v-btn variant=\"outlined\" color=\"primary\" class=\"text-white\" size=\"small\">\n {{ Array.isArray(hk.hotkey) ? hk.hotkey[0] : hk.hotkey }}\n </v-btn>\n <v-spacer></v-spacer>\n <span class=\"text-caption ml-2\">{{ hk.command }}</span>\n </v-list-item>\n </v-list>\n </v-card>\n </v-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';\nimport { SkMouseTrapProps } from './SkMouseTrap.types';\n\nexport default defineComponent({\n name: 'SkMouseTrap',\n\n props: {\n refreshInterval: {\n type: Number as PropType<SkMouseTrapProps['refreshInterval']>,\n default: 500,\n },\n },\n\n data() {\n return {\n commands: [] as HotKeyMetaData[],\n display: false,\n intervalId: null as number | null,\n };\n },\n\n created() {\n this.intervalId = window.setInterval(this.refreshState, this.refreshInterval);\n },\n\n beforeUnmount() {\n if (this.intervalId !== null) {\n clearInterval(this.intervalId);\n }\n },\n\n methods: {\n refreshState() {\n this.commands = SkldrMouseTrap.commands;\n this.display = this.commands.length > 0;\n },\n },\n});\n</script>\n","<template>\n <div\n class=\"sk-mousetrap-tooltip-wrapper\"\n ref=\"wrapperElement\"\n :class=\"[\n isControlKeyPressed && !disabled && highlightEffect !== 'none' ? `sk-mousetrap-highlight-${highlightEffect}` : '',\n ]\"\n >\n <slot></slot>\n <transition name=\"fade\">\n <div\n v-if=\"showTooltip && isControlKeyPressed && !disabled\"\n class=\"sk-mousetrap-tooltip\"\n :class=\"{\n 'sk-mt-tooltip-top': position === 'top',\n 'sk-mt-tooltip-bottom': position === 'bottom',\n 'sk-mt-tooltip-left': position === 'left',\n 'sk-mt-tooltip-right': position === 'right',\n }\"\n >\n {{ formattedHotkey }}\n </div>\n </transition>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';\nimport { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';\n\nexport default defineComponent({\n name: 'SkMouseTrapToolTip',\n\n props: {\n hotkey: {\n type: [String, Array] as PropType<string | string[]>,\n required: true,\n },\n command: {\n type: String,\n required: true,\n },\n disabled: {\n type: Boolean,\n default: false,\n },\n position: {\n type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,\n default: 'top',\n },\n showTooltip: {\n type: Boolean,\n default: true,\n },\n highlightEffect: {\n type: String as PropType<'glow' | 'scale' | 'border' | 'none'>,\n default: 'glow',\n },\n },\n\n emits: ['hotkey-triggered'],\n\n setup(props, { emit }) {\n const wrapperElement = ref<HTMLElement | null>(null);\n const isControlKeyPressed = ref(false);\n const hotkeyId = ref(`hotkey-${Math.random().toString(36).substring(2, 15)}`);\n\n // Format hotkey for display\n const formattedHotkey = computed(() => {\n const hotkey = Array.isArray(props.hotkey) ? props.hotkey[0] : props.hotkey;\n // Check if this is a sequence (has spaces) or a combination (has +)\n if (hotkey.includes(' ')) {\n // For sequences like \"g h\", display as \"g, h\"\n return hotkey\n .toLowerCase()\n .split(' ')\n .map((part) => part.charAt(0) + part.slice(1))\n .join(', ');\n } else {\n // For combinations like \"ctrl+s\", display as \"Ctrl + S\"\n return hotkey\n .toLowerCase()\n .split('+')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' + ');\n }\n });\n\n // Apply highlight effect to the actual button/control when Ctrl is pressed\n watch(\n () => isControlKeyPressed.value,\n (pressed) => {\n if (!wrapperElement.value || props.disabled) return;\n\n const clickableElement = wrapperElement.value.querySelector(\n 'button, a, input[type=\"button\"], [role=\"button\"]'\n ) as HTMLElement;\n\n if (clickableElement) {\n clickableElement.style.transition = 'all 250ms ease';\n\n if (pressed && props.highlightEffect !== 'none') {\n // Add slight brightness increase to the inner element\n clickableElement.style.filter = 'brightness(1.1)';\n } else {\n clickableElement.style.filter = '';\n }\n }\n }\n );\n\n // Handle Ctrl key detection\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Control') {\n isControlKeyPressed.value = true;\n }\n };\n\n const handleKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Control') {\n isControlKeyPressed.value = false;\n }\n };\n\n // Handle clicking the element when hotkey is pressed\n const handleHotkeyPress = () => {\n if (props.disabled || !wrapperElement.value) return;\n\n // Try finding a clickable element within our wrapper\n let clickableElement = wrapperElement.value.querySelector(\n 'button, a, input[type=\"button\"], [role=\"button\"]'\n ) as HTMLElement;\n\n // If no standard clickable element found, try to find navigation or list items\n if (!clickableElement) {\n clickableElement = wrapperElement.value.querySelector(\n 'v-list-item, .v-list-item, router-link, .router-link, a, [to]'\n ) as HTMLElement;\n }\n\n // If still no element found, try the wrapper itself - it might be clickable\n if (\n !clickableElement &&\n (wrapperElement.value.hasAttribute('to') ||\n wrapperElement.value.tagName === 'A' ||\n wrapperElement.value.classList.contains('v-list-item'))\n ) {\n clickableElement = wrapperElement.value;\n }\n\n // Get closest parent list item or router link if we found a title/content element\n if (!clickableElement) {\n const closestClickableParent = wrapperElement.value.closest('v-list-item, .v-list-item, a, [to]');\n if (closestClickableParent) {\n clickableElement = closestClickableParent as HTMLElement;\n }\n }\n\n if (clickableElement) {\n if (clickableElement.hasAttribute('to')) {\n // Handle router-link style navigation\n const routePath = clickableElement.getAttribute('to');\n if (routePath && window.location.pathname !== routePath) {\n // Use parent router if available (Vue component)\n const router = (window as any).$nuxt?.$router || (window as any).$router;\n if (router && typeof router.push === 'function') {\n router.push(routePath);\n } else {\n // Fallback to regular navigation\n window.location.pathname = routePath;\n }\n }\n emit('hotkey-triggered', props.hotkey);\n } else {\n // Regular click for standard elements\n clickableElement.click();\n emit('hotkey-triggered', props.hotkey);\n }\n } else {\n // If no clickable element found, emit the event for parent handling\n console.log('No clickable element found for hotkey', props.hotkey);\n emit('hotkey-triggered', props.hotkey);\n }\n };\n\n // Register/unregister the hotkey binding\n const registerHotkey = () => {\n if (!props.disabled) {\n SkldrMouseTrap.addBinding({\n hotkey: props.hotkey,\n command: props.command,\n callback: handleHotkeyPress,\n });\n }\n };\n\n const unregisterHotkey = () => {\n if (!props.disabled) {\n SkldrMouseTrap.removeBinding(props.hotkey);\n }\n };\n\n // Watch for changes to the disabled prop\n watch(\n () => props.disabled,\n (newValue) => {\n if (newValue) {\n unregisterHotkey();\n } else {\n registerHotkey();\n }\n }\n );\n\n onMounted(() => {\n // Register global keyboard listeners for the Ctrl key\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('keyup', handleKeyUp);\n\n // Register the hotkey\n registerHotkey();\n });\n\n onBeforeUnmount(() => {\n // Clean up event listeners\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('keyup', handleKeyUp);\n\n // Unregister the hotkey\n unregisterHotkey();\n });\n\n return {\n wrapperElement,\n isControlKeyPressed,\n formattedHotkey,\n };\n },\n});\n</script>\n\n<style scoped>\n.sk-mousetrap-tooltip-wrapper {\n display: inline-block;\n position: relative;\n}\n\n.sk-mousetrap-tooltip {\n position: absolute;\n background-color: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 12px;\n white-space: nowrap;\n pointer-events: none;\n z-index: 9999;\n}\n\n.sk-mt-tooltip-top {\n bottom: 100%;\n margin-bottom: 5px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.sk-mt-tooltip-bottom {\n top: 100%;\n margin-top: 5px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.sk-mt-tooltip-left {\n right: 100%;\n margin-right: 5px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n.sk-mt-tooltip-right {\n left: 100%;\n margin-left: 5px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n/* Highlight effects when Ctrl is pressed */\n.sk-mousetrap-highlight-glow {\n box-shadow: 0 0 8px 2px rgba(25, 118, 210, 0.6);\n transition: box-shadow 250ms ease;\n}\n\n.sk-mousetrap-highlight-scale {\n transform: scale(1.03);\n transition: transform 250ms ease;\n}\n\n.sk-mousetrap-highlight-border {\n outline: 2px solid rgba(25, 118, 210, 0.8);\n outline-offset: 2px;\n border-radius: 4px;\n transition: outline 250ms ease, outline-offset 250ms ease;\n}\n\n/* Fade transition */\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 250ms ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n</style>\n","<template>\n <div\n class=\"sk-mousetrap-tooltip-wrapper\"\n ref=\"wrapperElement\"\n :class=\"[\n isControlKeyPressed && !disabled && highlightEffect !== 'none' ? `sk-mousetrap-highlight-${highlightEffect}` : '',\n ]\"\n >\n <slot></slot>\n <transition name=\"fade\">\n <div\n v-if=\"showTooltip && isControlKeyPressed && !disabled\"\n class=\"sk-mousetrap-tooltip\"\n :class=\"{\n 'sk-mt-tooltip-top': position === 'top',\n 'sk-mt-tooltip-bottom': position === 'bottom',\n 'sk-mt-tooltip-left': position === 'left',\n 'sk-mt-tooltip-right': position === 'right',\n }\"\n >\n {{ formattedHotkey }}\n </div>\n </transition>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';\nimport { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';\n\nexport default defineComponent({\n name: 'SkMouseTrapToolTip',\n\n props: {\n hotkey: {\n type: [String, Array] as PropType<string | string[]>,\n required: true,\n },\n command: {\n type: String,\n required: true,\n },\n disabled: {\n type: Boolean,\n default: false,\n },\n position: {\n type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,\n default: 'top',\n },\n showTooltip: {\n type: Boolean,\n default: true,\n },\n highlightEffect: {\n type: String as PropType<'glow' | 'scale' | 'border' | 'none'>,\n default: 'glow',\n },\n },\n\n emits: ['hotkey-triggered'],\n\n setup(props, { emit }) {\n const wrapperElement = ref<HTMLElement | null>(null);\n const isControlKeyPressed = ref(false);\n const hotkeyId = ref(`hotkey-${Math.random().toString(36).substring(2, 15)}`);\n\n // Format hotkey for display\n const formattedHotkey = computed(() => {\n const hotkey = Array.isArray(props.hotkey) ? props.hotkey[0] : props.hotkey;\n // Check if this is a sequence (has spaces) or a combination (has +)\n if (hotkey.includes(' ')) {\n // For sequences like \"g h\", display as \"g, h\"\n return hotkey\n .toLowerCase()\n .split(' ')\n .map((part) => part.charAt(0) + part.slice(1))\n .join(', ');\n } else {\n // For combinations like \"ctrl+s\", display as \"Ctrl + S\"\n return hotkey\n .toLowerCase()\n .split('+')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' + ');\n }\n });\n\n // Apply highlight effect to the actual button/control when Ctrl is pressed\n watch(\n () => isControlKeyPressed.value,\n (pressed) => {\n if (!wrapperElement.value || props.disabled) return;\n\n const clickableElement = wrapperElement.value.querySelector(\n 'button, a, input[type=\"button\"], [role=\"button\"]'\n ) as HTMLElement;\n\n if (clickableElement) {\n clickableElement.style.transition = 'all 250ms ease';\n\n if (pressed && props.highlightEffect !== 'none') {\n // Add slight brightness increase to the inner element\n clickableElement.style.filter = 'brightness(1.1)';\n } else {\n clickableElement.style.filter = '';\n }\n }\n }\n );\n\n // Handle Ctrl key detection\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Control') {\n isControlKeyPressed.value = true;\n }\n };\n\n const handleKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Control') {\n isControlKeyPressed.value = false;\n }\n };\n\n // Handle clicking the element when hotkey is pressed\n const handleHotkeyPress = () => {\n if (props.disabled || !wrapperElement.value) return;\n\n // Try finding a clickable element within our wrapper\n let clickableElement = wrapperElement.value.querySelector(\n 'button, a, input[type=\"button\"], [role=\"button\"]'\n ) as HTMLElement;\n\n // If no standard clickable element found, try to find navigation or list items\n if (!clickableElement) {\n clickableElement = wrapperElement.value.querySelector(\n 'v-list-item, .v-list-item, router-link, .router-link, a, [to]'\n ) as HTMLElement;\n }\n\n // If still no element found, try the wrapper itself - it might be clickable\n if (\n !clickableElement &&\n (wrapperElement.value.hasAttribute('to') ||\n wrapperElement.value.tagName === 'A' ||\n wrapperElement.value.classList.contains('v-list-item'))\n ) {\n clickableElement = wrapperElement.value;\n }\n\n // Get closest parent list item or router link if we found a title/content element\n if (!clickableElement) {\n const closestClickableParent = wrapperElement.value.closest('v-list-item, .v-list-item, a, [to]');\n if (closestClickableParent) {\n clickableElement = closestClickableParent as HTMLElement;\n }\n }\n\n if (clickableElement) {\n if (clickableElement.hasAttribute('to')) {\n // Handle router-link style navigation\n const routePath = clickableElement.getAttribute('to');\n if (routePath && window.location.pathname !== routePath) {\n // Use parent router if available (Vue component)\n const router = (window as any).$nuxt?.$router || (window as any).$router;\n if (router && typeof router.push === 'function') {\n router.push(routePath);\n } else {\n // Fallback to regular navigation\n window.location.pathname = routePath;\n }\n }\n emit('hotkey-triggered', props.hotkey);\n } else {\n // Regular click for standard elements\n clickableElement.click();\n emit('hotkey-triggered', props.hotkey);\n }\n } else {\n // If no clickable element found, emit the event for parent handling\n console.log('No clickable element found for hotkey', props.hotkey);\n emit('hotkey-triggered', props.hotkey);\n }\n };\n\n // Register/unregister the hotkey binding\n const registerHotkey = () => {\n if (!props.disabled) {\n SkldrMouseTrap.addBinding({\n hotkey: props.hotkey,\n command: props.command,\n callback: handleHotkeyPress,\n });\n }\n };\n\n const unregisterHotkey = () => {\n if (!props.disabled) {\n SkldrMouseTrap.removeBinding(props.hotkey);\n }\n };\n\n // Watch for changes to the disabled prop\n watch(\n () => props.disabled,\n (newValue) => {\n if (newValue) {\n unregisterHotkey();\n } else {\n registerHotkey();\n }\n }\n );\n\n onMounted(() => {\n // Register global keyboard listeners for the Ctrl key\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('keyup', handleKeyUp);\n\n // Register the hotkey\n registerHotkey();\n });\n\n onBeforeUnmount(() => {\n // Clean up event listeners\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('keyup', handleKeyUp);\n\n // Unregister the hotkey\n unregisterHotkey();\n });\n\n return {\n wrapperElement,\n isControlKeyPressed,\n formattedHotkey,\n };\n },\n});\n</script>\n\n<style scoped>\n.sk-mousetrap-tooltip-wrapper {\n display: inline-block;\n position: relative;\n}\n\n.sk-mousetrap-tooltip {\n position: absolute;\n background-color: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 12px;\n white-space: nowrap;\n pointer-events: none;\n z-index: 9999;\n}\n\n.sk-mt-tooltip-top {\n bottom: 100%;\n margin-bottom: 5px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.sk-mt-tooltip-bottom {\n top: 100%;\n margin-top: 5px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.sk-mt-tooltip-left {\n right: 100%;\n margin-right: 5px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n.sk-mt-tooltip-right {\n left: 100%;\n margin-left: 5px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n/* Highlight effects when Ctrl is pressed */\n.sk-mousetrap-highlight-glow {\n box-shadow: 0 0 8px 2px rgba(25, 118, 210, 0.6);\n transition: box-shadow 250ms ease;\n}\n\n.sk-mousetrap-highlight-scale {\n transform: scale(1.03);\n transition: transform 250ms ease;\n}\n\n.sk-mousetrap-highlight-border {\n outline: 2px solid rgba(25, 118, 210, 0.8);\n outline-offset: 2px;\n border-radius: 4px;\n transition: outline 250ms ease, outline-offset 250ms ease;\n}\n\n/* Fade transition */\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 250ms ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n</style>\n","// common-ui/src/components/SnackbarService.ts\nimport { Status } from '@vue-skuilder/common';\n\nexport interface SnackbarOptions {\n text: string;\n status: Status;\n timeout?: number;\n}\n\n// Module for managing the snackbar service\nconst SnackbarServiceModule = (() => {\n // Private variable to hold the instance\n let _instance: { addSnack: (snack: SnackbarOptions) => void } | null = null;\n\n return {\n // Register the instance\n setInstance(instance: { addSnack: (snack: SnackbarOptions) => void }): void {\n _instance = instance;\n },\n\n // Get the current instance\n getInstance(): { addSnack: (snack: SnackbarOptions) => void } | null {\n return _instance;\n },\n\n // Alert user function\n alertUser(msg: SnackbarOptions): void {\n // Try getting the instance\n const snackBarService = _instance;\n if (snackBarService) {\n snackBarService.addSnack(msg);\n return;\n }\n console.error('SnackbarService not found');\n },\n };\n})();\n\nexport const { setInstance, getInstance, alertUser } = SnackbarServiceModule;\n","<template>\n <div>\n <v-snackbar\n v-for=\"snack in snacks\"\n :key=\"snacks.indexOf(snack)\"\n v-model=\"show[snacks.indexOf(snack)]\"\n :timeout=\"snack.timeout\"\n location=\"bottom right\"\n :color=\"getColor(snack)\"\n >\n <div class=\"d-flex align-center justify-space-between w-100\">\n <span>{{ snack.text }}</span>\n <v-btn icon variant=\"text\" @click=\"close()\">\n <v-icon>mdi-close</v-icon>\n </v-btn>\n </div>\n </v-snackbar>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { Status } from '@vue-skuilder/common';\nimport { SnackbarOptions, setInstance } from './SnackbarService';\n\nexport default defineComponent({\n name: 'SnackbarService',\n\n data() {\n return {\n /**\n * A history of snacks served in this session.\n *\n * Possible future work: write these to localstorage/pouchdb\n * for persistance\n */\n snacks: [] as SnackbarOptions[],\n show: [] as boolean[],\n };\n },\n mounted() {\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n setInstance(this);\n },\n\n methods: {\n addSnack(snack: SnackbarOptions): void {\n this.snacks.push(snack);\n this.show.push(true);\n },\n\n close(): void {\n this.show.pop();\n this.show.push(false);\n },\n\n getColor(snack: SnackbarOptions): string | undefined {\n if (snack.status === Status.ok) {\n return 'success';\n } else if (snack.status === Status.error) {\n return 'error';\n } else if (snack.status === Status.warning) {\n return 'yellow';\n }\n return undefined;\n },\n },\n});\n</script>\n","<template>\n <div>\n <v-snackbar\n v-for=\"snack in snacks\"\n :key=\"snacks.indexOf(snack)\"\n v-model=\"show[snacks.indexOf(snack)]\"\n :timeout=\"snack.timeout\"\n location=\"bottom right\"\n :color=\"getColor(snack)\"\n >\n <div class=\"d-flex align-center justify-space-between w-100\">\n <span>{{ snack.text }}</span>\n <v-btn icon variant=\"text\" @click=\"close()\">\n <v-icon>mdi-close</v-icon>\n </v-btn>\n </div>\n </v-snackbar>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { Status } from '@vue-skuilder/common';\nimport { SnackbarOptions, setInstance } from './SnackbarService';\n\nexport default defineComponent({\n name: 'SnackbarService',\n\n data() {\n return {\n /**\n * A history of snacks served in this session.\n *\n * Possible future work: write these to localstorage/pouchdb\n * for persistance\n */\n snacks: [] as SnackbarOptions[],\n show: [] as boolean[],\n };\n },\n mounted() {\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n setInstance(this);\n },\n\n methods: {\n addSnack(snack: SnackbarOptions): void {\n this.snacks.push(snack);\n this.show.push(true);\n },\n\n close(): void {\n this.show.pop();\n this.show.push(false);\n },\n\n getColor(snack: SnackbarOptions): string | undefined {\n if (snack.status === Status.ok) {\n return 'success';\n } else if (snack.status === Status.error) {\n return 'error';\n } else if (snack.status === Status.warning) {\n return 'yellow';\n }\n return undefined;\n },\n },\n});\n</script>\n","<template>\n <v-toolbar density=\"compact\">\n <v-toolbar-title>\n <span>{{ title }}</span>\n <span v-if=\"subtitle\" class=\"ms-2 text-subtitle-2\" data-cy=\"paginating-toolbar-subtitle\">{{ subtitle }}</span>\n </v-toolbar-title>\n <v-spacer></v-spacer>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == 1\" @click=\"$emit('first')\">\n <v-icon>mdi-page-first</v-icon>\n </v-btn>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == 1\" @click=\"$emit('prev')\">\n <v-icon>mdi-chevron-left</v-icon>\n </v-btn>\n\n <v-select\n :model-value=\"page\"\n :items=\"pages\"\n class=\"pageSelect\"\n density=\"compact\"\n hide-details\n :return-object=\"false\"\n variant=\"outlined\"\n @update:model-value=\"(val: unknown) => $emit('set-page', val)\"\n >\n <template #selection=\"{ item }\">\n {{ item.value }}\n </template>\n </v-select>\n\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == pages.length\" @click=\"$emit('next')\">\n <v-icon>mdi-chevron-right</v-icon>\n </v-btn>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == pages.length\" @click=\"$emit('last')\">\n <v-icon>mdi-page-last</v-icon>\n </v-btn>\n </v-toolbar>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport type { PaginatingToolbarProps, PaginatingToolbarEvents } from './PaginatingToolbar.types';\n\nexport default defineComponent({\n name: 'PaginatingToolbar',\n\n props: {\n pages: {\n type: Array as PropType<number[]>,\n required: true,\n },\n page: {\n type: Number,\n required: true,\n },\n title: {\n type: String,\n required: false,\n default: '',\n },\n subtitle: {\n type: String,\n required: false,\n default: '',\n },\n } as const,\n\n emits: ['first', 'prev', 'next', 'last', 'set-page'],\n});\n</script>\n\n<style scoped>\n.pageSelect {\n max-width: 60px;\n}\n</style>\n","<template>\n <v-toolbar density=\"compact\">\n <v-toolbar-title>\n <span>{{ title }}</span>\n <span v-if=\"subtitle\" class=\"ms-2 text-subtitle-2\" data-cy=\"paginating-toolbar-subtitle\">{{ subtitle }}</span>\n </v-toolbar-title>\n <v-spacer></v-spacer>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == 1\" @click=\"$emit('first')\">\n <v-icon>mdi-page-first</v-icon>\n </v-btn>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == 1\" @click=\"$emit('prev')\">\n <v-icon>mdi-chevron-left</v-icon>\n </v-btn>\n\n <v-select\n :model-value=\"page\"\n :items=\"pages\"\n class=\"pageSelect\"\n density=\"compact\"\n hide-details\n :return-object=\"false\"\n variant=\"outlined\"\n @update:model-value=\"(val: unknown) => $emit('set-page', val)\"\n >\n <template #selection=\"{ item }\">\n {{ item.value }}\n </template>\n </v-select>\n\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == pages.length\" @click=\"$emit('next')\">\n <v-icon>mdi-chevron-right</v-icon>\n </v-btn>\n <v-btn variant=\"text\" icon color=\"secondary\" :disabled=\"page == pages.length\" @click=\"$emit('last')\">\n <v-icon>mdi-page-last</v-icon>\n </v-btn>\n </v-toolbar>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport type { PaginatingToolbarProps, PaginatingToolbarEvents } from './PaginatingToolbar.types';\n\nexport default defineComponent({\n name: 'PaginatingToolbar',\n\n props: {\n pages: {\n type: Array as PropType<number[]>,\n required: true,\n },\n page: {\n type: Number,\n required: true,\n },\n title: {\n type: String,\n required: false,\n default: '',\n },\n subtitle: {\n type: String,\n required: false,\n default: '',\n },\n } as const,\n\n emits: ['first', 'prev', 'next', 'last', 'set-page'],\n});\n</script>\n\n<style scoped>\n.pageSelect {\n max-width: 60px;\n}\n</style>\n","<template>\n <div class=\"card-search\">\n <v-text-field\n v-model=\"query\"\n label=\"Search for cards...\"\n append-icon=\"mdi-magnify\"\n @click:append=\"search\"\n @keydown.enter=\"search\"\n ></v-text-field>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n name: 'CardSearch',\n emits: {\n search: (query: string) => typeof query === 'string'\n },\n data() {\n return {\n query: '',\n };\n },\n methods: {\n search() {\n this.$emit('search', this.query);\n },\n },\n});\n</script>\n","<template>\n <div class=\"card-search\">\n <v-text-field\n v-model=\"query\"\n label=\"Search for cards...\"\n append-icon=\"mdi-magnify\"\n @click:append=\"search\"\n @keydown.enter=\"search\"\n ></v-text-field>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n name: 'CardSearch',\n emits: {\n search: (query: string) => typeof query === 'string'\n },\n data() {\n return {\n query: '',\n };\n },\n methods: {\n search() {\n this.$emit('search', this.query);\n },\n },\n});\n</script>\n","<template>\n <div class=\"card-search-results\">\n <div v-if=\"loading\">Loading...</div>\n <div v-else-if=\"error\">{{ error }}</div>\n <div v-else>\n <v-list>\n <v-list-item \n v-for=\"card in cards\" \n :key=\"card._id\"\n @click=\"selectCard(card)\"\n :class=\"{'selected-card': card._id === selectedCardId}\"\n class=\"cursor-pointer\"\n >\n <v-list-item-title>{{ card._id }}</v-list-item-title>\n <v-list-item-subtitle>Course: {{ card.courseId }}</v-list-item-subtitle>\n </v-list-item>\n </v-list>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { DataLayerProvider, CardData } from '@vue-skuilder/db';\n\ninterface CardWithCourse extends CardData {\n courseId: string;\n}\n\nexport default defineComponent({\n name: 'CardSearchResults',\n emits: {\n 'card-selected': (payload: { cardId: string; courseId: string }) => \n typeof payload.cardId === 'string' && typeof payload.courseId === 'string'\n },\n props: {\n query: {\n type: String,\n required: true,\n },\n dataLayer: {\n type: Object as PropType<DataLayerProvider>,\n required: true,\n },\n courseFilter: {\n type: String as PropType<string | null>,\n default: null,\n },\n },\n data() {\n return {\n cards: [] as CardWithCourse[],\n loading: false,\n error: null as string | null,\n selectedCardId: null as string | null,\n };\n },\n watch: {\n query: {\n immediate: true,\n handler(newQuery) {\n if (newQuery) {\n this.fetchResults(newQuery);\n }\n },\n },\n },\n methods: {\n async fetchResults(query: string) {\n this.loading = true;\n this.error = null;\n try {\n let courseIds: string[] = [];\n \n // Get course IDs efficiently\n if (this.courseFilter) {\n // Single course search - no need to fetch all courses\n courseIds = [this.courseFilter];\n console.log(`Filtering search to course: ${this.courseFilter}`);\n } else {\n // Get all course IDs without expensive config lookups\n const { CourseLookup } = await import('@vue-skuilder/db');\n const lookupCourses = await CourseLookup.allCourseWare();\n courseIds = lookupCourses.map(c => c._id).filter(Boolean);\n console.log(`Searching across all ${courseIds.length} courses`);\n }\n \n const allCards: CardWithCourse[] = [];\n\n for (const courseId of courseIds) {\n const courseDB = this.dataLayer.getCourseDB(courseId);\n const cards = await courseDB.searchCards(query);\n \n for (const card of cards) {\n allCards.push({\n ...card,\n courseId: courseId,\n });\n }\n }\n this.cards = allCards;\n console.log(`Search completed: found ${allCards.length} cards across ${courseIds.length} courses`);\n } catch (e) {\n this.error = 'Error fetching search results.';\n console.error('Search error:', e);\n } finally {\n this.loading = false;\n }\n },\n \n selectCard(card: CardWithCourse) {\n this.selectedCardId = card._id;\n this.$emit('card-selected', { cardId: card._id, courseId: card.courseId });\n },\n },\n});\n</script>\n\n<style scoped>\n.selected-card {\n background-color: #e0f2f7; /* Light blue background */\n border-left: 4px solid #2196f3; /* Blue left border */\n}\n\n.cursor-pointer {\n cursor: pointer;\n}\n</style>\n","<template>\n <div class=\"card-search-results\">\n <div v-if=\"loading\">Loading...</div>\n <div v-else-if=\"error\">{{ error }}</div>\n <div v-else>\n <v-list>\n <v-list-item \n v-for=\"card in cards\" \n :key=\"card._id\"\n @click=\"selectCard(card)\"\n :class=\"{'selected-card': card._id === selectedCardId}\"\n class=\"cursor-pointer\"\n >\n <v-list-item-title>{{ card._id }}</v-list-item-title>\n <v-list-item-subtitle>Course: {{ card.courseId }}</v-list-item-subtitle>\n </v-list-item>\n </v-list>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { DataLayerProvider, CardData } from '@vue-skuilder/db';\n\ninterface CardWithCourse extends CardData {\n courseId: string;\n}\n\nexport default defineComponent({\n name: 'CardSearchResults',\n emits: {\n 'card-selected': (payload: { cardId: string; courseId: string }) => \n typeof payload.cardId === 'string' && typeof payload.courseId === 'string'\n },\n props: {\n query: {\n type: String,\n required: true,\n },\n dataLayer: {\n type: Object as PropType<DataLayerProvider>,\n required: true,\n },\n courseFilter: {\n type: String as PropType<string | null>,\n default: null,\n },\n },\n data() {\n return {\n cards: [] as CardWithCourse[],\n loading: false,\n error: null as string | null,\n selectedCardId: null as string | null,\n };\n },\n watch: {\n query: {\n immediate: true,\n handler(newQuery) {\n if (newQuery) {\n this.fetchResults(newQuery);\n }\n },\n },\n },\n methods: {\n async fetchResults(query: string) {\n this.loading = true;\n this.error = null;\n try {\n let courseIds: string[] = [];\n \n // Get course IDs efficiently\n if (this.courseFilter) {\n // Single course search - no need to fetch all courses\n courseIds = [this.courseFilter];\n console.log(`Filtering search to course: ${this.courseFilter}`);\n } else {\n // Get all course IDs without expensive config lookups\n const { CourseLookup } = await import('@vue-skuilder/db');\n const lookupCourses = await CourseLookup.allCourseWare();\n courseIds = lookupCourses.map(c => c._id).filter(Boolean);\n console.log(`Searching across all ${courseIds.length} courses`);\n }\n \n const allCards: CardWithCourse[] = [];\n\n for (const courseId of courseIds) {\n const courseDB = this.dataLayer.getCourseDB(courseId);\n const cards = await courseDB.searchCards(query);\n \n for (const card of cards) {\n allCards.push({\n ...card,\n courseId: courseId,\n });\n }\n }\n this.cards = allCards;\n console.log(`Search completed: found ${allCards.length} cards across ${courseIds.length} courses`);\n } catch (e) {\n this.error = 'Error fetching search results.';\n console.error('Search error:', e);\n } finally {\n this.loading = false;\n }\n },\n \n selectCard(card: CardWithCourse) {\n this.selectedCardId = card._id;\n this.$emit('card-selected', { cardId: card._id, courseId: card.courseId });\n },\n },\n});\n</script>\n\n<style scoped>\n.selected-card {\n background-color: #e0f2f7; /* Light blue background */\n border-left: 4px solid #2196f3; /* Blue left border */\n}\n\n.cursor-pointer {\n cursor: pointer;\n}\n</style>\n","<template>\n <div class=\"card-history-viewer\">\n <h3 v-if=\"userId\">Card History for User: {{ userId }}</h3>\n <div v-if=\"loading\">Loading...</div>\n <div v-else-if=\"error\" class=\"error-message\">{{ error }}</div>\n <div v-else-if=\"cardHistory\">\n <v-card class=\"mb-4\">\n <v-card-title>Summary</v-card-title>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title\n >Best Interval: {{ cardHistory.bestInterval }} seconds ({{ bestIntervalHumanized }})</v-list-item-title\n >\n <v-list-item-subtitle>The to-date largest interval between successful card reviews.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title>Lapses: {{ cardHistory.lapses }}</v-list-item-title>\n <v-list-item-subtitle>The number of times that a card has been failed in review.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title>Streak: {{ cardHistory.streak }}</v-list-item-title>\n <v-list-item-subtitle>The number of consecutive successful impressions on this card.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n </v-card>\n <v-data-table :headers=\"headers\" :items=\"history\" class=\"elevation-1\"></v-data-table>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { UserDBInterface, UserDBReader, CardHistory, CardRecord, getCardHistoryID } from '@vue-skuilder/db';\nimport moment, { Moment } from 'moment';\n\ninterface FormattedRecord extends CardRecord {\n formattedTimeStamp: string;\n intervalFromPrevious?: number;\n userFriendlyInterval?: string;\n timeSpentSeconds: number;\n}\n\nexport default defineComponent({\n name: 'CardHistoryViewer',\n props: {\n cardId: {\n type: String,\n required: true,\n },\n courseId: {\n type: String,\n required: true,\n },\n userId: {\n type: String,\n required: true,\n },\n userDB: {\n type: Object as PropType<UserDBReader>,\n required: true,\n },\n },\n data() {\n return {\n history: [] as FormattedRecord[],\n cardHistory: null as CardHistory<CardRecord> | null,\n bestIntervalHumanized: '',\n loading: false,\n error: null as string | null,\n headers: [\n { title: 'Timestamp', key: 'formattedTimeStamp' },\n { title: 'Interval', key: 'userFriendlyInterval' },\n { title: 'Time Spent (s)', key: 'timeSpentSeconds' },\n { title: 'Correct?', key: 'isCorrect' },\n { title: 'Performance', key: 'performance' },\n { title: 'Prior Attempts', key: 'priorAttemps' },\n { title: 'User Answer', key: 'userAnswer' },\n ],\n };\n },\n watch: {\n cardId: {\n immediate: true,\n handler(newCardId) {\n if (newCardId && this.userDB) {\n this.fetchHistory();\n }\n },\n },\n userDB: {\n handler(newUserDB) {\n if (newUserDB && this.cardId) {\n this.fetchHistory();\n }\n },\n },\n },\n methods: {\n async fetchHistory() {\n this.loading = true;\n this.error = null;\n try {\n const cardHistoryID = getCardHistoryID(this.courseId, this.cardId);\n const historyDoc: CardHistory<CardRecord> = await this.userDB.get(cardHistoryID);\n this.cardHistory = historyDoc;\n this.bestIntervalHumanized = moment.duration(historyDoc.bestInterval, 'seconds').humanize();\n\n // Sort records by timestamp and format them\n const sortedRecords = [...historyDoc.records].sort(\n (a, b) => moment(a.timeStamp).valueOf() - moment(b.timeStamp).valueOf()\n );\n\n this.history = sortedRecords.map((record, index) => {\n const currentTime = moment(record.timeStamp);\n const formatted: FormattedRecord = {\n ...record,\n formattedTimeStamp: currentTime.format('YYYY-MM-DD HH:mm:ss'),\n timeSpentSeconds: Math.round(record.timeSpent / 1000),\n isCorrect: (record as any).isCorrect,\n performance: (record as any).performance,\n priorAttemps: (record as any).priorAttemps,\n userAnswer: (record as any).userAnswer,\n };\n\n // Calculate interval from previous record\n if (index > 0) {\n const previousTime = moment(sortedRecords[index - 1].timeStamp);\n const intervalSeconds = currentTime.diff(previousTime, 'seconds', true);\n formatted.intervalFromPrevious = Math.round(intervalSeconds * 100) / 100;\n formatted.userFriendlyInterval = `${formatted.intervalFromPrevious} sec (${moment.duration(intervalSeconds, 'seconds').humanize()})`;\n }\n\n return formatted;\n });\n } catch (e) {\n this.error = 'Error fetching card history.';\n console.error(e);\n } finally {\n this.loading = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.error-message {\n color: #f44336;\n padding: 16px;\n background-color: #ffebee;\n border-radius: 4px;\n margin: 8px 0;\n}\n\n.negative-interval {\n color: #f44336;\n font-weight: bold;\n background-color: #ffebee;\n padding: 2px 4px;\n border-radius: 3px;\n}\n\n.invalid-timestamp {\n color: #f44336;\n font-weight: bold;\n}\n\n.card-history-viewer {\n margin: 16px 0;\n}\n</style>\n","<template>\n <div class=\"card-history-viewer\">\n <h3 v-if=\"userId\">Card History for User: {{ userId }}</h3>\n <div v-if=\"loading\">Loading...</div>\n <div v-else-if=\"error\" class=\"error-message\">{{ error }}</div>\n <div v-else-if=\"cardHistory\">\n <v-card class=\"mb-4\">\n <v-card-title>Summary</v-card-title>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title\n >Best Interval: {{ cardHistory.bestInterval }} seconds ({{ bestIntervalHumanized }})</v-list-item-title\n >\n <v-list-item-subtitle>The to-date largest interval between successful card reviews.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title>Lapses: {{ cardHistory.lapses }}</v-list-item-title>\n <v-list-item-subtitle>The number of times that a card has been failed in review.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n <v-list-item>\n <v-list-item-content>\n <v-list-item-title>Streak: {{ cardHistory.streak }}</v-list-item-title>\n <v-list-item-subtitle>The number of consecutive successful impressions on this card.</v-list-item-subtitle>\n </v-list-item-content>\n </v-list-item>\n </v-card>\n <v-data-table :headers=\"headers\" :items=\"history\" class=\"elevation-1\"></v-data-table>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { UserDBInterface, UserDBReader, CardHistory, CardRecord, getCardHistoryID } from '@vue-skuilder/db';\nimport moment, { Moment } from 'moment';\n\ninterface FormattedRecord extends CardRecord {\n formattedTimeStamp: string;\n intervalFromPrevious?: number;\n userFriendlyInterval?: string;\n timeSpentSeconds: number;\n}\n\nexport default defineComponent({\n name: 'CardHistoryViewer',\n props: {\n cardId: {\n type: String,\n required: true,\n },\n courseId: {\n type: String,\n required: true,\n },\n userId: {\n type: String,\n required: true,\n },\n userDB: {\n type: Object as PropType<UserDBReader>,\n required: true,\n },\n },\n data() {\n return {\n history: [] as FormattedRecord[],\n cardHistory: null as CardHistory<CardRecord> | null,\n bestIntervalHumanized: '',\n loading: false,\n error: null as string | null,\n headers: [\n { title: 'Timestamp', key: 'formattedTimeStamp' },\n { title: 'Interval', key: 'userFriendlyInterval' },\n { title: 'Time Spent (s)', key: 'timeSpentSeconds' },\n { title: 'Correct?', key: 'isCorrect' },\n { title: 'Performance', key: 'performance' },\n { title: 'Prior Attempts', key: 'priorAttemps' },\n { title: 'User Answer', key: 'userAnswer' },\n ],\n };\n },\n watch: {\n cardId: {\n immediate: true,\n handler(newCardId) {\n if (newCardId && this.userDB) {\n this.fetchHistory();\n }\n },\n },\n userDB: {\n handler(newUserDB) {\n if (newUserDB && this.cardId) {\n this.fetchHistory();\n }\n },\n },\n },\n methods: {\n async fetchHistory() {\n this.loading = true;\n this.error = null;\n try {\n const cardHistoryID = getCardHistoryID(this.courseId, this.cardId);\n const historyDoc: CardHistory<CardRecord> = await this.userDB.get(cardHistoryID);\n this.cardHistory = historyDoc;\n this.bestIntervalHumanized = moment.duration(historyDoc.bestInterval, 'seconds').humanize();\n\n // Sort records by timestamp and format them\n const sortedRecords = [...historyDoc.records].sort(\n (a, b) => moment(a.timeStamp).valueOf() - moment(b.timeStamp).valueOf()\n );\n\n this.history = sortedRecords.map((record, index) => {\n const currentTime = moment(record.timeStamp);\n const formatted: FormattedRecord = {\n ...record,\n formattedTimeStamp: currentTime.format('YYYY-MM-DD HH:mm:ss'),\n timeSpentSeconds: Math.round(record.timeSpent / 1000),\n isCorrect: (record as any).isCorrect,\n performance: (record as any).performance,\n priorAttemps: (record as any).priorAttemps,\n userAnswer: (record as any).userAnswer,\n };\n\n // Calculate interval from previous record\n if (index > 0) {\n const previousTime = moment(sortedRecords[index - 1].timeStamp);\n const intervalSeconds = currentTime.diff(previousTime, 'seconds', true);\n formatted.intervalFromPrevious = Math.round(intervalSeconds * 100) / 100;\n formatted.userFriendlyInterval = `${formatted.intervalFromPrevious} sec (${moment.duration(intervalSeconds, 'seconds').humanize()})`;\n }\n\n return formatted;\n });\n } catch (e) {\n this.error = 'Error fetching card history.';\n console.error(e);\n } finally {\n this.loading = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.error-message {\n color: #f44336;\n padding: 16px;\n background-color: #ffebee;\n border-radius: 4px;\n margin: 8px 0;\n}\n\n.negative-interval {\n color: #f44336;\n font-weight: bold;\n background-color: #ffebee;\n padding: 2px 4px;\n border-radius: 3px;\n}\n\n.invalid-timestamp {\n color: #f44336;\n font-weight: bold;\n}\n\n.card-history-viewer {\n margin: 16px 0;\n}\n</style>\n","import { type DefineComponent, defineComponent } from 'vue';\nimport { FieldType, DataShape, ViewData, Answer, Evaluation } from '@vue-skuilder/common';\n\n// [ ] #vue3 - post migration, specify this more precisely (no longer a hodge-podge)\nexport type ViewComponent =\n | DefineComponent<unknown, unknown, unknown>\n | ReturnType<typeof defineComponent>;\n\nexport function isDefineComponent(\n v: ViewComponent\n): v is DefineComponent<unknown, unknown, unknown> {\n return (v as DefineComponent<unknown, unknown, unknown>).__isFragment !== undefined;\n}\n\n// export function isComponentOptions(v: ViewComponent): v is ComponentOptions<any> {\n// return (v as ComponentOptions<Vue>).name !== undefined;\n// }\n\n// tslint:disable-next-line:max-classes-per-file\nexport abstract class Displayable {\n public static dataShapes: DataShape[];\n\n public static views: Array<ViewComponent>;\n public static seedData?: Array<unknown>;\n /**\n * True if this displayable content type is meant to have\n * user-submitted questions. False if supplied seedData array\n * is comprehensive for the content type. EG, a SingleDigitAddition\n * type may comprehensively supply 0+0,0+1,...,9+9 as its seed\n * data, and not want any user input.\n */\n public static acceptsUserData: boolean = true;\n\n /**\n *\n */\n constructor(viewData: ViewData[]) {\n if (viewData.length === 0) {\n throw new Error(`\nDisplayable Constructor was called with no view Data.\n `);\n }\n validateData(this.dataShapes(), viewData);\n }\n\n public abstract dataShapes(): DataShape[];\n public abstract views(): Array<ViewComponent>;\n}\n\nfunction validateData(shape: DataShape[], data: ViewData[]) {\n for (let i = 0; i < shape.length; i++) {\n shape[i].fields.forEach((field, j) => {\n console.log(`[Displayable] shape[${i}].field[${j}]:\\n ${JSON.stringify(field)}`);\n if (data[i][field.name] === undefined && field.type !== FieldType.MEDIA_UPLOADS) {\n // throw new Error(`field validation failed:\\n\\t${field.name}, (${field.type})`);\n console.warn(`[Displayable] missing data`);\n }\n });\n }\n}\n\n// tslint:disable-next-line:max-classes-per-file\nexport abstract class Question extends Displayable {\n /**\n * returns a yes/no evaluation of a user's answer. Informs the SRS\n * algorithm's decision to expand or reset a card's spacing\n * @param answer\n */\n protected abstract isCorrect(answer: Answer): boolean;\n /**\n * returns a number from [0,1] representing the user's performance on the question,\n * which informs elo adjustments and SRS multipliers\n *\n * @param answer the user's answer\n * @param timeSpent the time the user spent on the card in milliseconds\n * @returns a rating of the user's displayed skill, from 0-1\n */\n protected displayedSkill(answer: Answer, timeSpent: number): number {\n console.warn(`Question is running the reference implementation of displayedSkill.\n Consider overriding!`);\n // experts should answer this question in <= 5 secnods (5000 ms)\n const expertSpeed = 5000;\n const userSpeed = Math.min(timeSpent, 10 * expertSpeed);\n\n // if userResponse is > 10 x expertSpeed, discount as probably afk / distracted ?\n\n const speedPenalty = userSpeed / expertSpeed;\n const speedPenaltyMultiplier = userSpeed > expertSpeed ? Math.pow(0.8, speedPenalty) : 1;\n\n let ret = this.isCorrect(answer) ? 1 : 0;\n\n ret = ret * speedPenaltyMultiplier;\n\n return Math.min(ret, 1);\n }\n\n /**\n *\n * @param answer the student's answer\n * @param timeSpent the amount of time spent in ms\n * @returns\n */\n public evaluate(answer: Answer, timeSpent: number): Evaluation {\n return {\n isCorrect: this.isCorrect(answer),\n performance: this.displayedSkill(answer, timeSpent),\n };\n }\n\n /*\n TODO:\n\n This class and its interface are critical in this app - it defines the app's\n methodology, or its architecture of methodologies, for making inferences based on\n student performance.\n\n Some future directions:\n\n displayedSkill() should receive contextual data as well as the direct report of\n a user's interaction with this card. Context includes, eg, the status of the current\n study session (is the user on tilt? have there been leading Qs? have there been distractors?),\n the status of the user's interaction with the card (new? mature? history of resets?)\n\n displayedSkill() should reach out to a course (or just the card, or card-type?) in order to receive\n custom skill-dimension evaluators. EG: a card in the ear training course tagged 'articulation'\n could trigger a fetch of a particular evaluator fcn for articulation. In general, we\n want high-dimension evaluations.\n\n answers should be processed for the likelihood of non-substantive-incorrectness. eg,\n 7 * 4 = 8 is much more likely to be a typo than a substantive error when compared\n with 7 * 4 = 21\n */\n}\n","/**\n * Authentication API service for interacting with Express backend auth endpoints.\n * Uses configurable API base path from environment or runtime config.\n */\n\n// Global runtime configuration (set by consuming app)\ndeclare global {\n interface Window {\n __SKUILDER_CONFIG__?: {\n apiBase?: string;\n [key: string]: unknown;\n };\n }\n}\n\n// Get API base path: check env first, then runtime config, then fallback to empty\nconst getApiBase = (): string => {\n let source = 'fallback (empty string)';\n let result = '';\n\n // Try import.meta.env first (build-time, only works in consuming app builds)\n if (typeof import.meta !== 'undefined' && import.meta.env) {\n const base = import.meta.env.VITE_API_BASE_URL;\n if (base) {\n const cleaned = base.replace(/\\/$/, '');\n result = cleaned.startsWith('/') ? cleaned : `/${cleaned}`;\n source = 'import.meta.env.VITE_API_BASE_URL';\n }\n }\n\n // Fallback to runtime config (works in library builds)\n if (!result && typeof window !== 'undefined' && window.__SKUILDER_CONFIG__?.apiBase) {\n const base = window.__SKUILDER_CONFIG__.apiBase;\n const cleaned = base.replace(/\\/$/, '');\n result = cleaned.startsWith('/') ? cleaned : `/${cleaned}`;\n source = 'window.__SKUILDER_CONFIG__.apiBase';\n }\n\n console.log('[authAPI] getApiBase() called');\n console.log('[authAPI] source:', source);\n console.log('[authAPI] result:', result);\n\n return result;\n};\n\nexport interface AuthResponse {\n ok: boolean;\n error?: string;\n}\n\nexport interface VerifyEmailResponse extends AuthResponse {\n username?: string;\n}\n\nexport interface Entitlement {\n status: 'trial' | 'paid';\n registrationDate: string;\n purchaseDate?: string;\n expires?: string;\n}\n\nexport type UserEntitlements = Record<string, Entitlement>;\n\nexport interface UserStatusResponse extends AuthResponse {\n username?: string;\n status?: 'pending_verification' | 'verified' | 'suspended';\n email?: string | null;\n entitlements?: UserEntitlements;\n}\n\n/**\n * Triggers verification email send for a newly created account.\n *\n * @param username - Username of the newly created account\n * @param email - User's email address (passed directly to avoid race conditions with DB sync)\n * @param origin - Optional frontend origin URL (e.g., window.location.origin)\n * Used to construct the correct verification link in the email.\n * If not provided, backend falls back to APP_URL env var.\n */\nexport async function sendVerificationEmail(\n username: string,\n email: string,\n origin?: string\n): Promise<AuthResponse> {\n try {\n const body: { username: string; email: string; origin?: string } = { username, email };\n if (origin) {\n body.origin = origin;\n }\n\n const response = await fetch(`${getApiBase()}/auth/send-verification`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Send verification email error:', errorText);\n return {\n ok: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n return await response.json();\n } catch (error) {\n console.error('Send verification email fetch error:', error);\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Network error',\n };\n }\n}\n\n/**\n * Verify user email using token from magic link.\n */\nexport async function verifyEmail(token: string): Promise<VerifyEmailResponse> {\n try {\n const response = await fetch(`${getApiBase()}/auth/verify`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ token }),\n });\n\n if (!response.ok) {\n return {\n ok: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n return await response.json();\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Network error',\n };\n }\n}\n\n/**\n * Get current user's account status (verification status, email).\n * Requires valid AuthSession cookie.\n */\nexport async function getUserStatus(): Promise<UserStatusResponse> {\n try {\n const response = await fetch(`${getApiBase()}/auth/status`, {\n method: 'GET',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n });\n\n if (!response.ok) {\n return {\n ok: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n return await response.json();\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Network error',\n };\n }\n}\n\n/**\n * Request password reset email for user identified by email.\n *\n * @param email - User's email address\n * @param origin - Optional frontend origin URL (e.g., window.location.origin)\n * Used to construct the correct password reset link in the email.\n * If not provided, backend falls back to APP_URL env var.\n */\nexport async function requestPasswordReset(\n email: string,\n origin?: string\n): Promise<AuthResponse> {\n try {\n const body: { email: string; origin?: string } = { email };\n if (origin) {\n body.origin = origin;\n }\n\n const response = await fetch(`${getApiBase()}/auth/request-reset`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n return {\n ok: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n return await response.json();\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Network error',\n };\n }\n}\n\n/**\n * Reset password using token from magic link.\n */\nexport async function resetPassword(\n token: string,\n newPassword: string\n): Promise<AuthResponse> {\n try {\n const response = await fetch(`${getApiBase()}/auth/reset-password`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ token, newPassword }),\n });\n\n if (!response.ok) {\n return {\n ok: false,\n error: `HTTP ${response.status}: ${response.statusText}`,\n };\n }\n\n return await response.json();\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Network error',\n };\n }\n}\n","import { ref, computed } from 'vue';\nimport { getUserStatus } from '../services/authAPI';\nimport type { Entitlement, UserEntitlements } from '../services/authAPI';\n\nexport interface TrialStatus {\n isActive: boolean;\n isPaid: boolean;\n daysRemaining: number | null;\n expiresDate: string | null;\n}\n\n/**\n * Composable for managing user entitlements and trial status\n *\n * @param courseId - The course identifier to check entitlements for\n * @returns Object with entitlements data and helper methods\n *\n * @example\n * ```typescript\n * const { trialStatus, hasPremiumAccess, fetchEntitlements, loading } = useEntitlements('letterspractice-basic');\n *\n * onMounted(async () => {\n * await fetchEntitlements();\n * console.log('Days remaining:', trialStatus.value.daysRemaining);\n * });\n * ```\n */\nexport function useEntitlements(courseId: string) {\n const entitlements = ref<UserEntitlements>({});\n const loading = ref(false);\n const error = ref<string | null>(null);\n\n /**\n * Get trial status for the specified course\n */\n const trialStatus = computed<TrialStatus>(() => {\n const entitlement: Entitlement | undefined = entitlements.value[courseId];\n\n if (!entitlement) {\n return {\n isActive: false,\n isPaid: false,\n daysRemaining: null,\n expiresDate: null,\n };\n }\n\n if (entitlement.status === 'paid') {\n return {\n isActive: true,\n isPaid: true,\n daysRemaining: null,\n expiresDate: null,\n };\n }\n\n // Trial status\n const expiresDate = entitlement.expires;\n if (!expiresDate) {\n return {\n isActive: true,\n isPaid: false,\n daysRemaining: null,\n expiresDate: null,\n };\n }\n\n const expires = new Date(expiresDate);\n const now = new Date();\n const daysLeft = Math.ceil((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));\n\n return {\n isActive: daysLeft > 0,\n isPaid: false,\n daysRemaining: Math.max(0, daysLeft),\n expiresDate,\n };\n });\n\n /**\n * Whether the user has premium (paid) access to the course\n */\n const hasPremiumAccess = computed<boolean>(() => {\n return trialStatus.value.isPaid;\n });\n\n /**\n * Fetch entitlements from the backend\n */\n async function fetchEntitlements(): Promise<void> {\n loading.value = true;\n error.value = null;\n\n try {\n const result = await getUserStatus();\n if (result.ok) {\n entitlements.value = result.entitlements || {};\n } else {\n error.value = result.error || 'Failed to fetch entitlements';\n console.error('[useEntitlements] Error:', error.value);\n }\n } catch (e) {\n error.value = e instanceof Error ? e.message : 'Unknown error';\n console.error('[useEntitlements] Exception:', e);\n } finally {\n loading.value = false;\n }\n }\n\n return {\n entitlements,\n trialStatus,\n hasPremiumAccess,\n loading,\n error,\n fetchEntitlements,\n };\n}\n","<!-- @vue-skuilder/common-ui/src/components/StudySessionTimer.vue -->\n<template>\n <v-tooltip location=\"right\" :open-delay=\"0\" :close-delay=\"200\" color=\"secondary\" class=\"text-subtitle-1\">\n <template #activator=\"{ props }\">\n <div class=\"timer-container\" v-bind=\"props\" @mouseenter=\"hovered = true\" @mouseleave=\"hovered = false\">\n <v-progress-circular\n alt=\"Time remaining in study session\"\n size=\"64\"\n width=\"8\"\n rotate=\"0\"\n :color=\"timerColor\"\n :model-value=\"percentageRemaining\"\n >\n <v-btn\n v-if=\"timeRemaining > 0 && hovered\"\n icon\n color=\"transparent\"\n location=\"bottom left\"\n @click=\"addSessionTime\"\n >\n <v-icon size=\"large\">mdi-plus</v-icon>\n </v-btn>\n </v-progress-circular>\n </div>\n </template>\n {{ formattedTimeRemaining }}\n </v-tooltip>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, computed, ref } from 'vue';\n\nexport default defineComponent({\n name: 'StudySessionTimer',\n\n props: {\n /**\n * Time remaining in seconds\n */\n timeRemaining: {\n type: Number,\n required: true,\n },\n /**\n * Total session time limit in minutes\n */\n sessionTimeLimit: {\n type: Number,\n required: true,\n default: 5,\n },\n },\n\n emits: ['add-time'],\n\n setup(props, { emit }) {\n const hovered = ref(false);\n\n /**\n * Formats the time remaining into a readable string\n */\n const formattedTimeRemaining = computed(() => {\n let timeString = '';\n const seconds = props.timeRemaining;\n\n if (seconds > 60) {\n timeString = Math.floor(seconds / 60).toString() + ':';\n }\n\n const secondsRemaining = seconds % 60;\n timeString += secondsRemaining >= 10 ? secondsRemaining : '0' + secondsRemaining;\n\n if (seconds <= 60) {\n timeString += ' seconds';\n }\n\n timeString += ' left!';\n\n return timeString;\n });\n\n /**\n * Calculates the percentage of time remaining for the progress indicator\n */\n const percentageRemaining = computed(() => {\n return props.timeRemaining > 60\n ? 100 * (props.timeRemaining / (60 * props.sessionTimeLimit))\n : 100 * (props.timeRemaining / 60);\n });\n\n /**\n * Determines the color of the timer based on time remaining\n */\n const timerColor = computed(() => {\n return props.timeRemaining > 60 ? 'primary' : 'orange darken-3';\n });\n\n /**\n * Handles adding time to the session\n */\n const addSessionTime = () => {\n emit('add-time');\n };\n\n return {\n hovered,\n formattedTimeRemaining,\n percentageRemaining,\n timerColor,\n addSessionTime,\n };\n },\n});\n</script>\n\n<style scoped>\n.timer-container {\n display: inline-flex;\n cursor: pointer;\n}\n</style>\n","<!-- @vue-skuilder/common-ui/src/components/StudySessionTimer.vue -->\n<template>\n <v-tooltip location=\"right\" :open-delay=\"0\" :close-delay=\"200\" color=\"secondary\" class=\"text-subtitle-1\">\n <template #activator=\"{ props }\">\n <div class=\"timer-container\" v-bind=\"props\" @mouseenter=\"hovered = true\" @mouseleave=\"hovered = false\">\n <v-progress-circular\n alt=\"Time remaining in study session\"\n size=\"64\"\n width=\"8\"\n rotate=\"0\"\n :color=\"timerColor\"\n :model-value=\"percentageRemaining\"\n >\n <v-btn\n v-if=\"timeRemaining > 0 && hovered\"\n icon\n color=\"transparent\"\n location=\"bottom left\"\n @click=\"addSessionTime\"\n >\n <v-icon size=\"large\">mdi-plus</v-icon>\n </v-btn>\n </v-progress-circular>\n </div>\n </template>\n {{ formattedTimeRemaining }}\n </v-tooltip>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, computed, ref } from 'vue';\n\nexport default defineComponent({\n name: 'StudySessionTimer',\n\n props: {\n /**\n * Time remaining in seconds\n */\n timeRemaining: {\n type: Number,\n required: true,\n },\n /**\n * Total session time limit in minutes\n */\n sessionTimeLimit: {\n type: Number,\n required: true,\n default: 5,\n },\n },\n\n emits: ['add-time'],\n\n setup(props, { emit }) {\n const hovered = ref(false);\n\n /**\n * Formats the time remaining into a readable string\n */\n const formattedTimeRemaining = computed(() => {\n let timeString = '';\n const seconds = props.timeRemaining;\n\n if (seconds > 60) {\n timeString = Math.floor(seconds / 60).toString() + ':';\n }\n\n const secondsRemaining = seconds % 60;\n timeString += secondsRemaining >= 10 ? secondsRemaining : '0' + secondsRemaining;\n\n if (seconds <= 60) {\n timeString += ' seconds';\n }\n\n timeString += ' left!';\n\n return timeString;\n });\n\n /**\n * Calculates the percentage of time remaining for the progress indicator\n */\n const percentageRemaining = computed(() => {\n return props.timeRemaining > 60\n ? 100 * (props.timeRemaining / (60 * props.sessionTimeLimit))\n : 100 * (props.timeRemaining / 60);\n });\n\n /**\n * Determines the color of the timer based on time remaining\n */\n const timerColor = computed(() => {\n return props.timeRemaining > 60 ? 'primary' : 'orange darken-3';\n });\n\n /**\n * Handles adding time to the session\n */\n const addSessionTime = () => {\n emit('add-time');\n };\n\n return {\n hovered,\n formattedTimeRemaining,\n percentageRemaining,\n timerColor,\n addSessionTime,\n };\n },\n});\n</script>\n\n<style scoped>\n.timer-container {\n display: inline-flex;\n cursor: pointer;\n}\n</style>\n","<template>\n <!-- Frameless mode: direct render -->\n <component\n v-if=\"frameless\"\n :is=\"view\"\n ref=\"activeView\"\n :key=\"course_id + '-' + card_id + '-' + sessionOrder\"\n :data=\"data\"\n :modify-difficulty=\"user_elo.global.score - card_elo\"\n class=\"cardView\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"$emit('requestReplan', $event)\"\n @ready-to-advance=\"$emit('readyToAdvance')\"\n />\n\n <!-- Traditional mode: v-card wrapper -->\n <v-card v-else elevation=\"12\">\n <component\n :is=\"view\"\n ref=\"activeView\"\n :key=\"course_id + '-' + card_id + '-' + sessionOrder\"\n :data=\"data\"\n :modify-difficulty=\"user_elo.global.score - card_elo\"\n class=\"cardView ma-2 pa-2\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"$emit('requestReplan', $event)\"\n @ready-to-advance=\"$emit('readyToAdvance')\"\n />\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { CardRecord } from '@vue-skuilder/db';\nimport { CourseElo, ViewData } from '@vue-skuilder/common';\nimport { ViewComponent } from '../../composables';\n\ninterface CardViewerRefs {\n activeView: ViewComponent;\n}\n\nexport default defineComponent({\n name: 'CardViewer',\n\n ref: {} as CardViewerRefs,\n\n props: {\n sessionOrder: {\n type: Number,\n required: false,\n default: 0,\n },\n card_id: {\n type: String as () => PouchDB.Core.DocumentId,\n required: true,\n default: '',\n },\n course_id: {\n type: String,\n required: true,\n default: '',\n },\n view: {\n type: [Function, Object] as PropType<ViewComponent>,\n required: true,\n },\n data: {\n type: Array as () => ViewData[],\n required: true,\n },\n user_elo: {\n type: Object as () => CourseElo,\n default: () => ({\n global: {\n score: 1000,\n count: 0,\n },\n tags: {},\n misc: {},\n }),\n },\n card_elo: {\n type: Number,\n default: 1000,\n },\n frameless: {\n type: Boolean,\n default: false,\n },\n },\n\n emits: ['emitResponse', 'requestReplan', 'readyToAdvance'],\n\n methods: {\n processResponse(r: CardRecord): void {\n console.log(`\n Card was displayed at ${r.timeStamp}\n User spent ${r.timeSpent} milliseconds with the card.\n `);\n this.$emit('emitResponse', r);\n },\n },\n});\n</script>\n\n<style scoped>\n.cardView {\n padding: 15px;\n border-radius: 8px;\n}\n\n</style>\n","<template>\n <!-- Frameless mode: direct render -->\n <component\n v-if=\"frameless\"\n :is=\"view\"\n ref=\"activeView\"\n :key=\"course_id + '-' + card_id + '-' + sessionOrder\"\n :data=\"data\"\n :modify-difficulty=\"user_elo.global.score - card_elo\"\n class=\"cardView\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"$emit('requestReplan', $event)\"\n @ready-to-advance=\"$emit('readyToAdvance')\"\n />\n\n <!-- Traditional mode: v-card wrapper -->\n <v-card v-else elevation=\"12\">\n <component\n :is=\"view\"\n ref=\"activeView\"\n :key=\"course_id + '-' + card_id + '-' + sessionOrder\"\n :data=\"data\"\n :modify-difficulty=\"user_elo.global.score - card_elo\"\n class=\"cardView ma-2 pa-2\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"$emit('requestReplan', $event)\"\n @ready-to-advance=\"$emit('readyToAdvance')\"\n />\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { CardRecord } from '@vue-skuilder/db';\nimport { CourseElo, ViewData } from '@vue-skuilder/common';\nimport { ViewComponent } from '../../composables';\n\ninterface CardViewerRefs {\n activeView: ViewComponent;\n}\n\nexport default defineComponent({\n name: 'CardViewer',\n\n ref: {} as CardViewerRefs,\n\n props: {\n sessionOrder: {\n type: Number,\n required: false,\n default: 0,\n },\n card_id: {\n type: String as () => PouchDB.Core.DocumentId,\n required: true,\n default: '',\n },\n course_id: {\n type: String,\n required: true,\n default: '',\n },\n view: {\n type: [Function, Object] as PropType<ViewComponent>,\n required: true,\n },\n data: {\n type: Array as () => ViewData[],\n required: true,\n },\n user_elo: {\n type: Object as () => CourseElo,\n default: () => ({\n global: {\n score: 1000,\n count: 0,\n },\n tags: {},\n misc: {},\n }),\n },\n card_elo: {\n type: Number,\n default: 1000,\n },\n frameless: {\n type: Boolean,\n default: false,\n },\n },\n\n emits: ['emitResponse', 'requestReplan', 'readyToAdvance'],\n\n methods: {\n processResponse(r: CardRecord): void {\n console.log(`\n Card was displayed at ${r.timeStamp}\n User spent ${r.timeSpent} milliseconds with the card.\n `);\n this.$emit('emitResponse', r);\n },\n },\n});\n</script>\n\n<style scoped>\n.cardView {\n padding: 15px;\n border-radius: 8px;\n}\n\n</style>\n","<template>\n <v-card v-if=\"sessionController\" class=\"session-debug ma-2\" elevation=\"2\">\n <v-card-title class=\"text-caption bg-grey-darken-3\">\n Session Controller Debug\n <v-spacer></v-spacer>\n <v-icon size=\"small\">mdi-bug</v-icon>\n </v-card-title>\n\n <v-card-text class=\"pa-2\">\n <v-row dense>\n <!-- Review Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-calendar-check</v-icon>\n <strong>Review Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.reviewQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.reviewQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.reviewQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.reviewQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.reviewQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.reviewQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n\n <!-- New Cards Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-file-document-plus</v-icon>\n <strong>New Cards Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.newQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.newQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.newQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.newQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.newQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.newQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n\n <!-- Failed Cards Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-alert-circle</v-icon>\n <strong>Failed Cards Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.failedQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.failedQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.failedQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.failedQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.failedQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.failedQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n </v-row>\n\n <!-- Hydrated Cards Cache -->\n <v-row dense>\n <v-col cols=\"12\">\n <v-divider class=\"my-2\"></v-divider>\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-database</v-icon>\n <strong>Hydrated Cards Cache</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Cached: {{ debugInfo.hydratedCache.count }}</span>\n <span class=\"text-caption ml-2\">Failed Cache: {{ debugInfo.hydratedCache.failedCacheSize }}</span>\n </div>\n <div v-if=\"debugInfo.hydratedCache.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.hydratedCache.items.slice(0, 8)\"\n :key=\"idx\"\n class=\"debug-item d-inline-block mr-3\"\n >\n <span class=\"text-caption\">{{ item.courseID }}::{{ item.cardID }}</span>\n </div>\n <div v-if=\"debugInfo.hydratedCache.items.length > 8\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.hydratedCache.items.length - 8 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n </v-row>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, computed, ref, onMounted, onUnmounted } from 'vue';\nimport { SessionController } from '@vue-skuilder/db';\n\ninterface QueueDebugInfo {\n length: number;\n dequeueCount: number;\n items: Array<{ courseID: string; cardID: string; status: string }>;\n}\n\ninterface HydratedCacheInfo {\n count: number;\n failedCacheSize: number;\n items: Array<{ courseID: string; cardID: string }>;\n}\n\nexport interface SessionDebugInfo {\n reviewQueue: QueueDebugInfo;\n newQueue: QueueDebugInfo;\n failedQueue: QueueDebugInfo;\n hydratedCache: HydratedCacheInfo;\n}\n\nexport default defineComponent({\n name: 'SessionControllerDebug',\n\n props: {\n sessionController: {\n type: Object as PropType<SessionController<any> | null>,\n required: true,\n },\n },\n\n setup(props) {\n const refreshTrigger = ref(0);\n let pollInterval: NodeJS.Timeout | null = null;\n\n onMounted(() => {\n // Poll every 500ms to update debug display\n pollInterval = setInterval(() => {\n refreshTrigger.value++;\n }, 500);\n });\n\n onUnmounted(() => {\n if (pollInterval) {\n clearInterval(pollInterval);\n }\n });\n\n const debugInfo = computed((): SessionDebugInfo => {\n // Create dependency on refreshTrigger to force updates\n refreshTrigger.value;\n\n if (!props.sessionController) {\n return {\n reviewQueue: { length: 0, dequeueCount: 0, items: [] },\n newQueue: { length: 0, dequeueCount: 0, items: [] },\n failedQueue: { length: 0, dequeueCount: 0, items: [] },\n hydratedCache: { count: 0, failedCacheSize: 0, items: [] },\n };\n }\n\n return props.sessionController.getDebugInfo();\n });\n\n return {\n debugInfo,\n };\n },\n});\n</script>\n\n<style scoped>\n.session-debug {\n font-family: 'Courier New', monospace;\n font-size: 0.75rem;\n max-height: 400px;\n overflow-y: auto;\n}\n\n.debug-section {\n padding: 8px;\n background-color: rgba(255, 255, 255, 0.05);\n border-radius: 4px;\n min-height: 120px;\n}\n\n.debug-header {\n display: flex;\n align-items: center;\n margin-bottom: 4px;\n padding-bottom: 4px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.debug-stats {\n margin-bottom: 8px;\n display: flex;\n gap: 8px;\n}\n\n.debug-items {\n margin-top: 4px;\n padding-left: 8px;\n}\n\n.debug-item {\n padding: 2px 0;\n border-left: 2px solid rgba(255, 255, 255, 0.2);\n padding-left: 6px;\n margin-bottom: 2px;\n}\n</style>\n","<template>\n <v-card v-if=\"sessionController\" class=\"session-debug ma-2\" elevation=\"2\">\n <v-card-title class=\"text-caption bg-grey-darken-3\">\n Session Controller Debug\n <v-spacer></v-spacer>\n <v-icon size=\"small\">mdi-bug</v-icon>\n </v-card-title>\n\n <v-card-text class=\"pa-2\">\n <v-row dense>\n <!-- Review Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-calendar-check</v-icon>\n <strong>Review Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.reviewQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.reviewQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.reviewQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.reviewQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.reviewQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.reviewQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n\n <!-- New Cards Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-file-document-plus</v-icon>\n <strong>New Cards Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.newQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.newQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.newQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.newQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.newQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.newQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n\n <!-- Failed Cards Queue -->\n <v-col cols=\"12\" md=\"4\">\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-alert-circle</v-icon>\n <strong>Failed Cards Queue</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Length: {{ debugInfo.failedQueue.length }}</span>\n <span class=\"text-caption ml-2\">Dequeued: {{ debugInfo.failedQueue.dequeueCount }}</span>\n </div>\n <div v-if=\"debugInfo.failedQueue.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.failedQueue.items.slice(0, 5)\"\n :key=\"idx\"\n class=\"debug-item\"\n >\n <span class=\"text-caption\">{{ idx }}: {{ item.courseID }}::{{ item.cardID }}</span>\n <span class=\"text-caption text-grey ml-1\">({{ item.status }})</span>\n </div>\n <div v-if=\"debugInfo.failedQueue.items.length > 5\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.failedQueue.items.length - 5 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n </v-row>\n\n <!-- Hydrated Cards Cache -->\n <v-row dense>\n <v-col cols=\"12\">\n <v-divider class=\"my-2\"></v-divider>\n <div class=\"debug-section\">\n <div class=\"debug-header\">\n <v-icon size=\"x-small\" class=\"mr-1\">mdi-database</v-icon>\n <strong>Hydrated Cards Cache</strong>\n </div>\n <div class=\"debug-stats\">\n <span class=\"text-caption\">Cached: {{ debugInfo.hydratedCache.count }}</span>\n <span class=\"text-caption ml-2\">Failed Cache: {{ debugInfo.hydratedCache.failedCacheSize }}</span>\n </div>\n <div v-if=\"debugInfo.hydratedCache.items.length > 0\" class=\"debug-items\">\n <div\n v-for=\"(item, idx) in debugInfo.hydratedCache.items.slice(0, 8)\"\n :key=\"idx\"\n class=\"debug-item d-inline-block mr-3\"\n >\n <span class=\"text-caption\">{{ item.courseID }}::{{ item.cardID }}</span>\n </div>\n <div v-if=\"debugInfo.hydratedCache.items.length > 8\" class=\"text-caption text-grey\">\n ... +{{ debugInfo.hydratedCache.items.length - 8 }} more\n </div>\n </div>\n <div v-else class=\"text-caption text-grey\">\n (empty)\n </div>\n </div>\n </v-col>\n </v-row>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, computed, ref, onMounted, onUnmounted } from 'vue';\nimport { SessionController } from '@vue-skuilder/db';\n\ninterface QueueDebugInfo {\n length: number;\n dequeueCount: number;\n items: Array<{ courseID: string; cardID: string; status: string }>;\n}\n\ninterface HydratedCacheInfo {\n count: number;\n failedCacheSize: number;\n items: Array<{ courseID: string; cardID: string }>;\n}\n\nexport interface SessionDebugInfo {\n reviewQueue: QueueDebugInfo;\n newQueue: QueueDebugInfo;\n failedQueue: QueueDebugInfo;\n hydratedCache: HydratedCacheInfo;\n}\n\nexport default defineComponent({\n name: 'SessionControllerDebug',\n\n props: {\n sessionController: {\n type: Object as PropType<SessionController<any> | null>,\n required: true,\n },\n },\n\n setup(props) {\n const refreshTrigger = ref(0);\n let pollInterval: NodeJS.Timeout | null = null;\n\n onMounted(() => {\n // Poll every 500ms to update debug display\n pollInterval = setInterval(() => {\n refreshTrigger.value++;\n }, 500);\n });\n\n onUnmounted(() => {\n if (pollInterval) {\n clearInterval(pollInterval);\n }\n });\n\n const debugInfo = computed((): SessionDebugInfo => {\n // Create dependency on refreshTrigger to force updates\n refreshTrigger.value;\n\n if (!props.sessionController) {\n return {\n reviewQueue: { length: 0, dequeueCount: 0, items: [] },\n newQueue: { length: 0, dequeueCount: 0, items: [] },\n failedQueue: { length: 0, dequeueCount: 0, items: [] },\n hydratedCache: { count: 0, failedCacheSize: 0, items: [] },\n };\n }\n\n return props.sessionController.getDebugInfo();\n });\n\n return {\n debugInfo,\n };\n },\n});\n</script>\n\n<style scoped>\n.session-debug {\n font-family: 'Courier New', monospace;\n font-size: 0.75rem;\n max-height: 400px;\n overflow-y: auto;\n}\n\n.debug-section {\n padding: 8px;\n background-color: rgba(255, 255, 255, 0.05);\n border-radius: 4px;\n min-height: 120px;\n}\n\n.debug-header {\n display: flex;\n align-items: center;\n margin-bottom: 4px;\n padding-bottom: 4px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.debug-stats {\n margin-bottom: 8px;\n display: flex;\n gap: 8px;\n}\n\n.debug-items {\n margin-top: 4px;\n padding-left: 8px;\n}\n\n.debug-item {\n padding: 2px 0;\n border-left: 2px solid rgba(255, 255, 255, 0.2);\n padding-left: 6px;\n margin-bottom: 2px;\n}\n</style>\n","// canvas-confetti v1.9.3 built on 2024-04-30T22:19:17.794Z\nvar module = {};\n\n// source content\n/* globals Map */\n\n(function main(global, module, isWorker, workerSize) {\n var canUseWorker = !!(\n global.Worker &&\n global.Blob &&\n global.Promise &&\n global.OffscreenCanvas &&\n global.OffscreenCanvasRenderingContext2D &&\n global.HTMLCanvasElement &&\n global.HTMLCanvasElement.prototype.transferControlToOffscreen &&\n global.URL &&\n global.URL.createObjectURL);\n\n var canUsePaths = typeof Path2D === 'function' && typeof DOMMatrix === 'function';\n var canDrawBitmap = (function () {\n // this mostly supports ssr\n if (!global.OffscreenCanvas) {\n return false;\n }\n\n var canvas = new OffscreenCanvas(1, 1);\n var ctx = canvas.getContext('2d');\n ctx.fillRect(0, 0, 1, 1);\n var bitmap = canvas.transferToImageBitmap();\n\n try {\n ctx.createPattern(bitmap, 'no-repeat');\n } catch (e) {\n return false;\n }\n\n return true;\n })();\n\n function noop() {}\n\n // create a promise if it exists, otherwise, just\n // call the function directly\n function promise(func) {\n var ModulePromise = module.exports.Promise;\n var Prom = ModulePromise !== void 0 ? ModulePromise : global.Promise;\n\n if (typeof Prom === 'function') {\n return new Prom(func);\n }\n\n func(noop, noop);\n\n return null;\n }\n\n var bitmapMapper = (function (skipTransform, map) {\n // see https://github.com/catdad/canvas-confetti/issues/209\n // creating canvases is actually pretty expensive, so we should create a\n // 1:1 map for bitmap:canvas, so that we can animate the confetti in\n // a performant manner, but also not store them forever so that we don't\n // have a memory leak\n return {\n transform: function(bitmap) {\n if (skipTransform) {\n return bitmap;\n }\n\n if (map.has(bitmap)) {\n return map.get(bitmap);\n }\n\n var canvas = new OffscreenCanvas(bitmap.width, bitmap.height);\n var ctx = canvas.getContext('2d');\n ctx.drawImage(bitmap, 0, 0);\n\n map.set(bitmap, canvas);\n\n return canvas;\n },\n clear: function () {\n map.clear();\n }\n };\n })(canDrawBitmap, new Map());\n\n var raf = (function () {\n var TIME = Math.floor(1000 / 60);\n var frame, cancel;\n var frames = {};\n var lastFrameTime = 0;\n\n if (typeof requestAnimationFrame === 'function' && typeof cancelAnimationFrame === 'function') {\n frame = function (cb) {\n var id = Math.random();\n\n frames[id] = requestAnimationFrame(function onFrame(time) {\n if (lastFrameTime === time || lastFrameTime + TIME - 1 < time) {\n lastFrameTime = time;\n delete frames[id];\n\n cb();\n } else {\n frames[id] = requestAnimationFrame(onFrame);\n }\n });\n\n return id;\n };\n cancel = function (id) {\n if (frames[id]) {\n cancelAnimationFrame(frames[id]);\n }\n };\n } else {\n frame = function (cb) {\n return setTimeout(cb, TIME);\n };\n cancel = function (timer) {\n return clearTimeout(timer);\n };\n }\n\n return { frame: frame, cancel: cancel };\n }());\n\n var getWorker = (function () {\n var worker;\n var prom;\n var resolves = {};\n\n function decorate(worker) {\n function execute(options, callback) {\n worker.postMessage({ options: options || {}, callback: callback });\n }\n worker.init = function initWorker(canvas) {\n var offscreen = canvas.transferControlToOffscreen();\n worker.postMessage({ canvas: offscreen }, [offscreen]);\n };\n\n worker.fire = function fireWorker(options, size, done) {\n if (prom) {\n execute(options, null);\n return prom;\n }\n\n var id = Math.random().toString(36).slice(2);\n\n prom = promise(function (resolve) {\n function workerDone(msg) {\n if (msg.data.callback !== id) {\n return;\n }\n\n delete resolves[id];\n worker.removeEventListener('message', workerDone);\n\n prom = null;\n\n bitmapMapper.clear();\n\n done();\n resolve();\n }\n\n worker.addEventListener('message', workerDone);\n execute(options, id);\n\n resolves[id] = workerDone.bind(null, { data: { callback: id }});\n });\n\n return prom;\n };\n\n worker.reset = function resetWorker() {\n worker.postMessage({ reset: true });\n\n for (var id in resolves) {\n resolves[id]();\n delete resolves[id];\n }\n };\n }\n\n return function () {\n if (worker) {\n return worker;\n }\n\n if (!isWorker && canUseWorker) {\n var code = [\n 'var CONFETTI, SIZE = {}, module = {};',\n '(' + main.toString() + ')(this, module, true, SIZE);',\n 'onmessage = function(msg) {',\n ' if (msg.data.options) {',\n ' CONFETTI(msg.data.options).then(function () {',\n ' if (msg.data.callback) {',\n ' postMessage({ callback: msg.data.callback });',\n ' }',\n ' });',\n ' } else if (msg.data.reset) {',\n ' CONFETTI && CONFETTI.reset();',\n ' } else if (msg.data.resize) {',\n ' SIZE.width = msg.data.resize.width;',\n ' SIZE.height = msg.data.resize.height;',\n ' } else if (msg.data.canvas) {',\n ' SIZE.width = msg.data.canvas.width;',\n ' SIZE.height = msg.data.canvas.height;',\n ' CONFETTI = module.exports.create(msg.data.canvas);',\n ' }',\n '}',\n ].join('\\n');\n try {\n worker = new Worker(URL.createObjectURL(new Blob([code])));\n } catch (e) {\n // eslint-disable-next-line no-console\n typeof console !== undefined && typeof console.warn === 'function' ? console.warn('🎊 Could not load worker', e) : null;\n\n return null;\n }\n\n decorate(worker);\n }\n\n return worker;\n };\n })();\n\n var defaults = {\n particleCount: 50,\n angle: 90,\n spread: 45,\n startVelocity: 45,\n decay: 0.9,\n gravity: 1,\n drift: 0,\n ticks: 200,\n x: 0.5,\n y: 0.5,\n shapes: ['square', 'circle'],\n zIndex: 100,\n colors: [\n '#26ccff',\n '#a25afd',\n '#ff5e7e',\n '#88ff5a',\n '#fcff42',\n '#ffa62d',\n '#ff36ff'\n ],\n // probably should be true, but back-compat\n disableForReducedMotion: false,\n scalar: 1\n };\n\n function convert(val, transform) {\n return transform ? transform(val) : val;\n }\n\n function isOk(val) {\n return !(val === null || val === undefined);\n }\n\n function prop(options, name, transform) {\n return convert(\n options && isOk(options[name]) ? options[name] : defaults[name],\n transform\n );\n }\n\n function onlyPositiveInt(number){\n return number < 0 ? 0 : Math.floor(number);\n }\n\n function randomInt(min, max) {\n // [min, max)\n return Math.floor(Math.random() * (max - min)) + min;\n }\n\n function toDecimal(str) {\n return parseInt(str, 16);\n }\n\n function colorsToRgb(colors) {\n return colors.map(hexToRgb);\n }\n\n function hexToRgb(str) {\n var val = String(str).replace(/[^0-9a-f]/gi, '');\n\n if (val.length < 6) {\n val = val[0]+val[0]+val[1]+val[1]+val[2]+val[2];\n }\n\n return {\n r: toDecimal(val.substring(0,2)),\n g: toDecimal(val.substring(2,4)),\n b: toDecimal(val.substring(4,6))\n };\n }\n\n function getOrigin(options) {\n var origin = prop(options, 'origin', Object);\n origin.x = prop(origin, 'x', Number);\n origin.y = prop(origin, 'y', Number);\n\n return origin;\n }\n\n function setCanvasWindowSize(canvas) {\n canvas.width = document.documentElement.clientWidth;\n canvas.height = document.documentElement.clientHeight;\n }\n\n function setCanvasRectSize(canvas) {\n var rect = canvas.getBoundingClientRect();\n canvas.width = rect.width;\n canvas.height = rect.height;\n }\n\n function getCanvas(zIndex) {\n var canvas = document.createElement('canvas');\n\n canvas.style.position = 'fixed';\n canvas.style.top = '0px';\n canvas.style.left = '0px';\n canvas.style.pointerEvents = 'none';\n canvas.style.zIndex = zIndex;\n\n return canvas;\n }\n\n function ellipse(context, x, y, radiusX, radiusY, rotation, startAngle, endAngle, antiClockwise) {\n context.save();\n context.translate(x, y);\n context.rotate(rotation);\n context.scale(radiusX, radiusY);\n context.arc(0, 0, 1, startAngle, endAngle, antiClockwise);\n context.restore();\n }\n\n function randomPhysics(opts) {\n var radAngle = opts.angle * (Math.PI / 180);\n var radSpread = opts.spread * (Math.PI / 180);\n\n return {\n x: opts.x,\n y: opts.y,\n wobble: Math.random() * 10,\n wobbleSpeed: Math.min(0.11, Math.random() * 0.1 + 0.05),\n velocity: (opts.startVelocity * 0.5) + (Math.random() * opts.startVelocity),\n angle2D: -radAngle + ((0.5 * radSpread) - (Math.random() * radSpread)),\n tiltAngle: (Math.random() * (0.75 - 0.25) + 0.25) * Math.PI,\n color: opts.color,\n shape: opts.shape,\n tick: 0,\n totalTicks: opts.ticks,\n decay: opts.decay,\n drift: opts.drift,\n random: Math.random() + 2,\n tiltSin: 0,\n tiltCos: 0,\n wobbleX: 0,\n wobbleY: 0,\n gravity: opts.gravity * 3,\n ovalScalar: 0.6,\n scalar: opts.scalar,\n flat: opts.flat\n };\n }\n\n function updateFetti(context, fetti) {\n fetti.x += Math.cos(fetti.angle2D) * fetti.velocity + fetti.drift;\n fetti.y += Math.sin(fetti.angle2D) * fetti.velocity + fetti.gravity;\n fetti.velocity *= fetti.decay;\n\n if (fetti.flat) {\n fetti.wobble = 0;\n fetti.wobbleX = fetti.x + (10 * fetti.scalar);\n fetti.wobbleY = fetti.y + (10 * fetti.scalar);\n\n fetti.tiltSin = 0;\n fetti.tiltCos = 0;\n fetti.random = 1;\n } else {\n fetti.wobble += fetti.wobbleSpeed;\n fetti.wobbleX = fetti.x + ((10 * fetti.scalar) * Math.cos(fetti.wobble));\n fetti.wobbleY = fetti.y + ((10 * fetti.scalar) * Math.sin(fetti.wobble));\n\n fetti.tiltAngle += 0.1;\n fetti.tiltSin = Math.sin(fetti.tiltAngle);\n fetti.tiltCos = Math.cos(fetti.tiltAngle);\n fetti.random = Math.random() + 2;\n }\n\n var progress = (fetti.tick++) / fetti.totalTicks;\n\n var x1 = fetti.x + (fetti.random * fetti.tiltCos);\n var y1 = fetti.y + (fetti.random * fetti.tiltSin);\n var x2 = fetti.wobbleX + (fetti.random * fetti.tiltCos);\n var y2 = fetti.wobbleY + (fetti.random * fetti.tiltSin);\n\n context.fillStyle = 'rgba(' + fetti.color.r + ', ' + fetti.color.g + ', ' + fetti.color.b + ', ' + (1 - progress) + ')';\n\n context.beginPath();\n\n if (canUsePaths && fetti.shape.type === 'path' && typeof fetti.shape.path === 'string' && Array.isArray(fetti.shape.matrix)) {\n context.fill(transformPath2D(\n fetti.shape.path,\n fetti.shape.matrix,\n fetti.x,\n fetti.y,\n Math.abs(x2 - x1) * 0.1,\n Math.abs(y2 - y1) * 0.1,\n Math.PI / 10 * fetti.wobble\n ));\n } else if (fetti.shape.type === 'bitmap') {\n var rotation = Math.PI / 10 * fetti.wobble;\n var scaleX = Math.abs(x2 - x1) * 0.1;\n var scaleY = Math.abs(y2 - y1) * 0.1;\n var width = fetti.shape.bitmap.width * fetti.scalar;\n var height = fetti.shape.bitmap.height * fetti.scalar;\n\n var matrix = new DOMMatrix([\n Math.cos(rotation) * scaleX,\n Math.sin(rotation) * scaleX,\n -Math.sin(rotation) * scaleY,\n Math.cos(rotation) * scaleY,\n fetti.x,\n fetti.y\n ]);\n\n // apply the transform matrix from the confetti shape\n matrix.multiplySelf(new DOMMatrix(fetti.shape.matrix));\n\n var pattern = context.createPattern(bitmapMapper.transform(fetti.shape.bitmap), 'no-repeat');\n pattern.setTransform(matrix);\n\n context.globalAlpha = (1 - progress);\n context.fillStyle = pattern;\n context.fillRect(\n fetti.x - (width / 2),\n fetti.y - (height / 2),\n width,\n height\n );\n context.globalAlpha = 1;\n } else if (fetti.shape === 'circle') {\n context.ellipse ?\n context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) :\n ellipse(context, fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI);\n } else if (fetti.shape === 'star') {\n var rot = Math.PI / 2 * 3;\n var innerRadius = 4 * fetti.scalar;\n var outerRadius = 8 * fetti.scalar;\n var x = fetti.x;\n var y = fetti.y;\n var spikes = 5;\n var step = Math.PI / spikes;\n\n while (spikes--) {\n x = fetti.x + Math.cos(rot) * outerRadius;\n y = fetti.y + Math.sin(rot) * outerRadius;\n context.lineTo(x, y);\n rot += step;\n\n x = fetti.x + Math.cos(rot) * innerRadius;\n y = fetti.y + Math.sin(rot) * innerRadius;\n context.lineTo(x, y);\n rot += step;\n }\n } else {\n context.moveTo(Math.floor(fetti.x), Math.floor(fetti.y));\n context.lineTo(Math.floor(fetti.wobbleX), Math.floor(y1));\n context.lineTo(Math.floor(x2), Math.floor(y2));\n context.lineTo(Math.floor(x1), Math.floor(fetti.wobbleY));\n }\n\n context.closePath();\n context.fill();\n\n return fetti.tick < fetti.totalTicks;\n }\n\n function animate(canvas, fettis, resizer, size, done) {\n var animatingFettis = fettis.slice();\n var context = canvas.getContext('2d');\n var animationFrame;\n var destroy;\n\n var prom = promise(function (resolve) {\n function onDone() {\n animationFrame = destroy = null;\n\n context.clearRect(0, 0, size.width, size.height);\n bitmapMapper.clear();\n\n done();\n resolve();\n }\n\n function update() {\n if (isWorker && !(size.width === workerSize.width && size.height === workerSize.height)) {\n size.width = canvas.width = workerSize.width;\n size.height = canvas.height = workerSize.height;\n }\n\n if (!size.width && !size.height) {\n resizer(canvas);\n size.width = canvas.width;\n size.height = canvas.height;\n }\n\n context.clearRect(0, 0, size.width, size.height);\n\n animatingFettis = animatingFettis.filter(function (fetti) {\n return updateFetti(context, fetti);\n });\n\n if (animatingFettis.length) {\n animationFrame = raf.frame(update);\n } else {\n onDone();\n }\n }\n\n animationFrame = raf.frame(update);\n destroy = onDone;\n });\n\n return {\n addFettis: function (fettis) {\n animatingFettis = animatingFettis.concat(fettis);\n\n return prom;\n },\n canvas: canvas,\n promise: prom,\n reset: function () {\n if (animationFrame) {\n raf.cancel(animationFrame);\n }\n\n if (destroy) {\n destroy();\n }\n }\n };\n }\n\n function confettiCannon(canvas, globalOpts) {\n var isLibCanvas = !canvas;\n var allowResize = !!prop(globalOpts || {}, 'resize');\n var hasResizeEventRegistered = false;\n var globalDisableForReducedMotion = prop(globalOpts, 'disableForReducedMotion', Boolean);\n var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, 'useWorker');\n var worker = shouldUseWorker ? getWorker() : null;\n var resizer = isLibCanvas ? setCanvasWindowSize : setCanvasRectSize;\n var initialized = (canvas && worker) ? !!canvas.__confetti_initialized : false;\n var preferLessMotion = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion)').matches;\n var animationObj;\n\n function fireLocal(options, size, done) {\n var particleCount = prop(options, 'particleCount', onlyPositiveInt);\n var angle = prop(options, 'angle', Number);\n var spread = prop(options, 'spread', Number);\n var startVelocity = prop(options, 'startVelocity', Number);\n var decay = prop(options, 'decay', Number);\n var gravity = prop(options, 'gravity', Number);\n var drift = prop(options, 'drift', Number);\n var colors = prop(options, 'colors', colorsToRgb);\n var ticks = prop(options, 'ticks', Number);\n var shapes = prop(options, 'shapes');\n var scalar = prop(options, 'scalar');\n var flat = !!prop(options, 'flat');\n var origin = getOrigin(options);\n\n var temp = particleCount;\n var fettis = [];\n\n var startX = canvas.width * origin.x;\n var startY = canvas.height * origin.y;\n\n while (temp--) {\n fettis.push(\n randomPhysics({\n x: startX,\n y: startY,\n angle: angle,\n spread: spread,\n startVelocity: startVelocity,\n color: colors[temp % colors.length],\n shape: shapes[randomInt(0, shapes.length)],\n ticks: ticks,\n decay: decay,\n gravity: gravity,\n drift: drift,\n scalar: scalar,\n flat: flat\n })\n );\n }\n\n // if we have a previous canvas already animating,\n // add to it\n if (animationObj) {\n return animationObj.addFettis(fettis);\n }\n\n animationObj = animate(canvas, fettis, resizer, size , done);\n\n return animationObj.promise;\n }\n\n function fire(options) {\n var disableForReducedMotion = globalDisableForReducedMotion || prop(options, 'disableForReducedMotion', Boolean);\n var zIndex = prop(options, 'zIndex', Number);\n\n if (disableForReducedMotion && preferLessMotion) {\n return promise(function (resolve) {\n resolve();\n });\n }\n\n if (isLibCanvas && animationObj) {\n // use existing canvas from in-progress animation\n canvas = animationObj.canvas;\n } else if (isLibCanvas && !canvas) {\n // create and initialize a new canvas\n canvas = getCanvas(zIndex);\n document.body.appendChild(canvas);\n }\n\n if (allowResize && !initialized) {\n // initialize the size of a user-supplied canvas\n resizer(canvas);\n }\n\n var size = {\n width: canvas.width,\n height: canvas.height\n };\n\n if (worker && !initialized) {\n worker.init(canvas);\n }\n\n initialized = true;\n\n if (worker) {\n canvas.__confetti_initialized = true;\n }\n\n function onResize() {\n if (worker) {\n // TODO this really shouldn't be immediate, because it is expensive\n var obj = {\n getBoundingClientRect: function () {\n if (!isLibCanvas) {\n return canvas.getBoundingClientRect();\n }\n }\n };\n\n resizer(obj);\n\n worker.postMessage({\n resize: {\n width: obj.width,\n height: obj.height\n }\n });\n return;\n }\n\n // don't actually query the size here, since this\n // can execute frequently and rapidly\n size.width = size.height = null;\n }\n\n function done() {\n animationObj = null;\n\n if (allowResize) {\n hasResizeEventRegistered = false;\n global.removeEventListener('resize', onResize);\n }\n\n if (isLibCanvas && canvas) {\n if (document.body.contains(canvas)) {\n document.body.removeChild(canvas); \n }\n canvas = null;\n initialized = false;\n }\n }\n\n if (allowResize && !hasResizeEventRegistered) {\n hasResizeEventRegistered = true;\n global.addEventListener('resize', onResize, false);\n }\n\n if (worker) {\n return worker.fire(options, size, done);\n }\n\n return fireLocal(options, size, done);\n }\n\n fire.reset = function () {\n if (worker) {\n worker.reset();\n }\n\n if (animationObj) {\n animationObj.reset();\n }\n };\n\n return fire;\n }\n\n // Make default export lazy to defer worker creation until called.\n var defaultFire;\n function getDefaultFire() {\n if (!defaultFire) {\n defaultFire = confettiCannon(null, { useWorker: true, resize: true });\n }\n return defaultFire;\n }\n\n function transformPath2D(pathString, pathMatrix, x, y, scaleX, scaleY, rotation) {\n var path2d = new Path2D(pathString);\n\n var t1 = new Path2D();\n t1.addPath(path2d, new DOMMatrix(pathMatrix));\n\n var t2 = new Path2D();\n // see https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix/DOMMatrix\n t2.addPath(t1, new DOMMatrix([\n Math.cos(rotation) * scaleX,\n Math.sin(rotation) * scaleX,\n -Math.sin(rotation) * scaleY,\n Math.cos(rotation) * scaleY,\n x,\n y\n ]));\n\n return t2;\n }\n\n function shapeFromPath(pathData) {\n if (!canUsePaths) {\n throw new Error('path confetti are not supported in this browser');\n }\n\n var path, matrix;\n\n if (typeof pathData === 'string') {\n path = pathData;\n } else {\n path = pathData.path;\n matrix = pathData.matrix;\n }\n\n var path2d = new Path2D(path);\n var tempCanvas = document.createElement('canvas');\n var tempCtx = tempCanvas.getContext('2d');\n\n if (!matrix) {\n // attempt to figure out the width of the path, up to 1000x1000\n var maxSize = 1000;\n var minX = maxSize;\n var minY = maxSize;\n var maxX = 0;\n var maxY = 0;\n var width, height;\n\n // do some line skipping... this is faster than checking\n // every pixel and will be mostly still correct\n for (var x = 0; x < maxSize; x += 2) {\n for (var y = 0; y < maxSize; y += 2) {\n if (tempCtx.isPointInPath(path2d, x, y, 'nonzero')) {\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n }\n }\n\n width = maxX - minX;\n height = maxY - minY;\n\n var maxDesiredSize = 10;\n var scale = Math.min(maxDesiredSize/width, maxDesiredSize/height);\n\n matrix = [\n scale, 0, 0, scale,\n -Math.round((width/2) + minX) * scale,\n -Math.round((height/2) + minY) * scale\n ];\n }\n\n return {\n type: 'path',\n path: path,\n matrix: matrix\n };\n }\n\n function shapeFromText(textData) {\n var text,\n scalar = 1,\n color = '#000000',\n // see https://nolanlawson.com/2022/04/08/the-struggle-of-using-native-emoji-on-the-web/\n fontFamily = '\"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\", \"EmojiOne Color\", \"Android Emoji\", \"Twemoji Mozilla\", \"system emoji\", sans-serif';\n\n if (typeof textData === 'string') {\n text = textData;\n } else {\n text = textData.text;\n scalar = 'scalar' in textData ? textData.scalar : scalar;\n fontFamily = 'fontFamily' in textData ? textData.fontFamily : fontFamily;\n color = 'color' in textData ? textData.color : color;\n }\n\n // all other confetti are 10 pixels,\n // so this pixel size is the de-facto 100% scale confetti\n var fontSize = 10 * scalar;\n var font = '' + fontSize + 'px ' + fontFamily;\n\n var canvas = new OffscreenCanvas(fontSize, fontSize);\n var ctx = canvas.getContext('2d');\n\n ctx.font = font;\n var size = ctx.measureText(text);\n var width = Math.ceil(size.actualBoundingBoxRight + size.actualBoundingBoxLeft);\n var height = Math.ceil(size.actualBoundingBoxAscent + size.actualBoundingBoxDescent);\n\n var padding = 2;\n var x = size.actualBoundingBoxLeft + padding;\n var y = size.actualBoundingBoxAscent + padding;\n width += padding + padding;\n height += padding + padding;\n\n canvas = new OffscreenCanvas(width, height);\n ctx = canvas.getContext('2d');\n ctx.font = font;\n ctx.fillStyle = color;\n\n ctx.fillText(text, x, y);\n\n var scale = 1 / scalar;\n\n return {\n type: 'bitmap',\n // TODO these probably need to be transfered for workers\n bitmap: canvas.transferToImageBitmap(),\n matrix: [scale, 0, 0, scale, -width * scale / 2, -height * scale / 2]\n };\n }\n\n module.exports = function() {\n return getDefaultFire().apply(this, arguments);\n };\n module.exports.reset = function() {\n getDefaultFire().reset();\n };\n module.exports.create = confettiCannon;\n module.exports.shapeFromPath = shapeFromPath;\n module.exports.shapeFromText = shapeFromText;\n}((function () {\n if (typeof window !== 'undefined') {\n return window;\n }\n\n if (typeof self !== 'undefined') {\n return self;\n }\n\n return this || {};\n})(), module, false));\n\n// end source content\n\nexport default module.exports;\nexport var create = module.exports.create;\n","<template>\n <div v-if=\"sessionPrepared\" class=\"StudySession\">\n <v-row v-if=\"!frameless\" align=\"center\">\n <!-- <h1 class=\"text-h3\" v-if=\"courseNames[courseID]\">{{ courseNames[courseID] }}:</h1> -->\n <v-spacer></v-spacer>\n <v-progress-circular v-if=\"loading\" color=\"primary\" indeterminate size=\"32\" width=\"4\" />\n </v-row>\n\n <!-- Debug Panel (only visible if window.debugMode is true) -->\n <session-controller-debug v-if=\"debugMode\" :session-controller=\"sessionController\" />\n\n <br v-if=\"!frameless\" />\n\n <div v-if=\"sessionFinished\">\n <slot name=\"session-finished\" :session-record=\"sessionRecord\" :report=\"sessionController?.report\">\n <div class=\"text-h4\">\n <p>Study session finished! Great job!</p>\n <p v-if=\"sessionController\">{{ sessionController.report }}</p>\n <!-- <p>\n Start <a @click=\"$emit('session-finished')\">another study session</a>, or try\n <router-link :to=\"`/edit/${courseID}`\">adding some new content</router-link> to challenge yourself and others!\n </p> -->\n <heat-map :activity-records-getter=\"() => user.getActivityRecords()\" />\n </div>\n </slot>\n </div>\n\n <div v-else ref=\"shadowWrapper\" class=\"card-transition-container\">\n <transition :name=\"transitionName\">\n <card-viewer\n v-if=\"view\"\n ref=\"cardViewer\"\n :key=\"cardID\"\n :class=\"loading ? 'muted' : ''\"\n :view=\"view\"\n :data=\"data\"\n :card_id=\"cardID\"\n :course_id=\"courseID\"\n :session-order=\"cardCount\"\n :user_elo=\"user_elo(courseID)\"\n :card_elo=\"card_elo\"\n :frameless=\"frameless\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"handleReplanRequest\"\n @ready-to-advance=\"handleReadyToAdvance\"\n />\n </transition>\n </div>\n\n <template v-if=\"!frameless\">\n <br />\n <div v-if=\"sessionController\">\n <span v-for=\"i in sessionController.failedCount\" :key=\"i\" class=\"text-h5\">•</span>\n </div>\n </template>\n\n <!--\n todo: reinstate tag editing at session-time ?\n\n <div v-if=\"!sessionFinished && editTags\">\n <p>Add tags to this card:</p>\n <sk-tags-input :course-i-d=\"courseID\" :card-i-d=\"cardID\" />\n </div> -->\n\n <v-row v-if=\"!hideFooter\" align=\"center\" class=\"footer-controls pa-5\">\n <v-col cols=\"auto\" class=\"d-flex flex-grow-0 mr-auto\">\n <StudySessionTimer\n :time-remaining=\"timeRemaining\"\n :session-time-limit=\"sessionTimeLimit\"\n @add-time=\"incrementSessionClock\"\n />\n </v-col>\n\n <v-spacer></v-spacer>\n\n <v-col cols=\"auto\" class=\"footer-right\">\n <SkMouseTrap />\n </v-col>\n </v-row>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, markRaw, PropType } from 'vue';\nimport { ViewComponent } from '../composables';\nimport { isQuestionView } from '../composables/CompositionViewable';\nimport HeatMap from './HeatMap.vue';\nimport SkMouseTrap from './SkMouseTrap.vue';\nimport { alertUser } from './SnackbarService';\nimport StudySessionTimer from './StudySessionTimer.vue';\nimport CardViewer from './cardRendering/CardViewer.vue';\nimport SessionControllerDebug from './SessionControllerDebug.vue';\n\nimport { CourseElo, Status, toCourseElo, ViewData } from '@vue-skuilder/common';\nimport {\n CardHistory,\n CardRecord,\n ClassroomDBInterface,\n ContentSourceID,\n CourseRegistrationDoc,\n DataLayerProvider,\n getStudySource,\n HydratedCard,\n isQuestionRecord,\n ReplanOptions,\n ResponseResult,\n SessionController,\n StudyContentSource,\n StudySessionRecord,\n UserDBInterface,\n} from '@vue-skuilder/db';\nimport confetti from 'canvas-confetti';\n\nimport { StudySessionConfig, CardTransitionPreset, CardTransitionMode } from './StudySession.types';\n\ninterface StudyRefs {\n shadowWrapper: HTMLDivElement;\n cardViewer: InstanceType<typeof CardViewer>;\n}\n\ntype StudyInstance = ReturnType<typeof defineComponent> & {\n $refs: StudyRefs;\n};\n\nexport default defineComponent({\n name: 'StudySession',\n\n ref: {} as StudyRefs,\n\n components: {\n CardViewer,\n StudySessionTimer,\n SkMouseTrap,\n HeatMap,\n SessionControllerDebug,\n },\n\n props: {\n sessionTimeLimit: {\n type: Number,\n required: true,\n },\n contentSources: {\n type: Array as PropType<ContentSourceID[]>,\n required: true,\n },\n user: {\n type: Object as PropType<UserDBInterface>,\n required: true,\n },\n dataLayer: {\n type: Object as PropType<DataLayerProvider>,\n required: true,\n },\n sessionConfig: {\n type: Object as PropType<StudySessionConfig>,\n default: () => ({ likesConfetti: false }),\n },\n getViewComponent: {\n type: Function as PropType<(viewId: string) => ViewComponent>,\n required: true,\n },\n frameless: {\n type: Boolean,\n default: false,\n },\n hideFooter: {\n type: Boolean,\n default: false,\n },\n transitionName: {\n type: String as PropType<CardTransitionPreset | string>,\n default: 'component-fade',\n },\n transitionMode: {\n type: String as PropType<CardTransitionMode>,\n default: 'out-in',\n },\n },\n\n emits: [\n 'session-finished',\n 'session-started',\n 'card-loaded',\n 'card-response',\n 'time-changed',\n 'session-prepared',\n 'session-error',\n 'replan-requested',\n ],\n\n data() {\n return {\n // editTags: false,\n cardID: '',\n view: null as ViewComponent | null,\n data: [] as ViewData[],\n courseID: '',\n card_elo: 1000,\n courseNames: {} as { [courseID: string]: string },\n cardCount: 1,\n sessionController: null as SessionController<ViewComponent> | null,\n sessionPrepared: false,\n sessionFinished: false,\n sessionRecord: [] as StudySessionRecord[],\n percentageRemaining: 100,\n timerIsActive: true,\n loading: false,\n userCourseRegDoc: null as CourseRegistrationDoc | null,\n sessionContentSources: [] as StudyContentSource[],\n timeRemaining: 300, // 5 minutes * 60 seconds\n replanPending: false,\n replanOptions: null as ReplanOptions | null,\n deferredNextCardAction: null as string | null,\n intervalHandler: null as NodeJS.Timeout | null,\n cardType: '',\n debugMode: (window as any).debugMode === true,\n };\n },\n\n computed: {\n currentCard(): StudySessionRecord {\n return this.sessionRecord[this.sessionRecord.length - 1];\n },\n },\n\n // watch: {\n // editCard: {\n // async handler(value: boolean) {\n // if (value) {\n // this.dataInputFormStore.dataInputForm.dataShape = await getCardDataShape(this.courseID, this.cardID);\n\n // const cfg = await getCredentialledCourseConfig(this.courseID);\n // this.dataInputFormStore.dataInputForm.course = cfg!;\n\n // this.editCardReady = true;\n\n // for (const oldField in this.dataInputFormStore.dataInputForm.localStore) {\n // if (oldField) {\n // console.log(`[Study] Removing old data: ${oldField}`);\n // delete this.dataInputFormStore.dataInputForm.localStore[oldField];\n // }\n // }\n\n // for (const field in this.data[0]) {\n // if (field) {\n // console.log(`[Study] Writing ${field}: ${this.data[0][field]} to the dataInputForm state...`);\n // this.dataInputFormStore.dataInputForm.localStore[field] = this.data[0][field];\n // }\n // }\n // } else {\n // this.editCardReady = false;\n // }\n // },\n // },\n // },\n\n async created() {\n this.userCourseRegDoc = await this.user.getCourseRegistrationsDoc();\n console.log('[StudySession] Created lifecycle hook - starting initSession');\n await this.initSession();\n console.log('[StudySession] InitSession completed in created hook');\n },\n\n errorCaptured(err: unknown, _instance: unknown, info: string) {\n console.error(`[StudySession] Card render error (${info}), skipping card \"${this.cardID}\":`, err);\n if (this.sessionController) {\n void this.sessionController.nextCard().then((card) => this.loadCard(card));\n }\n return false; // prevent propagation\n },\n\n methods: {\n /**\n * Handle a `ready-to-advance` event from a card view that previously\n * submitted with `deferAdvance: true`. Triggers the stashed navigation.\n */\n async handleReadyToAdvance() {\n const action = this.deferredNextCardAction;\n if (!action) {\n console.warn('[StudySession] ready-to-advance received but no deferred action stashed — ignoring');\n return;\n }\n console.log(`[StudySession] ready-to-advance received — advancing with stashed action: ${action}`);\n this.deferredNextCardAction = null;\n this.loadCard(await this.sessionController!.nextCard(action));\n },\n\n /**\n * Handle a replan request from a card view component.\n *\n * Card views emit 'request-replan' when they've made state changes that\n * should be reflected in the session's content queue (e.g. a GPC intro\n * unlocking new exercise cards).\n *\n * The replan is deferred — not fired immediately. processResponse()\n * checks this flag after submitResponse() has recorded ELO/tag\n * interactions, ensuring the pipeline sees fully up-to-date state.\n *\n * Accepts either a ReplanOptions object (new API) or a bare hints\n * Record (legacy). SessionController.requestReplan() normalises both.\n */\n handleReplanRequest(options?: ReplanOptions) {\n if (this.sessionController) {\n const label = (options as ReplanOptions)?.label;\n const tag = label ? ` [${label}]` : '';\n console.log(`[StudySession] Replan requested by card view${tag}, deferring until after response processing`);\n this.replanPending = true;\n this.replanOptions = (options as ReplanOptions) ?? null;\n }\n },\n\n user_elo(courseID: string): CourseElo {\n const courseDoc = this.userCourseRegDoc!.courses.find((c) => c.courseID === courseID);\n if (courseDoc) {\n return toCourseElo(courseDoc.elo);\n }\n return toCourseElo(undefined);\n },\n\n handleClassroomMessage() {\n return (v: unknown) => {\n alertUser({\n text: this.user?.getUsername() || '[Unknown user]',\n status: Status.ok,\n });\n console.log(`[StudySession] There was a change in the classroom DB:`);\n console.log(`[StudySession] change: ${v}`);\n console.log(`[StudySession] Stringified change: ${JSON.stringify(v)}`);\n return {};\n };\n },\n\n incrementSessionClock() {\n const max = 60 * this.sessionTimeLimit - this.timeRemaining;\n this.sessionController!.addTime(Math.min(max, 60));\n this.tick();\n },\n\n tick() {\n this.timeRemaining = this.sessionController!.secondsRemaining;\n\n this.percentageRemaining =\n this.timeRemaining > 60\n ? 100 * (this.timeRemaining / (60 * this.sessionTimeLimit))\n : 100 * (this.timeRemaining / 60);\n\n this.$emit('time-changed', this.timeRemaining);\n\n if (this.timeRemaining === 0) {\n clearInterval(this.intervalHandler!);\n }\n },\n\n async initSession() {\n let sessionClassroomDBs: ClassroomDBInterface[] = [];\n try {\n console.log(`[StudySession] starting study session w/ sources: ${JSON.stringify(this.contentSources)}`);\n console.log('[StudySession] Beginning preparation process');\n\n this.sessionContentSources = markRaw(\n (\n await Promise.all(\n this.contentSources.map(async (s) => {\n try {\n return await getStudySource(s, this.user);\n } catch (e) {\n console.error(`Failed to load study source: ${s.type}/${s.id}`, e);\n return null;\n }\n })\n )\n ).filter((s: unknown) => s !== null)\n );\n\n this.timeRemaining = this.sessionTimeLimit * 60;\n\n sessionClassroomDBs = await Promise.all(\n this.contentSources\n .filter((s) => s.type === 'classroom')\n .map(async (c) => await this.dataLayer.getClassroomDB(c.id, 'student'))\n );\n\n sessionClassroomDBs.forEach((_db) => {\n // db.setChangeFcn(this.handleClassroomMessage());\n });\n\n const scOptions: { defaultBatchLimit?: number; initialReviewCap?: number } = {};\n if (this.sessionConfig?.defaultBatchLimit !== undefined) {\n scOptions.defaultBatchLimit = this.sessionConfig.defaultBatchLimit;\n }\n if (this.sessionConfig?.initialReviewCap !== undefined) {\n scOptions.initialReviewCap = this.sessionConfig.initialReviewCap;\n }\n\n this.sessionController = markRaw(\n new SessionController<ViewComponent>(\n this.sessionContentSources,\n 60 * this.sessionTimeLimit,\n this.dataLayer,\n this.getViewComponent,\n undefined, // mixer — use default\n scOptions\n )\n );\n this.sessionController.sessionRecord = this.sessionRecord;\n\n // Apply init hints if provided (e.g. post-lesson boost tags)\n if (this.sessionConfig?.initHints) {\n for (const source of this.sessionContentSources) {\n source.setEphemeralHints?.(this.sessionConfig.initHints as any);\n }\n console.log('[StudySession] Applied init hints to content sources');\n }\n\n await this.sessionController.prepareSession();\n this.intervalHandler = setInterval(this.tick, 1000);\n\n this.sessionPrepared = true;\n\n console.log('[StudySession] Session preparation complete, emitting session-prepared event');\n this.$emit('session-prepared');\n console.log('[StudySession] Event emission completed');\n } catch (error) {\n console.error('[StudySession] Error during session preparation:', error);\n // Notify parent component about the error\n this.$emit('session-error', { message: 'Failed to prepare study session', error });\n }\n\n try {\n this.contentSources\n .filter((s) => s.type === 'course')\n .forEach(\n async (c) => (this.courseNames[c.id] = (await this.dataLayer.getCoursesDB().getCourseConfig(c.id)).name)\n );\n\n console.log(`[StudySession] Session created:\n ${this.sessionController?.toString() || 'Session controller not initialized'}\n User courses: ${this.contentSources\n .filter((s) => s.type === 'course')\n .map((c) => c.id)\n .toString()}\n User classrooms: ${sessionClassroomDBs.map((db: any) => db._id).toString() || 'No classrooms'}\n `);\n } catch (error) {\n console.error('[StudySession] Error during final session setup:', error);\n }\n\n if (this.sessionController) {\n try {\n this.$emit('session-started');\n this.loadCard(await this.sessionController.nextCard());\n } catch (error) {\n console.error('[StudySession] Error loading next card:', error);\n this.$emit('session-error', { message: 'Failed to load study card', error });\n }\n } else {\n console.error('[StudySession] Cannot load card: session controller not initialized');\n this.$emit('session-error', { message: 'Study session initialization failed' });\n }\n },\n\n countCardViews(course_id: string, card_id: string): number {\n return this.sessionRecord.filter((r) => r.card.course_id === course_id && r.card.card_id === card_id).length;\n },\n\n async processResponse(this: StudyInstance, r: CardRecord) {\n this.$emit('card-response', r);\n\n this.timerIsActive = true;\n\n r.cardID = this.cardID;\n r.courseID = this.courseID;\n this.currentCard.records.push(r);\n\n console.log(`[StudySession] StudySession.processResponse is running...`);\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] About to call logCardRecord...`);\n \n const cardHistory = this.logCardRecord(r).catch((e: unknown) => {\n console.error(`[StudySession] putCardRecord failed:`, e);\n throw e;\n });\n // console.log(`[StudySession] logCardRecord called, cardHistory promise created...`);\n\n // Get view constraints for response processing\n let maxAttemptsPerView = 1;\n let maxSessionViews = 1;\n if (isQuestionView(this.$refs.cardViewer?.$refs.activeView)) {\n const view = this.$refs.cardViewer.$refs.activeView;\n maxAttemptsPerView = view.maxAttemptsPerView;\n maxSessionViews = view.maxSessionViews;\n }\n const sessionViews = this.countCardViews(this.courseID, this.cardID);\n\n // Process response through SessionController\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] About to call submitResponse...`);\n const result: ResponseResult = await this.sessionController!.submitResponse(\n r,\n cardHistory,\n this.userCourseRegDoc!,\n this.currentCard,\n this.courseID,\n this.cardID,\n maxAttemptsPerView,\n maxSessionViews,\n sessionViews\n );\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] submitResponse completed, result:`, result);\n\n // Handle UI feedback based on result\n try {\n this.handleUIFeedback(result);\n } catch (error) {\n console.error(`[StudySession] Error handling UI feedback: ${error}.\\n\\nResult: ${JSON.stringify(result)}`);\n }\n\n // Fire deferred replan if requested — state is now fully recorded\n if (this.replanPending) {\n const opts = this.replanOptions;\n const label = opts?.label;\n const tag = label ? ` [${label}]` : '';\n console.log(`[StudySession] Firing deferred replan (post-submitResponse)${tag}`);\n this.replanPending = false;\n this.replanOptions = null;\n void this.sessionController!.requestReplan(opts ?? undefined);\n this.$emit('replan-requested');\n }\n\n // Handle navigation based on result\n if (result.deferred) {\n // Card requested deferred advancement — stash the action and wait\n // for a `ready-to-advance` event from the view before navigating.\n console.log(`[StudySession] Deferred advance — stashing action: ${result.nextCardAction}`);\n this.deferredNextCardAction = result.nextCardAction;\n } else if (result.shouldLoadNextCard) {\n this.loadCard(await this.sessionController!.nextCard(result.nextCardAction));\n }\n\n // Clear feedback shadow if requested\n if (result.shouldClearFeedbackShadow) {\n this.clearFeedbackShadow();\n }\n },\n\n handleUIFeedback(result: ResponseResult) {\n if (result.isCorrect) {\n // Handle correct response UI\n if (!this.frameless) {\n try {\n if (this.$refs.shadowWrapper && result.performanceScore !== undefined) {\n this.$refs.shadowWrapper.setAttribute('style', `--r: ${255 * (1 - result.performanceScore)}; --g:${255}`);\n this.$refs.shadowWrapper.classList.add('correct');\n }\n } catch (e) {\n console.warn(`[StudySession] Error setting shadowWrapper style: ${e}`);\n }\n }\n\n // Show confetti for correct responses\n if (this.sessionConfig.likesConfetti) {\n confetti({\n origin: {\n y: 1,\n x: 0.25 + 0.5 * Math.random(),\n },\n disableForReducedMotion: true,\n angle: 60 + 60 * Math.random(),\n });\n }\n } else {\n // Handle incorrect response UI\n if (!this.frameless) {\n try {\n if (this.$refs.shadowWrapper) {\n this.$refs.shadowWrapper.classList.add('incorrect');\n }\n } catch (e) {\n console.warn(`[StudySession] Error setting shadowWrapper style: ${e}`);\n }\n }\n }\n },\n\n clearFeedbackShadow() {\n if (this.frameless) return;\n\n setTimeout(() => {\n try {\n if (this.$refs.shadowWrapper) {\n (this.$refs.shadowWrapper as HTMLElement).classList.remove('correct', 'incorrect');\n }\n } catch (e) {\n // swallow error\n console.warn(`[StudySession] Error clearing shadowWrapper style: ${e}`);\n }\n }, 1250);\n },\n\n async logCardRecord(r: CardRecord): Promise<CardHistory<CardRecord>> {\n console.log(`[StudySession] About to call user.putCardRecord...`);\n const result = await this.user!.putCardRecord(r);\n console.log(`[StudySession] user.putCardRecord completed`);\n return result;\n },\n\n async loadCard(card: HydratedCard | null) {\n if (this.loading) {\n console.warn(`Attempted to load card while loading another...`);\n return;\n }\n\n console.log(`[StudySession] loading: ${JSON.stringify(card)}`);\n if (card === null) {\n this.sessionFinished = true;\n this.$emit('session-finished', this.sessionRecord);\n return;\n }\n this.cardType = card.item.status;\n\n this.loading = true;\n\n this.cardCount++;\n this.data = card.data;\n this.view = markRaw(card.view);\n this.cardID = card.item.cardID;\n this.courseID = card.item.courseID;\n this.card_elo = card.item.elo || 1000;\n\n this.sessionRecord.push({\n card: {\n course_id: card.item.courseID,\n card_id: card.item.cardID,\n card_elo: this.card_elo,\n tags: card.tags ?? [],\n },\n item: card.item,\n records: [],\n });\n\n this.$emit('card-loaded', {\n courseID: card.item.courseID,\n cardID: card.item.cardID,\n cardCount: this.cardCount,\n });\n\n this.loading = false;\n },\n },\n});\n</script>\n\n<style scoped>\n.footer-controls {\n position: fixed;\n bottom: 0;\n background-color: var(--v-background); /* Match your app's background color */\n z-index: 100;\n}\n\n.footer-right {\n position: fixed;\n bottom: 0;\n right: 0;\n background-color: var(--v-background); /* Match your app's background color */\n z-index: 100;\n}\n\n.correct {\n animation: varFade 1250ms ease-out;\n}\n\n.incorrect {\n animation: purpleFade 1250ms ease-out;\n}\n\na {\n text-decoration: underline;\n}\n\n.StudySession {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.card-transition-container {\n position: relative;\n overflow: hidden;\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n@keyframes varFade {\n 0% {\n box-shadow:\n rgba(var(--r), var(--g), 0, 0.25) 0px 7px 8px -4px,\n rgba(var(--r), var(--g), 0, 0.25) 0px 12px 17px 2px,\n rgba(var(--r), var(--g), 0, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(0, 150, 0, 0) 0px 0px;\n }\n}\n\n@keyframes greenFade {\n 0% {\n box-shadow:\n rgba(0, 150, 0, 0.25) 0px 7px 8px -4px,\n rgba(0, 150, 0, 0.25) 0px 12px 17px 2px,\n rgba(0, 150, 0, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(0, 150, 0, 0) 0px 0px;\n }\n}\n@keyframes purpleFade {\n 0% {\n box-shadow:\n rgba(115, 0, 75, 0.25) 0px 7px 8px -4px,\n rgba(115, 0, 75, 0.25) 0px 12px 17px 2px,\n rgba(115, 0, 75, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(115, 0, 75, 0) 0px 0px;\n }\n}\n</style>\n\n<style>\n/* Transition presets — unscoped so classes reach child component root elements.\n Vue's <Transition> adds these classes to the child's root DOM node, which\n may be a Vuetify component (v-card). Scoped selectors can fail to match\n when the scope attribute doesn't propagate through component boundaries. */\n\n/* Preset: component-fade (default) — simple opacity crossfade, use with mode=\"out-in\" */\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.2s ease;\n}\n.component-fade-enter-from,\n.component-fade-leave-to {\n opacity: 0;\n}\n\n/* Preset: card-slide — backOutLeft exit / slide-in-right enter, use with mode=\"default\" */\n/* !important on position/inset: child components with scoped styles (e.g.\n `.gpc-intro[data-v-xxx] { position: relative }`) have higher specificity\n than this unscoped class. Without !important the leaving element stays\n in-flow and the enter/leave elements split vertical space — visible as\n the new card sliding in \"low\" then popping to center. */\n.card-slide-leave-active {\n animation: cardBackOutLeft 400ms cubic-bezier(0.4, 0, 0.7, 0.2) both;\n position: absolute !important;\n inset: 0;\n margin: auto;\n height: fit-content;\n will-change: transform, opacity;\n pointer-events: none;\n}\n.card-slide-leave-active * {\n animation-play-state: paused !important;\n transition: none !important;\n}\n.card-slide-enter-active {\n animation: cardSlideInRight 250ms ease-out 100ms both;\n will-change: transform, opacity;\n}\n\n@keyframes cardBackOutLeft {\n 0% {\n transform: translateX(0) scale(1);\n opacity: 1;\n }\n 40% {\n transform: translateX(0) scale(0.88);\n opacity: 0.8;\n }\n 100% {\n transform: translateX(-100%) scale(0.88);\n opacity: 0;\n }\n}\n\n@keyframes cardSlideInRight {\n 0% {\n transform: translateX(60%);\n opacity: 0;\n }\n 100% {\n transform: translateX(0);\n opacity: 1;\n }\n}\n\n/* Preset: card-scale — scale-down exit / gentle overshoot enter, use with mode=\"out-in\" */\n.card-scale-leave-active {\n transition: transform 150ms ease-in, opacity 150ms ease-in;\n}\n.card-scale-enter-active {\n transition: transform 200ms cubic-bezier(0.34, 1.4, 0.64, 1), opacity 150ms ease-out;\n}\n.card-scale-leave-to {\n transform: scale(0.92);\n opacity: 0;\n}\n.card-scale-enter-from {\n transform: scale(1.03);\n opacity: 0;\n}\n</style>\n","<template>\n <div v-if=\"sessionPrepared\" class=\"StudySession\">\n <v-row v-if=\"!frameless\" align=\"center\">\n <!-- <h1 class=\"text-h3\" v-if=\"courseNames[courseID]\">{{ courseNames[courseID] }}:</h1> -->\n <v-spacer></v-spacer>\n <v-progress-circular v-if=\"loading\" color=\"primary\" indeterminate size=\"32\" width=\"4\" />\n </v-row>\n\n <!-- Debug Panel (only visible if window.debugMode is true) -->\n <session-controller-debug v-if=\"debugMode\" :session-controller=\"sessionController\" />\n\n <br v-if=\"!frameless\" />\n\n <div v-if=\"sessionFinished\">\n <slot name=\"session-finished\" :session-record=\"sessionRecord\" :report=\"sessionController?.report\">\n <div class=\"text-h4\">\n <p>Study session finished! Great job!</p>\n <p v-if=\"sessionController\">{{ sessionController.report }}</p>\n <!-- <p>\n Start <a @click=\"$emit('session-finished')\">another study session</a>, or try\n <router-link :to=\"`/edit/${courseID}`\">adding some new content</router-link> to challenge yourself and others!\n </p> -->\n <heat-map :activity-records-getter=\"() => user.getActivityRecords()\" />\n </div>\n </slot>\n </div>\n\n <div v-else ref=\"shadowWrapper\" class=\"card-transition-container\">\n <transition :name=\"transitionName\">\n <card-viewer\n v-if=\"view\"\n ref=\"cardViewer\"\n :key=\"cardID\"\n :class=\"loading ? 'muted' : ''\"\n :view=\"view\"\n :data=\"data\"\n :card_id=\"cardID\"\n :course_id=\"courseID\"\n :session-order=\"cardCount\"\n :user_elo=\"user_elo(courseID)\"\n :card_elo=\"card_elo\"\n :frameless=\"frameless\"\n @emit-response=\"processResponse($event)\"\n @request-replan=\"handleReplanRequest\"\n @ready-to-advance=\"handleReadyToAdvance\"\n />\n </transition>\n </div>\n\n <template v-if=\"!frameless\">\n <br />\n <div v-if=\"sessionController\">\n <span v-for=\"i in sessionController.failedCount\" :key=\"i\" class=\"text-h5\">•</span>\n </div>\n </template>\n\n <!--\n todo: reinstate tag editing at session-time ?\n\n <div v-if=\"!sessionFinished && editTags\">\n <p>Add tags to this card:</p>\n <sk-tags-input :course-i-d=\"courseID\" :card-i-d=\"cardID\" />\n </div> -->\n\n <v-row v-if=\"!hideFooter\" align=\"center\" class=\"footer-controls pa-5\">\n <v-col cols=\"auto\" class=\"d-flex flex-grow-0 mr-auto\">\n <StudySessionTimer\n :time-remaining=\"timeRemaining\"\n :session-time-limit=\"sessionTimeLimit\"\n @add-time=\"incrementSessionClock\"\n />\n </v-col>\n\n <v-spacer></v-spacer>\n\n <v-col cols=\"auto\" class=\"footer-right\">\n <SkMouseTrap />\n </v-col>\n </v-row>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, markRaw, PropType } from 'vue';\nimport { ViewComponent } from '../composables';\nimport { isQuestionView } from '../composables/CompositionViewable';\nimport HeatMap from './HeatMap.vue';\nimport SkMouseTrap from './SkMouseTrap.vue';\nimport { alertUser } from './SnackbarService';\nimport StudySessionTimer from './StudySessionTimer.vue';\nimport CardViewer from './cardRendering/CardViewer.vue';\nimport SessionControllerDebug from './SessionControllerDebug.vue';\n\nimport { CourseElo, Status, toCourseElo, ViewData } from '@vue-skuilder/common';\nimport {\n CardHistory,\n CardRecord,\n ClassroomDBInterface,\n ContentSourceID,\n CourseRegistrationDoc,\n DataLayerProvider,\n getStudySource,\n HydratedCard,\n isQuestionRecord,\n ReplanOptions,\n ResponseResult,\n SessionController,\n StudyContentSource,\n StudySessionRecord,\n UserDBInterface,\n} from '@vue-skuilder/db';\nimport confetti from 'canvas-confetti';\n\nimport { StudySessionConfig, CardTransitionPreset, CardTransitionMode } from './StudySession.types';\n\ninterface StudyRefs {\n shadowWrapper: HTMLDivElement;\n cardViewer: InstanceType<typeof CardViewer>;\n}\n\ntype StudyInstance = ReturnType<typeof defineComponent> & {\n $refs: StudyRefs;\n};\n\nexport default defineComponent({\n name: 'StudySession',\n\n ref: {} as StudyRefs,\n\n components: {\n CardViewer,\n StudySessionTimer,\n SkMouseTrap,\n HeatMap,\n SessionControllerDebug,\n },\n\n props: {\n sessionTimeLimit: {\n type: Number,\n required: true,\n },\n contentSources: {\n type: Array as PropType<ContentSourceID[]>,\n required: true,\n },\n user: {\n type: Object as PropType<UserDBInterface>,\n required: true,\n },\n dataLayer: {\n type: Object as PropType<DataLayerProvider>,\n required: true,\n },\n sessionConfig: {\n type: Object as PropType<StudySessionConfig>,\n default: () => ({ likesConfetti: false }),\n },\n getViewComponent: {\n type: Function as PropType<(viewId: string) => ViewComponent>,\n required: true,\n },\n frameless: {\n type: Boolean,\n default: false,\n },\n hideFooter: {\n type: Boolean,\n default: false,\n },\n transitionName: {\n type: String as PropType<CardTransitionPreset | string>,\n default: 'component-fade',\n },\n transitionMode: {\n type: String as PropType<CardTransitionMode>,\n default: 'out-in',\n },\n },\n\n emits: [\n 'session-finished',\n 'session-started',\n 'card-loaded',\n 'card-response',\n 'time-changed',\n 'session-prepared',\n 'session-error',\n 'replan-requested',\n ],\n\n data() {\n return {\n // editTags: false,\n cardID: '',\n view: null as ViewComponent | null,\n data: [] as ViewData[],\n courseID: '',\n card_elo: 1000,\n courseNames: {} as { [courseID: string]: string },\n cardCount: 1,\n sessionController: null as SessionController<ViewComponent> | null,\n sessionPrepared: false,\n sessionFinished: false,\n sessionRecord: [] as StudySessionRecord[],\n percentageRemaining: 100,\n timerIsActive: true,\n loading: false,\n userCourseRegDoc: null as CourseRegistrationDoc | null,\n sessionContentSources: [] as StudyContentSource[],\n timeRemaining: 300, // 5 minutes * 60 seconds\n replanPending: false,\n replanOptions: null as ReplanOptions | null,\n deferredNextCardAction: null as string | null,\n intervalHandler: null as NodeJS.Timeout | null,\n cardType: '',\n debugMode: (window as any).debugMode === true,\n };\n },\n\n computed: {\n currentCard(): StudySessionRecord {\n return this.sessionRecord[this.sessionRecord.length - 1];\n },\n },\n\n // watch: {\n // editCard: {\n // async handler(value: boolean) {\n // if (value) {\n // this.dataInputFormStore.dataInputForm.dataShape = await getCardDataShape(this.courseID, this.cardID);\n\n // const cfg = await getCredentialledCourseConfig(this.courseID);\n // this.dataInputFormStore.dataInputForm.course = cfg!;\n\n // this.editCardReady = true;\n\n // for (const oldField in this.dataInputFormStore.dataInputForm.localStore) {\n // if (oldField) {\n // console.log(`[Study] Removing old data: ${oldField}`);\n // delete this.dataInputFormStore.dataInputForm.localStore[oldField];\n // }\n // }\n\n // for (const field in this.data[0]) {\n // if (field) {\n // console.log(`[Study] Writing ${field}: ${this.data[0][field]} to the dataInputForm state...`);\n // this.dataInputFormStore.dataInputForm.localStore[field] = this.data[0][field];\n // }\n // }\n // } else {\n // this.editCardReady = false;\n // }\n // },\n // },\n // },\n\n async created() {\n this.userCourseRegDoc = await this.user.getCourseRegistrationsDoc();\n console.log('[StudySession] Created lifecycle hook - starting initSession');\n await this.initSession();\n console.log('[StudySession] InitSession completed in created hook');\n },\n\n errorCaptured(err: unknown, _instance: unknown, info: string) {\n console.error(`[StudySession] Card render error (${info}), skipping card \"${this.cardID}\":`, err);\n if (this.sessionController) {\n void this.sessionController.nextCard().then((card) => this.loadCard(card));\n }\n return false; // prevent propagation\n },\n\n methods: {\n /**\n * Handle a `ready-to-advance` event from a card view that previously\n * submitted with `deferAdvance: true`. Triggers the stashed navigation.\n */\n async handleReadyToAdvance() {\n const action = this.deferredNextCardAction;\n if (!action) {\n console.warn('[StudySession] ready-to-advance received but no deferred action stashed — ignoring');\n return;\n }\n console.log(`[StudySession] ready-to-advance received — advancing with stashed action: ${action}`);\n this.deferredNextCardAction = null;\n this.loadCard(await this.sessionController!.nextCard(action));\n },\n\n /**\n * Handle a replan request from a card view component.\n *\n * Card views emit 'request-replan' when they've made state changes that\n * should be reflected in the session's content queue (e.g. a GPC intro\n * unlocking new exercise cards).\n *\n * The replan is deferred — not fired immediately. processResponse()\n * checks this flag after submitResponse() has recorded ELO/tag\n * interactions, ensuring the pipeline sees fully up-to-date state.\n *\n * Accepts either a ReplanOptions object (new API) or a bare hints\n * Record (legacy). SessionController.requestReplan() normalises both.\n */\n handleReplanRequest(options?: ReplanOptions) {\n if (this.sessionController) {\n const label = (options as ReplanOptions)?.label;\n const tag = label ? ` [${label}]` : '';\n console.log(`[StudySession] Replan requested by card view${tag}, deferring until after response processing`);\n this.replanPending = true;\n this.replanOptions = (options as ReplanOptions) ?? null;\n }\n },\n\n user_elo(courseID: string): CourseElo {\n const courseDoc = this.userCourseRegDoc!.courses.find((c) => c.courseID === courseID);\n if (courseDoc) {\n return toCourseElo(courseDoc.elo);\n }\n return toCourseElo(undefined);\n },\n\n handleClassroomMessage() {\n return (v: unknown) => {\n alertUser({\n text: this.user?.getUsername() || '[Unknown user]',\n status: Status.ok,\n });\n console.log(`[StudySession] There was a change in the classroom DB:`);\n console.log(`[StudySession] change: ${v}`);\n console.log(`[StudySession] Stringified change: ${JSON.stringify(v)}`);\n return {};\n };\n },\n\n incrementSessionClock() {\n const max = 60 * this.sessionTimeLimit - this.timeRemaining;\n this.sessionController!.addTime(Math.min(max, 60));\n this.tick();\n },\n\n tick() {\n this.timeRemaining = this.sessionController!.secondsRemaining;\n\n this.percentageRemaining =\n this.timeRemaining > 60\n ? 100 * (this.timeRemaining / (60 * this.sessionTimeLimit))\n : 100 * (this.timeRemaining / 60);\n\n this.$emit('time-changed', this.timeRemaining);\n\n if (this.timeRemaining === 0) {\n clearInterval(this.intervalHandler!);\n }\n },\n\n async initSession() {\n let sessionClassroomDBs: ClassroomDBInterface[] = [];\n try {\n console.log(`[StudySession] starting study session w/ sources: ${JSON.stringify(this.contentSources)}`);\n console.log('[StudySession] Beginning preparation process');\n\n this.sessionContentSources = markRaw(\n (\n await Promise.all(\n this.contentSources.map(async (s) => {\n try {\n return await getStudySource(s, this.user);\n } catch (e) {\n console.error(`Failed to load study source: ${s.type}/${s.id}`, e);\n return null;\n }\n })\n )\n ).filter((s: unknown) => s !== null)\n );\n\n this.timeRemaining = this.sessionTimeLimit * 60;\n\n sessionClassroomDBs = await Promise.all(\n this.contentSources\n .filter((s) => s.type === 'classroom')\n .map(async (c) => await this.dataLayer.getClassroomDB(c.id, 'student'))\n );\n\n sessionClassroomDBs.forEach((_db) => {\n // db.setChangeFcn(this.handleClassroomMessage());\n });\n\n const scOptions: { defaultBatchLimit?: number; initialReviewCap?: number } = {};\n if (this.sessionConfig?.defaultBatchLimit !== undefined) {\n scOptions.defaultBatchLimit = this.sessionConfig.defaultBatchLimit;\n }\n if (this.sessionConfig?.initialReviewCap !== undefined) {\n scOptions.initialReviewCap = this.sessionConfig.initialReviewCap;\n }\n\n this.sessionController = markRaw(\n new SessionController<ViewComponent>(\n this.sessionContentSources,\n 60 * this.sessionTimeLimit,\n this.dataLayer,\n this.getViewComponent,\n undefined, // mixer — use default\n scOptions\n )\n );\n this.sessionController.sessionRecord = this.sessionRecord;\n\n // Apply init hints if provided (e.g. post-lesson boost tags)\n if (this.sessionConfig?.initHints) {\n for (const source of this.sessionContentSources) {\n source.setEphemeralHints?.(this.sessionConfig.initHints as any);\n }\n console.log('[StudySession] Applied init hints to content sources');\n }\n\n await this.sessionController.prepareSession();\n this.intervalHandler = setInterval(this.tick, 1000);\n\n this.sessionPrepared = true;\n\n console.log('[StudySession] Session preparation complete, emitting session-prepared event');\n this.$emit('session-prepared');\n console.log('[StudySession] Event emission completed');\n } catch (error) {\n console.error('[StudySession] Error during session preparation:', error);\n // Notify parent component about the error\n this.$emit('session-error', { message: 'Failed to prepare study session', error });\n }\n\n try {\n this.contentSources\n .filter((s) => s.type === 'course')\n .forEach(\n async (c) => (this.courseNames[c.id] = (await this.dataLayer.getCoursesDB().getCourseConfig(c.id)).name)\n );\n\n console.log(`[StudySession] Session created:\n ${this.sessionController?.toString() || 'Session controller not initialized'}\n User courses: ${this.contentSources\n .filter((s) => s.type === 'course')\n .map((c) => c.id)\n .toString()}\n User classrooms: ${sessionClassroomDBs.map((db: any) => db._id).toString() || 'No classrooms'}\n `);\n } catch (error) {\n console.error('[StudySession] Error during final session setup:', error);\n }\n\n if (this.sessionController) {\n try {\n this.$emit('session-started');\n this.loadCard(await this.sessionController.nextCard());\n } catch (error) {\n console.error('[StudySession] Error loading next card:', error);\n this.$emit('session-error', { message: 'Failed to load study card', error });\n }\n } else {\n console.error('[StudySession] Cannot load card: session controller not initialized');\n this.$emit('session-error', { message: 'Study session initialization failed' });\n }\n },\n\n countCardViews(course_id: string, card_id: string): number {\n return this.sessionRecord.filter((r) => r.card.course_id === course_id && r.card.card_id === card_id).length;\n },\n\n async processResponse(this: StudyInstance, r: CardRecord) {\n this.$emit('card-response', r);\n\n this.timerIsActive = true;\n\n r.cardID = this.cardID;\n r.courseID = this.courseID;\n this.currentCard.records.push(r);\n\n console.log(`[StudySession] StudySession.processResponse is running...`);\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] About to call logCardRecord...`);\n \n const cardHistory = this.logCardRecord(r).catch((e: unknown) => {\n console.error(`[StudySession] putCardRecord failed:`, e);\n throw e;\n });\n // console.log(`[StudySession] logCardRecord called, cardHistory promise created...`);\n\n // Get view constraints for response processing\n let maxAttemptsPerView = 1;\n let maxSessionViews = 1;\n if (isQuestionView(this.$refs.cardViewer?.$refs.activeView)) {\n const view = this.$refs.cardViewer.$refs.activeView;\n maxAttemptsPerView = view.maxAttemptsPerView;\n maxSessionViews = view.maxSessionViews;\n }\n const sessionViews = this.countCardViews(this.courseID, this.cardID);\n\n // Process response through SessionController\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] About to call submitResponse...`);\n const result: ResponseResult = await this.sessionController!.submitResponse(\n r,\n cardHistory,\n this.userCourseRegDoc!,\n this.currentCard,\n this.courseID,\n this.cardID,\n maxAttemptsPerView,\n maxSessionViews,\n sessionViews\n );\n // DEBUG: Added logging to track hanging issue - can be removed if issue resolved\n // console.log(`[StudySession] submitResponse completed, result:`, result);\n\n // Handle UI feedback based on result\n try {\n this.handleUIFeedback(result);\n } catch (error) {\n console.error(`[StudySession] Error handling UI feedback: ${error}.\\n\\nResult: ${JSON.stringify(result)}`);\n }\n\n // Fire deferred replan if requested — state is now fully recorded\n if (this.replanPending) {\n const opts = this.replanOptions;\n const label = opts?.label;\n const tag = label ? ` [${label}]` : '';\n console.log(`[StudySession] Firing deferred replan (post-submitResponse)${tag}`);\n this.replanPending = false;\n this.replanOptions = null;\n void this.sessionController!.requestReplan(opts ?? undefined);\n this.$emit('replan-requested');\n }\n\n // Handle navigation based on result\n if (result.deferred) {\n // Card requested deferred advancement — stash the action and wait\n // for a `ready-to-advance` event from the view before navigating.\n console.log(`[StudySession] Deferred advance — stashing action: ${result.nextCardAction}`);\n this.deferredNextCardAction = result.nextCardAction;\n } else if (result.shouldLoadNextCard) {\n this.loadCard(await this.sessionController!.nextCard(result.nextCardAction));\n }\n\n // Clear feedback shadow if requested\n if (result.shouldClearFeedbackShadow) {\n this.clearFeedbackShadow();\n }\n },\n\n handleUIFeedback(result: ResponseResult) {\n if (result.isCorrect) {\n // Handle correct response UI\n if (!this.frameless) {\n try {\n if (this.$refs.shadowWrapper && result.performanceScore !== undefined) {\n this.$refs.shadowWrapper.setAttribute('style', `--r: ${255 * (1 - result.performanceScore)}; --g:${255}`);\n this.$refs.shadowWrapper.classList.add('correct');\n }\n } catch (e) {\n console.warn(`[StudySession] Error setting shadowWrapper style: ${e}`);\n }\n }\n\n // Show confetti for correct responses\n if (this.sessionConfig.likesConfetti) {\n confetti({\n origin: {\n y: 1,\n x: 0.25 + 0.5 * Math.random(),\n },\n disableForReducedMotion: true,\n angle: 60 + 60 * Math.random(),\n });\n }\n } else {\n // Handle incorrect response UI\n if (!this.frameless) {\n try {\n if (this.$refs.shadowWrapper) {\n this.$refs.shadowWrapper.classList.add('incorrect');\n }\n } catch (e) {\n console.warn(`[StudySession] Error setting shadowWrapper style: ${e}`);\n }\n }\n }\n },\n\n clearFeedbackShadow() {\n if (this.frameless) return;\n\n setTimeout(() => {\n try {\n if (this.$refs.shadowWrapper) {\n (this.$refs.shadowWrapper as HTMLElement).classList.remove('correct', 'incorrect');\n }\n } catch (e) {\n // swallow error\n console.warn(`[StudySession] Error clearing shadowWrapper style: ${e}`);\n }\n }, 1250);\n },\n\n async logCardRecord(r: CardRecord): Promise<CardHistory<CardRecord>> {\n console.log(`[StudySession] About to call user.putCardRecord...`);\n const result = await this.user!.putCardRecord(r);\n console.log(`[StudySession] user.putCardRecord completed`);\n return result;\n },\n\n async loadCard(card: HydratedCard | null) {\n if (this.loading) {\n console.warn(`Attempted to load card while loading another...`);\n return;\n }\n\n console.log(`[StudySession] loading: ${JSON.stringify(card)}`);\n if (card === null) {\n this.sessionFinished = true;\n this.$emit('session-finished', this.sessionRecord);\n return;\n }\n this.cardType = card.item.status;\n\n this.loading = true;\n\n this.cardCount++;\n this.data = card.data;\n this.view = markRaw(card.view);\n this.cardID = card.item.cardID;\n this.courseID = card.item.courseID;\n this.card_elo = card.item.elo || 1000;\n\n this.sessionRecord.push({\n card: {\n course_id: card.item.courseID,\n card_id: card.item.cardID,\n card_elo: this.card_elo,\n tags: card.tags ?? [],\n },\n item: card.item,\n records: [],\n });\n\n this.$emit('card-loaded', {\n courseID: card.item.courseID,\n cardID: card.item.cardID,\n cardCount: this.cardCount,\n });\n\n this.loading = false;\n },\n },\n});\n</script>\n\n<style scoped>\n.footer-controls {\n position: fixed;\n bottom: 0;\n background-color: var(--v-background); /* Match your app's background color */\n z-index: 100;\n}\n\n.footer-right {\n position: fixed;\n bottom: 0;\n right: 0;\n background-color: var(--v-background); /* Match your app's background color */\n z-index: 100;\n}\n\n.correct {\n animation: varFade 1250ms ease-out;\n}\n\n.incorrect {\n animation: purpleFade 1250ms ease-out;\n}\n\na {\n text-decoration: underline;\n}\n\n.StudySession {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.card-transition-container {\n position: relative;\n overflow: hidden;\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n@keyframes varFade {\n 0% {\n box-shadow:\n rgba(var(--r), var(--g), 0, 0.25) 0px 7px 8px -4px,\n rgba(var(--r), var(--g), 0, 0.25) 0px 12px 17px 2px,\n rgba(var(--r), var(--g), 0, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(0, 150, 0, 0) 0px 0px;\n }\n}\n\n@keyframes greenFade {\n 0% {\n box-shadow:\n rgba(0, 150, 0, 0.25) 0px 7px 8px -4px,\n rgba(0, 150, 0, 0.25) 0px 12px 17px 2px,\n rgba(0, 150, 0, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(0, 150, 0, 0) 0px 0px;\n }\n}\n@keyframes purpleFade {\n 0% {\n box-shadow:\n rgba(115, 0, 75, 0.25) 0px 7px 8px -4px,\n rgba(115, 0, 75, 0.25) 0px 12px 17px 2px,\n rgba(115, 0, 75, 0.25) 0px 5px 22px 4px;\n }\n 100% {\n box-shadow: rgba(115, 0, 75, 0) 0px 0px;\n }\n}\n</style>\n\n<style>\n/* Transition presets — unscoped so classes reach child component root elements.\n Vue's <Transition> adds these classes to the child's root DOM node, which\n may be a Vuetify component (v-card). Scoped selectors can fail to match\n when the scope attribute doesn't propagate through component boundaries. */\n\n/* Preset: component-fade (default) — simple opacity crossfade, use with mode=\"out-in\" */\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.2s ease;\n}\n.component-fade-enter-from,\n.component-fade-leave-to {\n opacity: 0;\n}\n\n/* Preset: card-slide — backOutLeft exit / slide-in-right enter, use with mode=\"default\" */\n/* !important on position/inset: child components with scoped styles (e.g.\n `.gpc-intro[data-v-xxx] { position: relative }`) have higher specificity\n than this unscoped class. Without !important the leaving element stays\n in-flow and the enter/leave elements split vertical space — visible as\n the new card sliding in \"low\" then popping to center. */\n.card-slide-leave-active {\n animation: cardBackOutLeft 400ms cubic-bezier(0.4, 0, 0.7, 0.2) both;\n position: absolute !important;\n inset: 0;\n margin: auto;\n height: fit-content;\n will-change: transform, opacity;\n pointer-events: none;\n}\n.card-slide-leave-active * {\n animation-play-state: paused !important;\n transition: none !important;\n}\n.card-slide-enter-active {\n animation: cardSlideInRight 250ms ease-out 100ms both;\n will-change: transform, opacity;\n}\n\n@keyframes cardBackOutLeft {\n 0% {\n transform: translateX(0) scale(1);\n opacity: 1;\n }\n 40% {\n transform: translateX(0) scale(0.88);\n opacity: 0.8;\n }\n 100% {\n transform: translateX(-100%) scale(0.88);\n opacity: 0;\n }\n}\n\n@keyframes cardSlideInRight {\n 0% {\n transform: translateX(60%);\n opacity: 0;\n }\n 100% {\n transform: translateX(0);\n opacity: 1;\n }\n}\n\n/* Preset: card-scale — scale-down exit / gentle overshoot enter, use with mode=\"out-in\" */\n.card-scale-leave-active {\n transition: transform 150ms ease-in, opacity 150ms ease-in;\n}\n.card-scale-enter-active {\n transition: transform 200ms cubic-bezier(0.34, 1.4, 0.64, 1), opacity 150ms ease-out;\n}\n.card-scale-leave-to {\n transform: scale(0.92);\n opacity: 0;\n}\n.card-scale-enter-from {\n transform: scale(1.03);\n opacity: 0;\n}\n</style>\n","<template>\n <v-card :class=\"`${className}`\" @mouseover=\"select\" @click=\"submitThisOption\">\n <markdown-renderer :md=\"content\" />\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, defineAsyncComponent, PropType } from 'vue';\n\nexport default defineComponent({\n name: 'MultipleChoiceOption',\n\n components: {\n MarkdownRenderer: defineAsyncComponent(() => import('../../components/cardRendering/MarkdownRenderer.vue')),\n },\n\n props: {\n content: {\n type: String,\n required: true,\n },\n selected: {\n type: Boolean,\n required: true,\n },\n number: {\n type: Number,\n required: true,\n },\n setSelection: {\n type: Function as PropType<(selection: number) => void>,\n required: true,\n },\n submit: {\n type: Function as PropType<() => void>,\n required: true,\n },\n markedWrong: {\n type: Boolean,\n required: true,\n },\n },\n\n computed: {\n className(): string {\n let color: string;\n\n switch (this.number) {\n case 0:\n color = 'bg-red';\n break;\n case 1:\n color = 'bg-purple';\n break;\n case 2:\n color = 'bg-indigo';\n break;\n case 3:\n color = 'bg-light-blue';\n break;\n case 4:\n color = 'bg-teal';\n break;\n case 5:\n color = 'bg-deep-orange';\n break;\n default:\n color = 'bg-grey';\n break;\n }\n\n if (this.selected && !this.markedWrong) {\n return `choice selected ${color} lighten-3 elevation-8`;\n } else if (!this.selected && !this.markedWrong) {\n return `choice not-selected ${color} lighten-4 elevation-1`;\n } else if (this.selected && this.markedWrong) {\n return `choice selected grey lighten-2 elevation-8`;\n } else if (!this.selected && this.markedWrong) {\n return 'choice not-selected grey lighten-2 elevation-0';\n } else {\n throw new Error(\n `'selected' and 'markedWrong' props in MultipleChoiceOption are in an impossible configuration.`\n );\n }\n },\n },\n\n methods: {\n select(): void {\n this.setSelection(this.number);\n },\n\n submitThisOption(): void {\n if (this.markedWrong) {\n return;\n } else {\n this.select();\n this.submit();\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.choice {\n text-align: center;\n display: inline-block;\n border-radius: 4px;\n padding-left: 15px;\n padding-right: 15px;\n padding-top: 5px;\n padding-bottom: 5px;\n margin: 10px;\n min-width: 75px; /* prevent tiny click-btns on, eg, one-letter answers */\n transition: all 0.2s ease-in-out;\n}\n\n.selected {\n transform: translateY(-10px) /* rotate(3deg) */ scale(1.15);\n z-index: 1;\n}\n\n.not-selected {\n z-index: 0;\n}\n</style>\n","<template>\n <v-card :class=\"`${className}`\" @mouseover=\"select\" @click=\"submitThisOption\">\n <markdown-renderer :md=\"content\" />\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, defineAsyncComponent, PropType } from 'vue';\n\nexport default defineComponent({\n name: 'MultipleChoiceOption',\n\n components: {\n MarkdownRenderer: defineAsyncComponent(() => import('../../components/cardRendering/MarkdownRenderer.vue')),\n },\n\n props: {\n content: {\n type: String,\n required: true,\n },\n selected: {\n type: Boolean,\n required: true,\n },\n number: {\n type: Number,\n required: true,\n },\n setSelection: {\n type: Function as PropType<(selection: number) => void>,\n required: true,\n },\n submit: {\n type: Function as PropType<() => void>,\n required: true,\n },\n markedWrong: {\n type: Boolean,\n required: true,\n },\n },\n\n computed: {\n className(): string {\n let color: string;\n\n switch (this.number) {\n case 0:\n color = 'bg-red';\n break;\n case 1:\n color = 'bg-purple';\n break;\n case 2:\n color = 'bg-indigo';\n break;\n case 3:\n color = 'bg-light-blue';\n break;\n case 4:\n color = 'bg-teal';\n break;\n case 5:\n color = 'bg-deep-orange';\n break;\n default:\n color = 'bg-grey';\n break;\n }\n\n if (this.selected && !this.markedWrong) {\n return `choice selected ${color} lighten-3 elevation-8`;\n } else if (!this.selected && !this.markedWrong) {\n return `choice not-selected ${color} lighten-4 elevation-1`;\n } else if (this.selected && this.markedWrong) {\n return `choice selected grey lighten-2 elevation-8`;\n } else if (!this.selected && this.markedWrong) {\n return 'choice not-selected grey lighten-2 elevation-0';\n } else {\n throw new Error(\n `'selected' and 'markedWrong' props in MultipleChoiceOption are in an impossible configuration.`\n );\n }\n },\n },\n\n methods: {\n select(): void {\n this.setSelection(this.number);\n },\n\n submitThisOption(): void {\n if (this.markedWrong) {\n return;\n } else {\n this.select();\n this.submit();\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.choice {\n text-align: center;\n display: inline-block;\n border-radius: 4px;\n padding-left: 15px;\n padding-right: 15px;\n padding-top: 5px;\n padding-bottom: 5px;\n margin: 10px;\n min-width: 75px; /* prevent tiny click-btns on, eg, one-letter answers */\n transition: all 0.2s ease-in-out;\n}\n\n.selected {\n transform: translateY(-10px) /* rotate(3deg) */ scale(1.15);\n z-index: 1;\n}\n\n.not-selected {\n z-index: 0;\n}\n</style>\n","<template>\n <div ref=\"containerRef\" class=\"multipleChoice\">\n <MultipleChoiceOption\n v-for=\"(choice, i) in choiceList\"\n :key=\"i\"\n :content=\"choice\"\n :selected=\"choiceList.indexOf(choice) === currentSelection\"\n :number=\"choiceList.indexOf(choice)\"\n :set-selection=\"setSelection\"\n :submit=\"forwardSelection\"\n :marked-wrong=\"choiceIsWrong(choice)\"\n />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport UserInput from './BaseUserInput';\nimport MultipleChoiceOption from './MultipleChoiceOption.vue';\nimport { defineComponent, PropType } from 'vue';\nimport { SkldrMouseTrap } from '../../utils/SkldrMouseTrap';\nimport { RadioMultipleChoiceAnswer } from './RadioMultipleChoice.types';\n\nexport default defineComponent({\n name: 'RadioMultipleChoice',\n components: {\n MultipleChoiceOption,\n },\n extends: UserInput,\n props: {\n choiceList: {\n type: Array as PropType<string[]>,\n required: true,\n },\n },\n data() {\n return {\n currentSelection: -1,\n incorrectSelections: [] as number[],\n containerRef: null as null | HTMLElement,\n _registeredHotkeys: [] as (string | string[])[],\n };\n },\n watch: {\n choiceList: {\n immediate: true,\n handler(newList) {\n if (newList?.length) {\n this.bindKeys();\n }\n },\n },\n },\n mounted() {\n if (this.containerRef) {\n this.containerRef.focus();\n }\n },\n unmounted() {\n this.unbindKeys();\n },\n methods: {\n forwardSelection(): void {\n if (this.choiceIsWrong(this.choiceList[this.currentSelection])) {\n return;\n } else if (this.currentSelection !== -1) {\n const ans: RadioMultipleChoiceAnswer = {\n choiceList: this.choiceList,\n selection: this.currentSelection,\n };\n const record = this.submitAnswer(ans);\n\n if (!record.isCorrect) {\n this.incorrectSelections.push(this.currentSelection);\n }\n }\n },\n setSelection(selection: number): void {\n if (selection < this.choiceList.length) {\n this.currentSelection = selection;\n }\n },\n incrementSelection(): void {\n if (this.currentSelection === -1) {\n this.currentSelection = Math.ceil(this.choiceList.length / 2);\n } else {\n this.currentSelection = Math.min(this.choiceList.length - 1, this.currentSelection + 1);\n }\n },\n decrementSelection(): void {\n if (this.currentSelection === -1) {\n this.currentSelection = Math.floor(this.choiceList.length / 2 - 1);\n } else {\n this.currentSelection = Math.max(0, this.currentSelection - 1);\n }\n },\n choiceIsWrong(choice: string): boolean {\n let ret = false;\n this.incorrectSelections.forEach((sel) => {\n if (this.choiceList[sel] === choice) {\n ret = true;\n }\n });\n return ret;\n },\n bindKeys() {\n const hotkeys = [\n {\n hotkey: 'left',\n callback: this.decrementSelection,\n command: 'Move selection left',\n },\n {\n hotkey: 'right',\n callback: this.incrementSelection,\n command: 'Move selection right',\n },\n {\n hotkey: 'enter',\n callback: this.forwardSelection,\n command: 'Submit selection',\n },\n // Add number key bindings\n ...Array.from({ length: this.choiceList.length }, (_, i) => ({\n hotkey: (i + 1).toString(),\n callback: () => this.setSelection(i),\n command: `Select ${((i) => {\n switch (i) {\n case 0:\n return 'first';\n case 1:\n return 'second';\n case 2:\n return 'third';\n case 3:\n return 'fourth';\n case 4:\n return 'fifth';\n case 5:\n return 'sixth';\n default:\n return `${i + 1}th`;\n }\n })(i)} option`,\n })),\n ];\n\n SkldrMouseTrap.addBinding(hotkeys);\n \n // Store hotkeys for cleanup\n this._registeredHotkeys = hotkeys.map(k => k.hotkey);\n },\n\n unbindKeys() {\n // Remove specific hotkeys instead of resetting all\n if (this._registeredHotkeys) {\n this._registeredHotkeys.forEach(key => {\n SkldrMouseTrap.removeBinding(key);\n });\n }\n },\n // bindNumberKey(n: number): void {\n // this.MouseTrap.bind(n.toString(), () => {\n // this.currentSelection = n - 1;\n // });\n // },\n },\n});\n</script>\n","<template>\n <div ref=\"containerRef\" class=\"multipleChoice\">\n <MultipleChoiceOption\n v-for=\"(choice, i) in choiceList\"\n :key=\"i\"\n :content=\"choice\"\n :selected=\"choiceList.indexOf(choice) === currentSelection\"\n :number=\"choiceList.indexOf(choice)\"\n :set-selection=\"setSelection\"\n :submit=\"forwardSelection\"\n :marked-wrong=\"choiceIsWrong(choice)\"\n />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport UserInput from './BaseUserInput';\nimport MultipleChoiceOption from './MultipleChoiceOption.vue';\nimport { defineComponent, PropType } from 'vue';\nimport { SkldrMouseTrap } from '../../utils/SkldrMouseTrap';\nimport { RadioMultipleChoiceAnswer } from './RadioMultipleChoice.types';\n\nexport default defineComponent({\n name: 'RadioMultipleChoice',\n components: {\n MultipleChoiceOption,\n },\n extends: UserInput,\n props: {\n choiceList: {\n type: Array as PropType<string[]>,\n required: true,\n },\n },\n data() {\n return {\n currentSelection: -1,\n incorrectSelections: [] as number[],\n containerRef: null as null | HTMLElement,\n _registeredHotkeys: [] as (string | string[])[],\n };\n },\n watch: {\n choiceList: {\n immediate: true,\n handler(newList) {\n if (newList?.length) {\n this.bindKeys();\n }\n },\n },\n },\n mounted() {\n if (this.containerRef) {\n this.containerRef.focus();\n }\n },\n unmounted() {\n this.unbindKeys();\n },\n methods: {\n forwardSelection(): void {\n if (this.choiceIsWrong(this.choiceList[this.currentSelection])) {\n return;\n } else if (this.currentSelection !== -1) {\n const ans: RadioMultipleChoiceAnswer = {\n choiceList: this.choiceList,\n selection: this.currentSelection,\n };\n const record = this.submitAnswer(ans);\n\n if (!record.isCorrect) {\n this.incorrectSelections.push(this.currentSelection);\n }\n }\n },\n setSelection(selection: number): void {\n if (selection < this.choiceList.length) {\n this.currentSelection = selection;\n }\n },\n incrementSelection(): void {\n if (this.currentSelection === -1) {\n this.currentSelection = Math.ceil(this.choiceList.length / 2);\n } else {\n this.currentSelection = Math.min(this.choiceList.length - 1, this.currentSelection + 1);\n }\n },\n decrementSelection(): void {\n if (this.currentSelection === -1) {\n this.currentSelection = Math.floor(this.choiceList.length / 2 - 1);\n } else {\n this.currentSelection = Math.max(0, this.currentSelection - 1);\n }\n },\n choiceIsWrong(choice: string): boolean {\n let ret = false;\n this.incorrectSelections.forEach((sel) => {\n if (this.choiceList[sel] === choice) {\n ret = true;\n }\n });\n return ret;\n },\n bindKeys() {\n const hotkeys = [\n {\n hotkey: 'left',\n callback: this.decrementSelection,\n command: 'Move selection left',\n },\n {\n hotkey: 'right',\n callback: this.incrementSelection,\n command: 'Move selection right',\n },\n {\n hotkey: 'enter',\n callback: this.forwardSelection,\n command: 'Submit selection',\n },\n // Add number key bindings\n ...Array.from({ length: this.choiceList.length }, (_, i) => ({\n hotkey: (i + 1).toString(),\n callback: () => this.setSelection(i),\n command: `Select ${((i) => {\n switch (i) {\n case 0:\n return 'first';\n case 1:\n return 'second';\n case 2:\n return 'third';\n case 3:\n return 'fourth';\n case 4:\n return 'fifth';\n case 5:\n return 'sixth';\n default:\n return `${i + 1}th`;\n }\n })(i)} option`,\n })),\n ];\n\n SkldrMouseTrap.addBinding(hotkeys);\n \n // Store hotkeys for cleanup\n this._registeredHotkeys = hotkeys.map(k => k.hotkey);\n },\n\n unbindKeys() {\n // Remove specific hotkeys instead of resetting all\n if (this._registeredHotkeys) {\n this._registeredHotkeys.forEach(key => {\n SkldrMouseTrap.removeBinding(key);\n });\n }\n },\n // bindNumberKey(n: number): void {\n // this.MouseTrap.bind(n.toString(), () => {\n // this.currentSelection = n - 1;\n // });\n // },\n },\n});\n</script>\n","<template>\n <div data-viewable=\"TrueFalse\">\n <RadioMultipleChoice :choice-list=\"['True', 'False']\" :MouseTrap=\"MouseTrap\" :submit=\"submit\" />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport RadioMultipleChoice from './RadioMultipleChoice.vue';\n\nexport default defineComponent({\n name: 'TrueFalse',\n components: {\n RadioMultipleChoice,\n },\n props: {\n MouseTrap: {\n type: Object as PropType<any>,\n required: true,\n },\n submit: {\n type: Function as PropType<(selection: number) => void>,\n required: true,\n },\n },\n});\n</script>\n","<template>\n <div data-viewable=\"TrueFalse\">\n <RadioMultipleChoice :choice-list=\"['True', 'False']\" :MouseTrap=\"MouseTrap\" :submit=\"submit\" />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport RadioMultipleChoice from './RadioMultipleChoice.vue';\n\nexport default defineComponent({\n name: 'TrueFalse',\n components: {\n RadioMultipleChoice,\n },\n props: {\n MouseTrap: {\n type: Object as PropType<any>,\n required: true,\n },\n submit: {\n type: Function as PropType<(selection: number) => void>,\n required: true,\n },\n },\n});\n</script>\n","<template>\n <v-container class=\"pa-0\">\n <v-text-field\n ref=\"input\"\n v-model=\"answer\"\n prepend-icon=\"edit\"\n :autofocus=\"autofocus\"\n row-height=\"24\"\n toggle-keys=\"[13,32]\"\n class=\"text-h5\"\n :rules=\"[isNumeric]\"\n @keyup.enter=\"submitAnswer(makeNumeric(answer))\"\n ></v-text-field>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { Answer } from '@vue-skuilder/common';\nimport UserInput from './BaseUserInput';\nimport { defineComponent } from 'vue';\n\ntype InputNumberRefs = {\n input: HTMLInputElement;\n};\n\ntype InputNumberInstance = ReturnType<typeof defineComponent> & {\n $refs: InputNumberRefs;\n};\n\nexport default defineComponent({\n name: 'UserInputNumber',\n\n ref: {} as InputNumberRefs,\n\n extends: UserInput,\n\n methods: {\n mounted(this: InputNumberInstance) {\n this.$refs.input.focus();\n },\n\n isNumeric(s: string): boolean {\n return !isNaN(Number.parseFloat(s));\n },\n\n makeNumeric(num: Answer): number {\n if (typeof num === 'string') {\n return Number.parseFloat(num);\n } else {\n throw new Error('Expected a string, got ' + typeof num);\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.userInput {\n border: none;\n text-align: center;\n border-bottom: 1px black;\n}\n</style>\n","<template>\n <v-container class=\"pa-0\">\n <v-text-field\n ref=\"input\"\n v-model=\"answer\"\n prepend-icon=\"edit\"\n :autofocus=\"autofocus\"\n row-height=\"24\"\n toggle-keys=\"[13,32]\"\n class=\"text-h5\"\n :rules=\"[isNumeric]\"\n @keyup.enter=\"submitAnswer(makeNumeric(answer))\"\n ></v-text-field>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { Answer } from '@vue-skuilder/common';\nimport UserInput from './BaseUserInput';\nimport { defineComponent } from 'vue';\n\ntype InputNumberRefs = {\n input: HTMLInputElement;\n};\n\ntype InputNumberInstance = ReturnType<typeof defineComponent> & {\n $refs: InputNumberRefs;\n};\n\nexport default defineComponent({\n name: 'UserInputNumber',\n\n ref: {} as InputNumberRefs,\n\n extends: UserInput,\n\n methods: {\n mounted(this: InputNumberInstance) {\n this.$refs.input.focus();\n },\n\n isNumeric(s: string): boolean {\n return !isNaN(Number.parseFloat(s));\n },\n\n makeNumeric(num: Answer): number {\n if (typeof num === 'string') {\n return Number.parseFloat(num);\n } else {\n throw new Error('Expected a string, got ' + typeof num);\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.userInput {\n border: none;\n text-align: center;\n border-bottom: 1px black;\n}\n</style>\n","<template>\n <card-viewer\n v-if=\"!loading\"\n class=\"ma-2\"\n :class=\"loading ? 'muted' : ''\"\n :view=\"view\"\n :data=\"data\"\n :card_id=\"cardID\"\n :course_id=\"courseID\"\n :session-order=\"sessionOrder\"\n @emit-response=\"processResponse($event)\"\n />\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, markRaw } from 'vue';\nimport { getDataLayer, QualifiedCardID } from '@vue-skuilder/db';\nimport {\n log,\n displayableDataToViewData,\n ViewData,\n ViewDescriptor,\n CardData,\n CardRecord,\n DisplayableData,\n} from '@vue-skuilder/common';\nimport { ViewComponent } from '../../composables';\nimport CardViewer from './CardViewer.vue';\n\nexport default defineComponent({\n name: 'CardLoader',\n\n components: {\n CardViewer,\n },\n\n props: {\n sessionOrder: {\n type: Number,\n required: false,\n default: 0,\n },\n qualified_id: {\n type: Object as PropType<QualifiedCardID>,\n required: true,\n },\n viewLookup: {\n type: Function as PropType<(viewDescription: ViewDescriptor | string) => ViewComponent>,\n required: true,\n },\n },\n\n data() {\n return {\n loading: true,\n view: null as ViewComponent | null,\n data: [] as ViewData[],\n courseID: '',\n cardID: '',\n };\n },\n\n created() {\n this.loadCard();\n },\n\n watch: {\n qualified_id: {\n immediate: true,\n handler() {\n this.loadCard();\n },\n },\n },\n\n methods: {\n processResponse(r: CardRecord) {\n log(`\n Card was displayed at ${r.timeStamp}\n User spent ${r.timeSpent} milliseconds with the card.\n `);\n this.$emit('emitResponse', r);\n },\n\n async loadCard() {\n const qualified_id = this.qualified_id;\n console.log(`Card Loader displaying: ${qualified_id.courseID}::${qualified_id.cardID}`);\n\n this.loading = true;\n const _courseID = qualified_id.courseID;\n const _cardID = qualified_id.cardID;\n const courseDB = getDataLayer().getCourseDB(_courseID);\n\n try {\n const tmpCardData = (await courseDB.getCourseDoc(_cardID)) as CardData;\n const tmpView = this.viewLookup(tmpCardData.id_view);\n const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {\n return courseDB.getCourseDoc(id, {\n attachments: true,\n binary: true,\n });\n });\n\n const tmpData = [];\n\n for (const docPromise of tmpDataDocs) {\n const doc = (await docPromise) as DisplayableData;\n tmpData.unshift(displayableDataToViewData(doc));\n }\n\n this.data = tmpData;\n this.view = markRaw(tmpView as ViewComponent);\n this.cardID = _cardID;\n this.courseID = _courseID;\n } catch (e) {\n throw new Error(`[CardLoader] Error loading card: ${JSON.stringify(e)}, ${e}`);\n } finally {\n this.loading = false;\n this.$emit('card-loaded');\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.cardView {\n padding: 15px;\n border-radius: 8px;\n}\n\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.3s ease;\n}\n.component-fade-enter, .component-fade-leave-to\n/* .component-fade-leave-active below version 2.1.8 */ {\n opacity: 0;\n}\n</style>\n","<template>\n <card-viewer\n v-if=\"!loading\"\n class=\"ma-2\"\n :class=\"loading ? 'muted' : ''\"\n :view=\"view\"\n :data=\"data\"\n :card_id=\"cardID\"\n :course_id=\"courseID\"\n :session-order=\"sessionOrder\"\n @emit-response=\"processResponse($event)\"\n />\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, markRaw } from 'vue';\nimport { getDataLayer, QualifiedCardID } from '@vue-skuilder/db';\nimport {\n log,\n displayableDataToViewData,\n ViewData,\n ViewDescriptor,\n CardData,\n CardRecord,\n DisplayableData,\n} from '@vue-skuilder/common';\nimport { ViewComponent } from '../../composables';\nimport CardViewer from './CardViewer.vue';\n\nexport default defineComponent({\n name: 'CardLoader',\n\n components: {\n CardViewer,\n },\n\n props: {\n sessionOrder: {\n type: Number,\n required: false,\n default: 0,\n },\n qualified_id: {\n type: Object as PropType<QualifiedCardID>,\n required: true,\n },\n viewLookup: {\n type: Function as PropType<(viewDescription: ViewDescriptor | string) => ViewComponent>,\n required: true,\n },\n },\n\n data() {\n return {\n loading: true,\n view: null as ViewComponent | null,\n data: [] as ViewData[],\n courseID: '',\n cardID: '',\n };\n },\n\n created() {\n this.loadCard();\n },\n\n watch: {\n qualified_id: {\n immediate: true,\n handler() {\n this.loadCard();\n },\n },\n },\n\n methods: {\n processResponse(r: CardRecord) {\n log(`\n Card was displayed at ${r.timeStamp}\n User spent ${r.timeSpent} milliseconds with the card.\n `);\n this.$emit('emitResponse', r);\n },\n\n async loadCard() {\n const qualified_id = this.qualified_id;\n console.log(`Card Loader displaying: ${qualified_id.courseID}::${qualified_id.cardID}`);\n\n this.loading = true;\n const _courseID = qualified_id.courseID;\n const _cardID = qualified_id.cardID;\n const courseDB = getDataLayer().getCourseDB(_courseID);\n\n try {\n const tmpCardData = (await courseDB.getCourseDoc(_cardID)) as CardData;\n const tmpView = this.viewLookup(tmpCardData.id_view);\n const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {\n return courseDB.getCourseDoc(id, {\n attachments: true,\n binary: true,\n });\n });\n\n const tmpData = [];\n\n for (const docPromise of tmpDataDocs) {\n const doc = (await docPromise) as DisplayableData;\n tmpData.unshift(displayableDataToViewData(doc));\n }\n\n this.data = tmpData;\n this.view = markRaw(tmpView as ViewComponent);\n this.cardID = _cardID;\n this.courseID = _courseID;\n } catch (e) {\n throw new Error(`[CardLoader] Error loading card: ${JSON.stringify(e)}, ${e}`);\n } finally {\n this.loading = false;\n this.$emit('card-loaded');\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.cardView {\n padding: 15px;\n border-radius: 8px;\n}\n\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.3s ease;\n}\n.component-fade-enter, .component-fade-leave-to\n/* .component-fade-leave-active below version 2.1.8 */ {\n opacity: 0;\n}\n</style>\n","// stores/useAuthStore.ts\nimport { defineStore, setActivePinia } from 'pinia';\nimport { getDataLayer, UserDBInterface } from '@vue-skuilder/db';\nimport { getPinia } from '../plugins/pinia';\n\nexport interface AuthState {\n _user: UserDBInterface | undefined;\n loginAndRegistration: {\n init: boolean;\n loggedIn: boolean;\n regDialogOpen: boolean;\n loginDialogOpen: boolean;\n };\n onLoadComplete: boolean;\n}\n\nexport async function getCurrentUser(): Promise<UserDBInterface | undefined> {\n const store = useAuthStore();\n\n if (!store.onLoadComplete) {\n // Wait for initialization\n let retries = 200;\n const timeout = 50;\n while (!store.onLoadComplete && retries > 0) {\n await new Promise((resolve) => setTimeout(resolve, timeout));\n retries--;\n }\n if (!store.onLoadComplete) {\n throw new Error('User initialization timed out');\n }\n }\n\n return getDataLayer().getUserDB();\n}\n\nexport const useAuthStore = () => {\n // Get the Pinia instance from the plugin\n const pinia = getPinia();\n if (pinia) {\n setActivePinia(pinia);\n }\n\n // Return the store\n return defineStore('auth', {\n state: (): AuthState => ({\n _user: undefined as UserDBInterface | undefined,\n loginAndRegistration: {\n init: false,\n loggedIn: false,\n regDialogOpen: false,\n loginDialogOpen: false,\n },\n onLoadComplete: false,\n }),\n\n actions: {\n async init() {\n try {\n this._user = getDataLayer().getUserDB();\n\n this.loginAndRegistration.loggedIn = this._user ? this._user.isLoggedIn() : false;\n\n this.onLoadComplete = true;\n this.loginAndRegistration.init = true;\n } catch (e) {\n console.error('Failed to initialize auth store:', e);\n // Set onLoadComplete even on error to prevent timeout\n this.loginAndRegistration.loggedIn = false;\n this.onLoadComplete = true;\n this.loginAndRegistration.init = true;\n }\n },\n\n setLoginDialog(open: boolean) {\n this.loginAndRegistration.loginDialogOpen = open;\n },\n\n setRegDialog(open: boolean) {\n this.loginAndRegistration.regDialogOpen = open;\n },\n\n async resetUserData() {\n try {\n if (!this._user) {\n throw new Error('No user available for data reset');\n }\n \n const result = await this._user.resetUserData();\n if (result.status !== 'ok') {\n throw new Error(result.error || 'Reset failed');\n }\n \n console.log('User data reset successfully');\n return result;\n } catch (error) {\n console.error('Failed to reset user data:', error);\n throw error;\n }\n },\n },\n\n getters: {\n currentUser: async () => getCurrentUser(),\n isLoggedIn: (state) => state.loginAndRegistration.loggedIn,\n isInitialized: (state) => state.loginAndRegistration.init,\n status: (state) => {\n return {\n loggedIn: state.loginAndRegistration.loggedIn,\n init: state.loginAndRegistration.init,\n user: state._user,\n };\n },\n },\n })();\n};\n","// stores/useConfigStore.ts\nimport { defineStore, setActivePinia } from 'pinia';\nimport { getCurrentUser } from './useAuthStore';\nimport { UserConfig } from '@vue-skuilder/db';\nimport { getPinia } from '../plugins/pinia';\n\nexport const useConfigStore = () => {\n // Get the Pinia instance from the plugin\n const pinia = getPinia();\n if (pinia) {\n setActivePinia(pinia);\n }\n\n // Return the store\n return defineStore('config', {\n state: () => ({\n config: {\n darkMode: false,\n likesConfetti: false,\n sessionTimeLimit: 5, // Default 5 minutes\n } as UserConfig,\n }),\n\n actions: {\n updateConfig(newConfig: UserConfig) {\n this.config = newConfig;\n },\n async updateDarkMode(darkMode: boolean) {\n this.config.darkMode = darkMode;\n const user = await getCurrentUser();\n if (user) {\n await user.setConfig({\n darkMode,\n });\n }\n },\n\n async updateLikesConfetti(likesConfetti: boolean) {\n this.config.likesConfetti = likesConfetti;\n const user = await getCurrentUser();\n if (user) {\n await user.setConfig({\n likesConfetti,\n });\n }\n },\n\n async updateSessionTimeLimit(sessionTimeLimit: number) {\n this.config.sessionTimeLimit = sessionTimeLimit;\n const user = await getCurrentUser();\n if (user) {\n await user.setConfig({\n sessionTimeLimit,\n });\n }\n },\n\n async hydrate() {\n try {\n const user = await getCurrentUser();\n if (user) {\n const cfg = await user.getConfig();\n console.log(`user config: ${JSON.stringify(cfg)}`);\n this.updateConfig(cfg);\n } else {\n console.log('No user logged in, using default config');\n }\n } catch (e) {\n console.warn('Failed to hydrate config store, using defaults:', e);\n }\n },\n async init() {\n await this.hydrate();\n },\n resetDefaults() {\n this.config = {\n darkMode: false,\n likesConfetti: false,\n sessionTimeLimit: 5,\n };\n },\n },\n })();\n};\n","import { ref, computed } from 'vue';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\nexport interface AuthUIConfig {\n showLoginRegistration: boolean;\n showLogout: boolean;\n showResetData: boolean;\n logoutLabel: string;\n resetLabel: string;\n}\n\nexport function useAuthUI() {\n const isLoading = ref(true);\n const syncStrategyDetected = ref(false);\n const isLocalOnlyMode = ref(false);\n\n const config = computed<AuthUIConfig>(() => {\n if (isLocalOnlyMode.value) {\n return {\n showLoginRegistration: false,\n showLogout: false,\n showResetData: true,\n logoutLabel: '',\n resetLabel: 'Reset User Data',\n };\n } else {\n return {\n showLoginRegistration: true,\n showLogout: true,\n showResetData: false,\n logoutLabel: 'Log out',\n resetLabel: '',\n };\n }\n });\n\n const detectSyncStrategy = async () => {\n try {\n isLoading.value = true;\n const user = await getCurrentUser();\n \n // Access the sync strategy through the user's private syncStrategy property\n // NoOpSyncStrategy (local-only) returns false for canCreateAccount\n // CouchDBSyncStrategy (remote sync) returns true for canCreateAccount\n const userInternal = user as { syncStrategy?: { canCreateAccount?: () => boolean } };\n const canCreateAccount = userInternal.syncStrategy?.canCreateAccount?.();\n \n isLocalOnlyMode.value = !canCreateAccount;\n syncStrategyDetected.value = true;\n } catch (error) {\n console.error('Failed to detect sync strategy:', error);\n // Default to remote sync mode on error\n isLocalOnlyMode.value = false;\n syncStrategyDetected.value = true;\n } finally {\n isLoading.value = false;\n }\n };\n\n return {\n config,\n isLoading,\n syncStrategyDetected,\n isLocalOnlyMode,\n detectSyncStrategy,\n };\n}","<template>\n <v-badge :content=\"items.length\" :model-value=\"hasNewItems\" color=\"accent\" location=\"end top\">\n <v-menu location=\"bottom end\" transition=\"scale-transition\">\n <template #activator=\"{ props }\">\n <v-chip v-bind=\"props\" class=\"ma-2\">\n <v-avatar start class=\"bg-primary\">\n <v-icon>mdi-school</v-icon>\n </v-avatar>\n {{ username }}\n </v-chip>\n </template>\n\n <v-list>\n <v-list-item v-for=\"item in items\" :key=\"item\" @click=\"dismiss(item)\">\n <v-list-item-title>{{ item }}</v-list-item-title>\n </v-list-item>\n\n <v-divider v-if=\"items.length\" />\n\n <v-list-item @click=\"gotoStats\">\n <template #prepend>\n <v-icon>mdi-trending-up</v-icon>\n </template>\n <v-list-item-title>Stats</v-list-item-title>\n </v-list-item>\n\n <v-list-item @click=\"gotoSettings\">\n <template #prepend>\n <v-icon>mdi-cog</v-icon>\n </template>\n <v-list-item-title>Settings</v-list-item-title>\n </v-list-item>\n\n <v-list-item v-if=\"authUIConfig.showLogout\" @click=\"logout\">\n <template #prepend>\n <v-icon>mdi-logout</v-icon>\n </template>\n <v-list-item-title>{{ authUIConfig.logoutLabel }}</v-list-item-title>\n </v-list-item>\n\n <v-list-item v-if=\"authUIConfig.showResetData\" @click=\"showResetDialog = true\">\n <template #prepend>\n <v-icon>mdi-delete-sweep</v-icon>\n </template>\n <v-list-item-title>{{ authUIConfig.resetLabel }}</v-list-item-title>\n </v-list-item>\n </v-list>\n </v-menu>\n </v-badge>\n\n <!-- Reset Confirmation Dialog -->\n <v-dialog v-model=\"showResetDialog\" max-width=\"500px\" persistent>\n <v-card>\n <v-card-title class=\"text-h5 d-flex align-center\">\n <v-icon color=\"warning\" class=\"mr-3\">mdi-alert-circle</v-icon>\n Reset All User Data\n </v-card-title>\n \n <v-card-text>\n <p class=\"mb-4\">This will permanently delete:</p>\n <ul class=\"mb-4\">\n <li>All course progress and history</li>\n <li>Scheduled card reviews</li>\n <li>Course registrations</li>\n <li>User preferences</li>\n </ul>\n <p class=\"mb-4 text-error font-weight-bold\">This cannot be undone.</p>\n \n <v-text-field\n v-model=\"confirmationText\"\n label='Type \"reset\" to confirm'\n outlined\n dense\n @keyup.enter=\"isConfirmationValid && executeReset()\"\n />\n </v-card-text>\n \n <v-card-actions>\n <v-spacer />\n <v-btn text @click=\"resetDialogState\">Cancel</v-btn>\n <v-btn \n color=\"error\" \n :disabled=\"!isConfirmationValid\"\n @click=\"executeReset\"\n >\n Reset All Data\n </v-btn>\n </v-card-actions>\n </v-card>\n </v-dialog>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, computed, onMounted } from 'vue';\nimport { useRouter } from 'vue-router';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { useConfigStore } from '../../stores/useConfigStore';\nimport { useAuthUI } from '../../composables/useAuthUI';\n\n// Define props (even if not used, prevents warnings)\ndefineProps<{\n showLoginButton?: boolean;\n redirectToPath?: string;\n}>();\n\nconst router = useRouter();\nconst authStore = useAuthStore();\nconst configStore = useConfigStore();\nconst authUI = useAuthUI();\n\nconst username = ref('');\nconst items = ref<string[]>([]);\n\n// Reset confirmation dialog state\nconst showResetDialog = ref(false);\nconst confirmationText = ref('');\n\nconst isConfirmationValid = computed(() => confirmationText.value === 'reset');\n\nconst resetDialogState = () => {\n confirmationText.value = '';\n showResetDialog.value = false;\n};\n\nconst hasNewItems = computed(() => items.value.length > 0);\n\nconst authUIConfig = computed(() => {\n const configValue = authUI.config.value;\n const fallback = {\n showLoginRegistration: true,\n showLogout: true,\n showResetData: false,\n logoutLabel: 'Log out',\n resetLabel: '',\n };\n \n return configValue || fallback;\n});\n\nonMounted(async () => {\n const user = await getCurrentUser();\n username.value = user.getUsername();\n \n // Detect sync strategy for conditional UI\n await authUI.detectSyncStrategy();\n});\n\nconst gotoSettings = async () => {\n router.push(`/u/${(await getCurrentUser()).getUsername()}`);\n};\n\nconst gotoStats = async () => {\n router.push(`/u/${(await getCurrentUser()).getUsername()}/stats`);\n};\n\nconst dismiss = (item: string) => {\n const index = items.value.indexOf(item);\n items.value.splice(index, 1);\n};\n\nconst logout = async () => {\n const res = await authStore._user!.logout();\n if (res.ok) {\n authStore.loginAndRegistration = {\n init: true,\n loggedIn: false,\n regDialogOpen: false,\n loginDialogOpen: false,\n };\n\n configStore.resetDefaults();\n router.push('/home');\n }\n};\n\nconst executeReset = async () => {\n try {\n await authStore.resetUserData();\n configStore.resetDefaults();\n \n // Close dialog and navigate to home\n resetDialogState();\n router.push('/home');\n } catch (error) {\n console.error('Failed to reset user data:', error);\n // Could add a snackbar notification here\n }\n};\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n</style>","<template>\n <v-badge :content=\"items.length\" :model-value=\"hasNewItems\" color=\"accent\" location=\"end top\">\n <v-menu location=\"bottom end\" transition=\"scale-transition\">\n <template #activator=\"{ props }\">\n <v-chip v-bind=\"props\" class=\"ma-2\">\n <v-avatar start class=\"bg-primary\">\n <v-icon>mdi-school</v-icon>\n </v-avatar>\n {{ username }}\n </v-chip>\n </template>\n\n <v-list>\n <v-list-item v-for=\"item in items\" :key=\"item\" @click=\"dismiss(item)\">\n <v-list-item-title>{{ item }}</v-list-item-title>\n </v-list-item>\n\n <v-divider v-if=\"items.length\" />\n\n <v-list-item @click=\"gotoStats\">\n <template #prepend>\n <v-icon>mdi-trending-up</v-icon>\n </template>\n <v-list-item-title>Stats</v-list-item-title>\n </v-list-item>\n\n <v-list-item @click=\"gotoSettings\">\n <template #prepend>\n <v-icon>mdi-cog</v-icon>\n </template>\n <v-list-item-title>Settings</v-list-item-title>\n </v-list-item>\n\n <v-list-item v-if=\"authUIConfig.showLogout\" @click=\"logout\">\n <template #prepend>\n <v-icon>mdi-logout</v-icon>\n </template>\n <v-list-item-title>{{ authUIConfig.logoutLabel }}</v-list-item-title>\n </v-list-item>\n\n <v-list-item v-if=\"authUIConfig.showResetData\" @click=\"showResetDialog = true\">\n <template #prepend>\n <v-icon>mdi-delete-sweep</v-icon>\n </template>\n <v-list-item-title>{{ authUIConfig.resetLabel }}</v-list-item-title>\n </v-list-item>\n </v-list>\n </v-menu>\n </v-badge>\n\n <!-- Reset Confirmation Dialog -->\n <v-dialog v-model=\"showResetDialog\" max-width=\"500px\" persistent>\n <v-card>\n <v-card-title class=\"text-h5 d-flex align-center\">\n <v-icon color=\"warning\" class=\"mr-3\">mdi-alert-circle</v-icon>\n Reset All User Data\n </v-card-title>\n \n <v-card-text>\n <p class=\"mb-4\">This will permanently delete:</p>\n <ul class=\"mb-4\">\n <li>All course progress and history</li>\n <li>Scheduled card reviews</li>\n <li>Course registrations</li>\n <li>User preferences</li>\n </ul>\n <p class=\"mb-4 text-error font-weight-bold\">This cannot be undone.</p>\n \n <v-text-field\n v-model=\"confirmationText\"\n label='Type \"reset\" to confirm'\n outlined\n dense\n @keyup.enter=\"isConfirmationValid && executeReset()\"\n />\n </v-card-text>\n \n <v-card-actions>\n <v-spacer />\n <v-btn text @click=\"resetDialogState\">Cancel</v-btn>\n <v-btn \n color=\"error\" \n :disabled=\"!isConfirmationValid\"\n @click=\"executeReset\"\n >\n Reset All Data\n </v-btn>\n </v-card-actions>\n </v-card>\n </v-dialog>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, computed, onMounted } from 'vue';\nimport { useRouter } from 'vue-router';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { useConfigStore } from '../../stores/useConfigStore';\nimport { useAuthUI } from '../../composables/useAuthUI';\n\n// Define props (even if not used, prevents warnings)\ndefineProps<{\n showLoginButton?: boolean;\n redirectToPath?: string;\n}>();\n\nconst router = useRouter();\nconst authStore = useAuthStore();\nconst configStore = useConfigStore();\nconst authUI = useAuthUI();\n\nconst username = ref('');\nconst items = ref<string[]>([]);\n\n// Reset confirmation dialog state\nconst showResetDialog = ref(false);\nconst confirmationText = ref('');\n\nconst isConfirmationValid = computed(() => confirmationText.value === 'reset');\n\nconst resetDialogState = () => {\n confirmationText.value = '';\n showResetDialog.value = false;\n};\n\nconst hasNewItems = computed(() => items.value.length > 0);\n\nconst authUIConfig = computed(() => {\n const configValue = authUI.config.value;\n const fallback = {\n showLoginRegistration: true,\n showLogout: true,\n showResetData: false,\n logoutLabel: 'Log out',\n resetLabel: '',\n };\n \n return configValue || fallback;\n});\n\nonMounted(async () => {\n const user = await getCurrentUser();\n username.value = user.getUsername();\n \n // Detect sync strategy for conditional UI\n await authUI.detectSyncStrategy();\n});\n\nconst gotoSettings = async () => {\n router.push(`/u/${(await getCurrentUser()).getUsername()}`);\n};\n\nconst gotoStats = async () => {\n router.push(`/u/${(await getCurrentUser()).getUsername()}/stats`);\n};\n\nconst dismiss = (item: string) => {\n const index = items.value.indexOf(item);\n items.value.splice(index, 1);\n};\n\nconst logout = async () => {\n const res = await authStore._user!.logout();\n if (res.ok) {\n authStore.loginAndRegistration = {\n init: true,\n loggedIn: false,\n regDialogOpen: false,\n loginDialogOpen: false,\n };\n\n configStore.resetDefaults();\n router.push('/home');\n }\n};\n\nconst executeReset = async () => {\n try {\n await authStore.resetUserData();\n configStore.resetDefaults();\n \n // Close dialog and navigate to home\n resetDialogState();\n router.push('/home');\n } catch (error) {\n console.error('Failed to reset user data:', error);\n // Could add a snackbar notification here\n }\n};\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n</style>","<template>\n <v-card>\n <v-card-title v-if=\"!loginRoute\" class=\"text-h5 bg-grey-lighten-2\">Log In</v-card-title>\n\n <v-card-text>\n <v-form onsubmit=\"return false;\" @submit.prevent=\"login\">\n <v-text-field\n id=\"\"\n v-model=\"username\"\n autofocus\n name=\"username\"\n label=\"Username\"\n prepend-icon=\"mdi-account-circle\"\n ></v-text-field>\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n hover=\"Show password input\"\n label=\"Enter your password\"\n hint=\"\"\n min=\"0\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n\n <v-snackbar v-model=\"badLoginAttempt\" location=\"bottom right\" :timeout=\"errorTimeout\">\n Username or password was incorrect.\n <v-btn color=\"pink\" variant=\"text\" @click=\"badLoginAttempt = false\">Close</v-btn>\n </v-snackbar>\n\n <div class=\"d-flex flex-column align-start\">\n <div class=\"mb-2\">\n <v-btn class=\"mr-2\" type=\"submit\" :loading=\"awaitingResponse\" :color=\"buttonStatus.color\">\n <v-icon start>mdi-lock-open</v-icon>\n Log In\n </v-btn>\n <router-link v-if=\"loginRoute\" to=\"signup\">\n <v-btn variant=\"text\">Create New Account</v-btn>\n </router-link>\n <v-btn v-else variant=\"text\" @click=\"toggle\">Create New Account</v-btn>\n </div>\n\n <slot name=\"forgot-password\">\n <!-- Default: always show forgot password link -->\n <a\n href=\"#\"\n class=\"text-caption text-decoration-none\"\n @click.prevent=\"handleForgotPassword\"\n >\n Forgot password?\n </a>\n </slot>\n </div>\n </v-form>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, computed } from 'vue';\nimport { useRouter, useRoute } from 'vue-router';\nimport { alertUser } from '../SnackbarService';\nimport { log } from '@vue-skuilder/common';\nimport { Status } from '@vue-skuilder/common';\nimport { User } from '@vue-skuilder/db';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { useConfigStore } from '../../stores/useConfigStore';\n\n// Define props\ninterface Props {\n redirectTo?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n redirectTo: '/study'\n});\n\n// Define emits for toggle, cleanup, and forgot password\nconst emit = defineEmits<{\n toggle: [];\n loginSuccess: [redirectPath: string];\n forgotPassword: [];\n}>();\n\nconst router = useRouter();\nconst route = useRoute();\nconst authStore = useAuthStore();\nconst configStore = useConfigStore();\n\nconst username = ref('');\nconst password = ref('');\nconst passwordVisible = ref(false);\nconst awaitingResponse = ref(false);\nconst badLoginAttempt = ref(false);\nconst errorTimeout = ref(7000);\nconst user = ref<User | undefined>(undefined);\n\nconst loginRoute = computed(() => route.name === 'login');\n\nconst buttonStatus = computed(() => ({\n color: badLoginAttempt.value ? 'error' : 'success',\n text: badLoginAttempt.value ? 'Try again' : 'Log In',\n}));\n\nconst initBadLogin = () => {\n badLoginAttempt.value = true;\n\n alertUser({\n text: 'Username or password was incorrect.',\n status: Status.error,\n timeout: errorTimeout.value,\n });\n setTimeout(() => {\n badLoginAttempt.value = false;\n }, errorTimeout.value);\n};\n\nconst login = async () => {\n awaitingResponse.value = true;\n log('Starting login attempt');\n log(`Login attempt for username: ${username.value}`);\n\n try {\n log('Attempting to get User instance');\n // #172 starting point - why is the pre-existing _user being referenced here?\n user.value = await getCurrentUser();\n log('Got User instance, attempting login');\n\n await user.value.login(username.value, password.value);\n log('Login successful');\n\n // load user config\n log('Initializing user config');\n configStore.init();\n log('User config initialized');\n\n // set login state\n log('Setting authentication state');\n authStore.loginAndRegistration.loggedIn = true;\n log(`Authentication state set, redirecting to: ${props.redirectTo}`);\n \n // Emit success event for cleanup\n emit('loginSuccess', props.redirectTo);\n \n router.push(props.redirectTo);\n log('Login and redirect complete');\n } catch (e) {\n // entry #186\n log('Login attempt failed');\n log(`Login error details: ${JSON.stringify(e)}`);\n console.log(`login error: ${JSON.stringify(e)}`);\n // - differentiate response\n // - return better message to UI\n log('Initiating bad login feedback');\n initBadLogin();\n }\n\n log('Resetting awaiting response state');\n awaitingResponse.value = false;\n};\n\n\nconst toggle = () => {\n log('Toggling registration / login forms.');\n emit('toggle');\n};\n\nconst handleForgotPassword = () => {\n log('Forgot password clicked');\n // If on login route, navigate directly\n if (loginRoute.value) {\n router.push('/request-reset');\n } else {\n // Otherwise, emit event for parent to handle (e.g., modal context)\n emit('forgotPassword');\n }\n};\n</script>\n\n<style lang=\"css\" scoped>\n.login {\n max-width: 650px;\n}\n</style>\n","<template>\n <v-card>\n <v-card-title v-if=\"!loginRoute\" class=\"text-h5 bg-grey-lighten-2\">Log In</v-card-title>\n\n <v-card-text>\n <v-form onsubmit=\"return false;\" @submit.prevent=\"login\">\n <v-text-field\n id=\"\"\n v-model=\"username\"\n autofocus\n name=\"username\"\n label=\"Username\"\n prepend-icon=\"mdi-account-circle\"\n ></v-text-field>\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n hover=\"Show password input\"\n label=\"Enter your password\"\n hint=\"\"\n min=\"0\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n\n <v-snackbar v-model=\"badLoginAttempt\" location=\"bottom right\" :timeout=\"errorTimeout\">\n Username or password was incorrect.\n <v-btn color=\"pink\" variant=\"text\" @click=\"badLoginAttempt = false\">Close</v-btn>\n </v-snackbar>\n\n <div class=\"d-flex flex-column align-start\">\n <div class=\"mb-2\">\n <v-btn class=\"mr-2\" type=\"submit\" :loading=\"awaitingResponse\" :color=\"buttonStatus.color\">\n <v-icon start>mdi-lock-open</v-icon>\n Log In\n </v-btn>\n <router-link v-if=\"loginRoute\" to=\"signup\">\n <v-btn variant=\"text\">Create New Account</v-btn>\n </router-link>\n <v-btn v-else variant=\"text\" @click=\"toggle\">Create New Account</v-btn>\n </div>\n\n <slot name=\"forgot-password\">\n <!-- Default: always show forgot password link -->\n <a\n href=\"#\"\n class=\"text-caption text-decoration-none\"\n @click.prevent=\"handleForgotPassword\"\n >\n Forgot password?\n </a>\n </slot>\n </div>\n </v-form>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, computed } from 'vue';\nimport { useRouter, useRoute } from 'vue-router';\nimport { alertUser } from '../SnackbarService';\nimport { log } from '@vue-skuilder/common';\nimport { Status } from '@vue-skuilder/common';\nimport { User } from '@vue-skuilder/db';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { useConfigStore } from '../../stores/useConfigStore';\n\n// Define props\ninterface Props {\n redirectTo?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n redirectTo: '/study'\n});\n\n// Define emits for toggle, cleanup, and forgot password\nconst emit = defineEmits<{\n toggle: [];\n loginSuccess: [redirectPath: string];\n forgotPassword: [];\n}>();\n\nconst router = useRouter();\nconst route = useRoute();\nconst authStore = useAuthStore();\nconst configStore = useConfigStore();\n\nconst username = ref('');\nconst password = ref('');\nconst passwordVisible = ref(false);\nconst awaitingResponse = ref(false);\nconst badLoginAttempt = ref(false);\nconst errorTimeout = ref(7000);\nconst user = ref<User | undefined>(undefined);\n\nconst loginRoute = computed(() => route.name === 'login');\n\nconst buttonStatus = computed(() => ({\n color: badLoginAttempt.value ? 'error' : 'success',\n text: badLoginAttempt.value ? 'Try again' : 'Log In',\n}));\n\nconst initBadLogin = () => {\n badLoginAttempt.value = true;\n\n alertUser({\n text: 'Username or password was incorrect.',\n status: Status.error,\n timeout: errorTimeout.value,\n });\n setTimeout(() => {\n badLoginAttempt.value = false;\n }, errorTimeout.value);\n};\n\nconst login = async () => {\n awaitingResponse.value = true;\n log('Starting login attempt');\n log(`Login attempt for username: ${username.value}`);\n\n try {\n log('Attempting to get User instance');\n // #172 starting point - why is the pre-existing _user being referenced here?\n user.value = await getCurrentUser();\n log('Got User instance, attempting login');\n\n await user.value.login(username.value, password.value);\n log('Login successful');\n\n // load user config\n log('Initializing user config');\n configStore.init();\n log('User config initialized');\n\n // set login state\n log('Setting authentication state');\n authStore.loginAndRegistration.loggedIn = true;\n log(`Authentication state set, redirecting to: ${props.redirectTo}`);\n \n // Emit success event for cleanup\n emit('loginSuccess', props.redirectTo);\n \n router.push(props.redirectTo);\n log('Login and redirect complete');\n } catch (e) {\n // entry #186\n log('Login attempt failed');\n log(`Login error details: ${JSON.stringify(e)}`);\n console.log(`login error: ${JSON.stringify(e)}`);\n // - differentiate response\n // - return better message to UI\n log('Initiating bad login feedback');\n initBadLogin();\n }\n\n log('Resetting awaiting response state');\n awaitingResponse.value = false;\n};\n\n\nconst toggle = () => {\n log('Toggling registration / login forms.');\n emit('toggle');\n};\n\nconst handleForgotPassword = () => {\n log('Forgot password clicked');\n // If on login route, navigate directly\n if (loginRoute.value) {\n router.push('/request-reset');\n } else {\n // Otherwise, emit event for parent to handle (e.g., modal context)\n emit('forgotPassword');\n }\n};\n</script>\n\n<style lang=\"css\" scoped>\n.login {\n max-width: 650px;\n}\n</style>\n","/**\n * Simple password validation utilities.\n *\n * Requirements:\n * - At least 6 characters\n * - At least 2 distinct characters\n */\n\nexport function validatePassword(password: string): string {\n if (!password) return '';\n\n // At least 6 characters\n if (password.length < 6) {\n return 'Password must be at least 6 characters';\n }\n\n // At least 2 distinct characters\n const uniqueChars = new Set(password).size;\n if (uniqueChars < 2) {\n return 'Password must contain at least 2 different characters';\n }\n\n return '';\n}\n\n/**\n * Check if password is valid (no error message)\n */\nexport function isPasswordValid(password: string): boolean {\n return validatePassword(password) === '';\n}\n","<template>\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\"> Create an Account </v-card-title>\n\n <v-card-text>\n <v-form @submit.prevent=\"createUser\">\n <v-text-field\n v-model=\"email\"\n name=\"email\"\n label=\"Email address\"\n type=\"email\"\n prepend-icon=\"mdi-email\"\n :error=\"emailError\"\n :hint=\"emailHint\"\n @blur=\"validateEmail\"\n ></v-text-field>\n <v-text-field\n id=\"\"\n ref=\"userNameTextField\"\n v-model=\"username\"\n name=\"username\"\n label=\"Choose a Username\"\n prepend-icon=\"mdi-account-circle\"\n :error=\"usernameError\"\n :hint=\"usernameHint\"\n @blur=\"validateUsername\"\n ></v-text-field>\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n hover=\"Show password\"\n label=\"Create a password\"\n :hint=\"passwordError\"\n :error=\"!!passwordError\"\n min=\"4\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n <v-text-field\n v-model=\"retypedPassword\"\n prepend-icon=\"mdi-lock\"\n name=\"retypedPassword\"\n hover=\"Show password\"\n label=\"Retype your password\"\n :disabled=\"password === '' || !!passwordError\"\n :hint=\"passwordRetypeError\"\n min=\"4\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n ></v-text-field>\n\n <!-- <v-checkbox label=\"Student\" v-model=\"student\" ></v-checkbox>\n <v-checkbox label=\"Teacher\" v-model=\"teacher\" ></v-checkbox>\n <v-checkbox label=\"Author\" v-model=\"author\" ></v-checkbox> -->\n\n <v-snackbar v-model=\"badLoginAttempt\" location=\"bottom right\" :timeout=\"5000\">\n Username or password was incorrect.\n <v-btn color=\"pink\" variant=\"text\" @click=\"badLoginAttempt = false\"> Close </v-btn>\n </v-snackbar>\n <v-btn\n class=\"mr-2 my-2\"\n type=\"submit\"\n :loading=\"awaitingResponse\"\n :color=\"buttonStatus.color\"\n :disabled=\"!!passwordError || password !== retypedPassword\"\n >\n <v-icon start>mdi-lock-open</v-icon>\n Create Account\n </v-btn>\n <router-link v-if=\"registrationRoute\" to=\"login\">\n <v-btn variant=\"text\">Log In</v-btn>\n </router-link>\n <v-btn v-else variant=\"text\" @click=\"toggle\"> Log In </v-btn>\n </v-form>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { UserDBInterface } from '@vue-skuilder/db';\nimport { alertUser } from '../SnackbarService';\nimport { Status, log } from '@vue-skuilder/common';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { sendVerificationEmail } from '../../services/authAPI';\nimport { validatePassword } from '../../utils/passwordValidation';\n\nexport default defineComponent({\n name: 'UserRegistration',\n\n props: {\n onSignupSuccess: {\n type: Function as PropType<(payload: { username: string }) => void>,\n required: false,\n },\n },\n\n emits: ['toggle', 'signup-success'],\n\n data() {\n return {\n email: '',\n username: '',\n password: '',\n retypedPassword: '',\n passwordVisible: false,\n emailError: false,\n emailHint: '',\n usernameValidationInProgress: false,\n usernameError: false,\n usernameHint: '',\n awaitingResponse: false,\n badLoginAttempt: false,\n userSecret: '',\n secret: 'goons',\n user: null as UserDBInterface | null,\n roles: ['Student', 'Teacher', 'Author'] as string[],\n student: true,\n teacher: false,\n author: false,\n authStore: useAuthStore(),\n };\n },\n\n computed: {\n registrationRoute(): boolean {\n return typeof this.$route.name === 'string' && this.$route.name.toLowerCase() === 'signup';\n },\n buttonStatus() {\n return {\n color: this.badLoginAttempt ? 'error' : 'success',\n text: this.badLoginAttempt ? 'Try again' : 'Log In',\n };\n },\n passwordError(): string {\n return validatePassword(this.password);\n },\n passwordRetypeError(): string {\n console.log('[RTE]');\n if (this.password !== this.retypedPassword) {\n return 'Passwords must match.';\n } else {\n return '';\n }\n },\n },\n\n async created() {\n this.user = await getCurrentUser();\n },\n\n methods: {\n toggle() {\n log('Toggling registration / login forms.');\n this.$emit('toggle');\n },\n\n validateEmail() {\n this.emailError = false;\n // Basic email validation\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (this.email && !emailRegex.test(this.email)) {\n this.emailError = true;\n this.emailHint = 'Please enter a valid email address';\n } else {\n this.emailHint = '';\n }\n },\n\n validateUsername() {\n this.usernameError = false;\n },\n\n async createUser() {\n this.awaitingResponse = true;\n\n // Validate password before proceeding\n if (this.passwordError) {\n alertUser({\n text: this.passwordError,\n status: Status.error,\n });\n this.awaitingResponse = false;\n return;\n }\n\n log(`\nUser creation\n-------------\n\nName: ${this.username}\nStudent: ${this.student}\nTeacher: ${this.teacher}\nAuthor: ${this.author}\n`);\n if (this.password === this.retypedPassword) {\n if (!this.user) {\n console.error('ERROR: No user object available');\n return;\n }\n\n this.user\n .createAccount(this.username, this.password)\n .then(async (resp: any) => {\n if (resp.status === Status.ok) {\n // Account created successfully via PouchDB\n this.authStore.loginAndRegistration.loggedIn = true;\n this.authStore.loginAndRegistration.init = false;\n this.authStore.loginAndRegistration.init = true;\n\n // Save email to user config if provided\n if (this.email) {\n try {\n const currentUser = await getCurrentUser();\n await currentUser.setConfig({ email: this.email });\n\n // Trigger verification email send with current origin\n const origin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const verificationResult = await sendVerificationEmail(this.username, this.email, origin);\n if (verificationResult.ok) {\n alertUser({\n text: 'Account created! Please check your email to verify your account.',\n status: Status.ok,\n });\n } else {\n log(`Warning: Failed to send verification email: ${verificationResult.error}`);\n // Continue anyway - user can still use the account\n }\n } catch (emailError) {\n console.error('Email save/send error:', emailError);\n log(`Warning: Failed to save email or send verification: ${emailError}`);\n // Continue anyway - account was created successfully\n }\n }\n\n // Emit signup success event for parent to handle\n // Use the username we just created rather than calling getCurrentUser() again\n // which can fail if the auth state hasn't fully synchronized\n\n // Call callback prop if provided (preferred method)\n if (this.onSignupSuccess) {\n console.log('[UserRegistration] Calling onSignupSuccess callback');\n this.onSignupSuccess({ username: this.username });\n }\n\n // Also emit for backward compatibility\n this.$emit('signup-success', { username: this.username });\n } else {\n if (resp.error === 'This username is taken!') {\n this.usernameError = true;\n this.usernameHint = 'Try a different name.';\n (this.$refs.userNameTextField as HTMLInputElement).focus();\n alertUser({\n text: `The name ${this.username} is taken!`,\n status: resp.status,\n });\n } else {\n alertUser({\n text: resp.error,\n status: resp.status,\n });\n }\n }\n })\n .catch((e) => {\n console.error('[UserRegistration] .catch() called with error:', e);\n if (e) {\n const errorText = e?.message || e?.error || e?.toString() || 'Account creation failed';\n alertUser({\n text: errorText,\n status: Status.error,\n });\n }\n });\n this.awaitingResponse = false;\n } else {\n alertUser({\n text: 'Passwords do not match.',\n status: Status.error,\n });\n this.awaitingResponse = false;\n }\n },\n },\n});\n</script>\n","<template>\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\"> Create an Account </v-card-title>\n\n <v-card-text>\n <v-form @submit.prevent=\"createUser\">\n <v-text-field\n v-model=\"email\"\n name=\"email\"\n label=\"Email address\"\n type=\"email\"\n prepend-icon=\"mdi-email\"\n :error=\"emailError\"\n :hint=\"emailHint\"\n @blur=\"validateEmail\"\n ></v-text-field>\n <v-text-field\n id=\"\"\n ref=\"userNameTextField\"\n v-model=\"username\"\n name=\"username\"\n label=\"Choose a Username\"\n prepend-icon=\"mdi-account-circle\"\n :error=\"usernameError\"\n :hint=\"usernameHint\"\n @blur=\"validateUsername\"\n ></v-text-field>\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n hover=\"Show password\"\n label=\"Create a password\"\n :hint=\"passwordError\"\n :error=\"!!passwordError\"\n min=\"4\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n <v-text-field\n v-model=\"retypedPassword\"\n prepend-icon=\"mdi-lock\"\n name=\"retypedPassword\"\n hover=\"Show password\"\n label=\"Retype your password\"\n :disabled=\"password === '' || !!passwordError\"\n :hint=\"passwordRetypeError\"\n min=\"4\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n ></v-text-field>\n\n <!-- <v-checkbox label=\"Student\" v-model=\"student\" ></v-checkbox>\n <v-checkbox label=\"Teacher\" v-model=\"teacher\" ></v-checkbox>\n <v-checkbox label=\"Author\" v-model=\"author\" ></v-checkbox> -->\n\n <v-snackbar v-model=\"badLoginAttempt\" location=\"bottom right\" :timeout=\"5000\">\n Username or password was incorrect.\n <v-btn color=\"pink\" variant=\"text\" @click=\"badLoginAttempt = false\"> Close </v-btn>\n </v-snackbar>\n <v-btn\n class=\"mr-2 my-2\"\n type=\"submit\"\n :loading=\"awaitingResponse\"\n :color=\"buttonStatus.color\"\n :disabled=\"!!passwordError || password !== retypedPassword\"\n >\n <v-icon start>mdi-lock-open</v-icon>\n Create Account\n </v-btn>\n <router-link v-if=\"registrationRoute\" to=\"login\">\n <v-btn variant=\"text\">Log In</v-btn>\n </router-link>\n <v-btn v-else variant=\"text\" @click=\"toggle\"> Log In </v-btn>\n </v-form>\n </v-card-text>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { UserDBInterface } from '@vue-skuilder/db';\nimport { alertUser } from '../SnackbarService';\nimport { Status, log } from '@vue-skuilder/common';\nimport { getCurrentUser, useAuthStore } from '../../stores/useAuthStore';\nimport { sendVerificationEmail } from '../../services/authAPI';\nimport { validatePassword } from '../../utils/passwordValidation';\n\nexport default defineComponent({\n name: 'UserRegistration',\n\n props: {\n onSignupSuccess: {\n type: Function as PropType<(payload: { username: string }) => void>,\n required: false,\n },\n },\n\n emits: ['toggle', 'signup-success'],\n\n data() {\n return {\n email: '',\n username: '',\n password: '',\n retypedPassword: '',\n passwordVisible: false,\n emailError: false,\n emailHint: '',\n usernameValidationInProgress: false,\n usernameError: false,\n usernameHint: '',\n awaitingResponse: false,\n badLoginAttempt: false,\n userSecret: '',\n secret: 'goons',\n user: null as UserDBInterface | null,\n roles: ['Student', 'Teacher', 'Author'] as string[],\n student: true,\n teacher: false,\n author: false,\n authStore: useAuthStore(),\n };\n },\n\n computed: {\n registrationRoute(): boolean {\n return typeof this.$route.name === 'string' && this.$route.name.toLowerCase() === 'signup';\n },\n buttonStatus() {\n return {\n color: this.badLoginAttempt ? 'error' : 'success',\n text: this.badLoginAttempt ? 'Try again' : 'Log In',\n };\n },\n passwordError(): string {\n return validatePassword(this.password);\n },\n passwordRetypeError(): string {\n console.log('[RTE]');\n if (this.password !== this.retypedPassword) {\n return 'Passwords must match.';\n } else {\n return '';\n }\n },\n },\n\n async created() {\n this.user = await getCurrentUser();\n },\n\n methods: {\n toggle() {\n log('Toggling registration / login forms.');\n this.$emit('toggle');\n },\n\n validateEmail() {\n this.emailError = false;\n // Basic email validation\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (this.email && !emailRegex.test(this.email)) {\n this.emailError = true;\n this.emailHint = 'Please enter a valid email address';\n } else {\n this.emailHint = '';\n }\n },\n\n validateUsername() {\n this.usernameError = false;\n },\n\n async createUser() {\n this.awaitingResponse = true;\n\n // Validate password before proceeding\n if (this.passwordError) {\n alertUser({\n text: this.passwordError,\n status: Status.error,\n });\n this.awaitingResponse = false;\n return;\n }\n\n log(`\nUser creation\n-------------\n\nName: ${this.username}\nStudent: ${this.student}\nTeacher: ${this.teacher}\nAuthor: ${this.author}\n`);\n if (this.password === this.retypedPassword) {\n if (!this.user) {\n console.error('ERROR: No user object available');\n return;\n }\n\n this.user\n .createAccount(this.username, this.password)\n .then(async (resp: any) => {\n if (resp.status === Status.ok) {\n // Account created successfully via PouchDB\n this.authStore.loginAndRegistration.loggedIn = true;\n this.authStore.loginAndRegistration.init = false;\n this.authStore.loginAndRegistration.init = true;\n\n // Save email to user config if provided\n if (this.email) {\n try {\n const currentUser = await getCurrentUser();\n await currentUser.setConfig({ email: this.email });\n\n // Trigger verification email send with current origin\n const origin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const verificationResult = await sendVerificationEmail(this.username, this.email, origin);\n if (verificationResult.ok) {\n alertUser({\n text: 'Account created! Please check your email to verify your account.',\n status: Status.ok,\n });\n } else {\n log(`Warning: Failed to send verification email: ${verificationResult.error}`);\n // Continue anyway - user can still use the account\n }\n } catch (emailError) {\n console.error('Email save/send error:', emailError);\n log(`Warning: Failed to save email or send verification: ${emailError}`);\n // Continue anyway - account was created successfully\n }\n }\n\n // Emit signup success event for parent to handle\n // Use the username we just created rather than calling getCurrentUser() again\n // which can fail if the auth state hasn't fully synchronized\n\n // Call callback prop if provided (preferred method)\n if (this.onSignupSuccess) {\n console.log('[UserRegistration] Calling onSignupSuccess callback');\n this.onSignupSuccess({ username: this.username });\n }\n\n // Also emit for backward compatibility\n this.$emit('signup-success', { username: this.username });\n } else {\n if (resp.error === 'This username is taken!') {\n this.usernameError = true;\n this.usernameHint = 'Try a different name.';\n (this.$refs.userNameTextField as HTMLInputElement).focus();\n alertUser({\n text: `The name ${this.username} is taken!`,\n status: resp.status,\n });\n } else {\n alertUser({\n text: resp.error,\n status: resp.status,\n });\n }\n }\n })\n .catch((e) => {\n console.error('[UserRegistration] .catch() called with error:', e);\n if (e) {\n const errorText = e?.message || e?.error || e?.toString() || 'Account creation failed';\n alertUser({\n text: errorText,\n status: Status.error,\n });\n }\n });\n this.awaitingResponse = false;\n } else {\n alertUser({\n text: 'Passwords do not match.',\n status: Status.error,\n });\n this.awaitingResponse = false;\n }\n },\n },\n});\n</script>\n","<template>\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Reset Password\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <v-form v-if=\"!requestSent\" @submit.prevent=\"handleSubmit\">\n <p class=\"mb-4\">\n Enter your email address and we'll send you a link to reset your password.\n </p>\n\n <v-text-field\n v-model=\"email\"\n name=\"email\"\n label=\"Email address\"\n type=\"email\"\n prepend-icon=\"mdi-email\"\n :error=\"emailError\"\n :hint=\"emailHint\"\n :disabled=\"isSubmitting\"\n @blur=\"validateEmail\"\n ></v-text-field>\n\n <v-btn\n type=\"submit\"\n color=\"primary\"\n class=\"mt-4\"\n :loading=\"isSubmitting\"\n :disabled=\"!email || emailError\"\n block\n >\n <v-icon start>mdi-email-send</v-icon>\n Send Reset Link\n </v-btn>\n </v-form>\n\n <!-- Success message -->\n <div v-else class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-email-check</v-icon>\n <h3 class=\"mb-2\">Check Your Email</h3>\n <p>\n If an account exists with <strong>{{ email }}</strong>, you will receive a password\n reset link shortly.\n </p>\n <p class=\"text-caption mt-4\">Didn't receive an email? Check your spam folder.</p>\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"!requestSent\">\n <slot name=\"back-action\">\n <!-- External apps can provide their own back button -->\n <v-btn variant=\"text\" @click=\"$emit('cancel')\">\n Cancel\n </v-btn>\n </slot>\n <v-spacer></v-spacer>\n </v-card-actions>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { requestPasswordReset } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'RequestPasswordReset',\n\n emits: ['cancel', 'success'],\n\n data() {\n return {\n email: '',\n emailError: false,\n emailHint: '',\n isSubmitting: false,\n requestSent: false,\n };\n },\n\n methods: {\n validateEmail() {\n this.emailError = false;\n this.emailHint = '';\n\n if (!this.email) return;\n\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(this.email)) {\n this.emailError = true;\n this.emailHint = 'Please enter a valid email address';\n }\n },\n\n async handleSubmit() {\n this.validateEmail();\n\n if (this.emailError || !this.email) {\n return;\n }\n\n this.isSubmitting = true;\n\n try {\n // Pass current origin for correct reset link in email\n const origin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const result = await requestPasswordReset(this.email, origin);\n\n if (result.ok) {\n this.requestSent = true;\n this.$emit('success', this.email);\n } else {\n alertUser({\n text: result.error || 'Failed to send reset email',\n status: Status.error,\n });\n }\n } catch (error) {\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n } finally {\n this.isSubmitting = false;\n }\n },\n },\n});\n</script>\n","<template>\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Reset Password\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <v-form v-if=\"!requestSent\" @submit.prevent=\"handleSubmit\">\n <p class=\"mb-4\">\n Enter your email address and we'll send you a link to reset your password.\n </p>\n\n <v-text-field\n v-model=\"email\"\n name=\"email\"\n label=\"Email address\"\n type=\"email\"\n prepend-icon=\"mdi-email\"\n :error=\"emailError\"\n :hint=\"emailHint\"\n :disabled=\"isSubmitting\"\n @blur=\"validateEmail\"\n ></v-text-field>\n\n <v-btn\n type=\"submit\"\n color=\"primary\"\n class=\"mt-4\"\n :loading=\"isSubmitting\"\n :disabled=\"!email || emailError\"\n block\n >\n <v-icon start>mdi-email-send</v-icon>\n Send Reset Link\n </v-btn>\n </v-form>\n\n <!-- Success message -->\n <div v-else class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-email-check</v-icon>\n <h3 class=\"mb-2\">Check Your Email</h3>\n <p>\n If an account exists with <strong>{{ email }}</strong>, you will receive a password\n reset link shortly.\n </p>\n <p class=\"text-caption mt-4\">Didn't receive an email? Check your spam folder.</p>\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"!requestSent\">\n <slot name=\"back-action\">\n <!-- External apps can provide their own back button -->\n <v-btn variant=\"text\" @click=\"$emit('cancel')\">\n Cancel\n </v-btn>\n </slot>\n <v-spacer></v-spacer>\n </v-card-actions>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { requestPasswordReset } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'RequestPasswordReset',\n\n emits: ['cancel', 'success'],\n\n data() {\n return {\n email: '',\n emailError: false,\n emailHint: '',\n isSubmitting: false,\n requestSent: false,\n };\n },\n\n methods: {\n validateEmail() {\n this.emailError = false;\n this.emailHint = '';\n\n if (!this.email) return;\n\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(this.email)) {\n this.emailError = true;\n this.emailHint = 'Please enter a valid email address';\n }\n },\n\n async handleSubmit() {\n this.validateEmail();\n\n if (this.emailError || !this.email) {\n return;\n }\n\n this.isSubmitting = true;\n\n try {\n // Pass current origin for correct reset link in email\n const origin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const result = await requestPasswordReset(this.email, origin);\n\n if (result.ok) {\n this.requestSent = true;\n this.$emit('success', this.email);\n } else {\n alertUser({\n text: result.error || 'Failed to send reset email',\n status: Status.error,\n });\n }\n } catch (error) {\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n } finally {\n this.isSubmitting = false;\n }\n },\n },\n});\n</script>\n","<template>\n <div v-if=\"userReady && display\">\n <transition name=\"component-fade\" mode=\"out-in\">\n <div v-if=\"guestMode\" key=\"login-buttons\">\n <!-- Registration button: only if showLoginRegistration is true -->\n <v-dialog v-model=\"regDialog\" width=\"500px\" v-if=\"authUIConfig.showLoginRegistration\">\n <template #activator=\"{ props }\">\n <v-btn class=\"mr-2\" size=\"small\" color=\"success\" v-bind=\"props\">Sign Up</v-btn>\n </template>\n <UserRegistration\n @toggle=\"toggle\"\n @signup-success=\"handleSignupSuccess\"\n :on-signup-success=\"handleSignupSuccess\"\n />\n </v-dialog>\n \n <!-- Login button: always show in guest mode -->\n <v-dialog v-model=\"loginDialog\" width=\"500px\">\n <template #activator=\"{ props }\">\n <v-btn size=\"small\" color=\"success\" v-bind=\"props\">Log In</v-btn>\n </template>\n <UserLogin @toggle=\"toggle\" @forgot-password=\"openResetDialog\" />\n </v-dialog>\n\n <!-- Password reset dialog: opened from login via \"Forgot password?\" -->\n <v-dialog v-model=\"resetDialog\" width=\"500px\">\n <RequestPasswordReset @cancel=\"closeResetDialog\" @success=\"closeResetDialog\" />\n </v-dialog>\n </div>\n <div v-else key=\"user-chip\">\n <user-chip />\n </div>\n </transition>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, onMounted, ref } from 'vue';\nimport { useRoute } from 'vue-router';\nimport UserLogin from './UserLogin.vue';\nimport UserRegistration from './UserRegistration.vue';\nimport RequestPasswordReset from './RequestPasswordReset.vue';\nimport UserChip from './UserChip.vue';\nimport { useAuthStore } from '../../stores/useAuthStore';\nimport { useAuthUI } from '../../composables/useAuthUI';\nimport { GuestUsername } from '@vue-skuilder/db';\n\n// Define props\nconst props = defineProps<{\n showLoginButton?: boolean;\n redirectToPath?: string;\n showRegistration?: boolean;\n onSignupSuccess?: (payload: { username: string }) => void;\n}>();\n\n// Define emits (keeping for backward compatibility if needed)\nconst emit = defineEmits<{\n 'signup-success': [payload: { username: string }]\n}>();\n\nconst route = useRoute();\nconst authStore = useAuthStore();\nconst authUI = useAuthUI();\n\n// Initialize auth UI detection\nonMounted(async () => {\n await authUI.detectSyncStrategy();\n});\n\nconst display = computed(() => {\n if (!route.name || typeof route.name !== 'string') {\n return true;\n }\n const routeName = route.name.toLowerCase();\n return !(routeName === 'login' || routeName === 'signup');\n});\n\nconst userReady = computed(() => authStore.onLoadComplete);\n\nconst guestMode = computed(() => {\n if (authStore._user) {\n return authStore._user.getUsername().startsWith(GuestUsername);\n }\n return !authStore.loginAndRegistration.loggedIn;\n});\n\nconst authUIConfig = computed(() => {\n const baseConfig = authUI.config.value || {\n showLoginRegistration: true,\n showLogout: true, \n showResetData: false,\n logoutLabel: 'Log out',\n resetLabel: '',\n };\n \n // Apply prop overrides\n return {\n ...baseConfig,\n ...(props.showRegistration !== undefined && { showLoginRegistration: props.showRegistration })\n };\n});\n\nconst regDialog = computed({\n get: () => authStore.loginAndRegistration.regDialogOpen,\n set: (value: boolean) => {\n authStore.loginAndRegistration.regDialogOpen = value;\n },\n});\n\nconst loginDialog = computed({\n get: () => authStore.loginAndRegistration.loginDialogOpen,\n set: (value: boolean) => {\n authStore.loginAndRegistration.loginDialogOpen = value;\n },\n});\n\n// Password reset dialog state (local, not in auth store)\nconst resetDialog = ref(false);\n\nconst toggle = () => {\n if (regDialog.value && loginDialog.value) {\n throw new Error('Registration / Login dialogs both activated.');\n } else if (regDialog.value === loginDialog.value) {\n throw new Error('Registration / Login dialogs toggled while both were dormant.');\n } else {\n regDialog.value = !regDialog.value;\n loginDialog.value = !loginDialog.value;\n }\n};\n\nconst openResetDialog = () => {\n loginDialog.value = false; // Close login dialog\n resetDialog.value = true; // Open reset dialog\n};\n\nconst closeResetDialog = () => {\n resetDialog.value = false;\n};\n\nconst handleSignupSuccess = (payload: { username: string }) => {\n // Close the registration dialog\n regDialog.value = false;\n\n // Call the callback prop if provided (preferred method)\n if (props.onSignupSuccess) {\n props.onSignupSuccess(payload);\n }\n\n // Also emit event for backward compatibility\n emit('signup-success', payload);\n};\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n</style>\n","<template>\n <div v-if=\"userReady && display\">\n <transition name=\"component-fade\" mode=\"out-in\">\n <div v-if=\"guestMode\" key=\"login-buttons\">\n <!-- Registration button: only if showLoginRegistration is true -->\n <v-dialog v-model=\"regDialog\" width=\"500px\" v-if=\"authUIConfig.showLoginRegistration\">\n <template #activator=\"{ props }\">\n <v-btn class=\"mr-2\" size=\"small\" color=\"success\" v-bind=\"props\">Sign Up</v-btn>\n </template>\n <UserRegistration\n @toggle=\"toggle\"\n @signup-success=\"handleSignupSuccess\"\n :on-signup-success=\"handleSignupSuccess\"\n />\n </v-dialog>\n \n <!-- Login button: always show in guest mode -->\n <v-dialog v-model=\"loginDialog\" width=\"500px\">\n <template #activator=\"{ props }\">\n <v-btn size=\"small\" color=\"success\" v-bind=\"props\">Log In</v-btn>\n </template>\n <UserLogin @toggle=\"toggle\" @forgot-password=\"openResetDialog\" />\n </v-dialog>\n\n <!-- Password reset dialog: opened from login via \"Forgot password?\" -->\n <v-dialog v-model=\"resetDialog\" width=\"500px\">\n <RequestPasswordReset @cancel=\"closeResetDialog\" @success=\"closeResetDialog\" />\n </v-dialog>\n </div>\n <div v-else key=\"user-chip\">\n <user-chip />\n </div>\n </transition>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, onMounted, ref } from 'vue';\nimport { useRoute } from 'vue-router';\nimport UserLogin from './UserLogin.vue';\nimport UserRegistration from './UserRegistration.vue';\nimport RequestPasswordReset from './RequestPasswordReset.vue';\nimport UserChip from './UserChip.vue';\nimport { useAuthStore } from '../../stores/useAuthStore';\nimport { useAuthUI } from '../../composables/useAuthUI';\nimport { GuestUsername } from '@vue-skuilder/db';\n\n// Define props\nconst props = defineProps<{\n showLoginButton?: boolean;\n redirectToPath?: string;\n showRegistration?: boolean;\n onSignupSuccess?: (payload: { username: string }) => void;\n}>();\n\n// Define emits (keeping for backward compatibility if needed)\nconst emit = defineEmits<{\n 'signup-success': [payload: { username: string }]\n}>();\n\nconst route = useRoute();\nconst authStore = useAuthStore();\nconst authUI = useAuthUI();\n\n// Initialize auth UI detection\nonMounted(async () => {\n await authUI.detectSyncStrategy();\n});\n\nconst display = computed(() => {\n if (!route.name || typeof route.name !== 'string') {\n return true;\n }\n const routeName = route.name.toLowerCase();\n return !(routeName === 'login' || routeName === 'signup');\n});\n\nconst userReady = computed(() => authStore.onLoadComplete);\n\nconst guestMode = computed(() => {\n if (authStore._user) {\n return authStore._user.getUsername().startsWith(GuestUsername);\n }\n return !authStore.loginAndRegistration.loggedIn;\n});\n\nconst authUIConfig = computed(() => {\n const baseConfig = authUI.config.value || {\n showLoginRegistration: true,\n showLogout: true, \n showResetData: false,\n logoutLabel: 'Log out',\n resetLabel: '',\n };\n \n // Apply prop overrides\n return {\n ...baseConfig,\n ...(props.showRegistration !== undefined && { showLoginRegistration: props.showRegistration })\n };\n});\n\nconst regDialog = computed({\n get: () => authStore.loginAndRegistration.regDialogOpen,\n set: (value: boolean) => {\n authStore.loginAndRegistration.regDialogOpen = value;\n },\n});\n\nconst loginDialog = computed({\n get: () => authStore.loginAndRegistration.loginDialogOpen,\n set: (value: boolean) => {\n authStore.loginAndRegistration.loginDialogOpen = value;\n },\n});\n\n// Password reset dialog state (local, not in auth store)\nconst resetDialog = ref(false);\n\nconst toggle = () => {\n if (regDialog.value && loginDialog.value) {\n throw new Error('Registration / Login dialogs both activated.');\n } else if (regDialog.value === loginDialog.value) {\n throw new Error('Registration / Login dialogs toggled while both were dormant.');\n } else {\n regDialog.value = !regDialog.value;\n loginDialog.value = !loginDialog.value;\n }\n};\n\nconst openResetDialog = () => {\n loginDialog.value = false; // Close login dialog\n resetDialog.value = true; // Open reset dialog\n};\n\nconst closeResetDialog = () => {\n resetDialog.value = false;\n};\n\nconst handleSignupSuccess = (payload: { username: string }) => {\n // Close the registration dialog\n regDialog.value = false;\n\n // Call the callback prop if provided (preferred method)\n if (props.onSignupSuccess) {\n props.onSignupSuccess(payload);\n }\n\n // Also emit event for backward compatibility\n emit('signup-success', payload);\n};\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n</style>\n","<template>\n <v-container class=\"fill-height\" fluid>\n <v-row align=\"center\" justify=\"center\">\n <v-col cols=\"12\" sm=\"8\" md=\"6\">\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Email Verification\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <!-- Loading state -->\n <div v-if=\"isVerifying\" class=\"text-center\">\n <v-progress-circular\n indeterminate\n color=\"primary\"\n size=\"64\"\n class=\"mb-4\"\n ></v-progress-circular>\n <p>Verifying your email...</p>\n </div>\n\n <!-- Success state -->\n <div v-else-if=\"verificationStatus === 'success'\" class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-check-circle</v-icon>\n <h3 class=\"mb-2\">Email Verified!</h3>\n <p>Your account has been successfully verified.</p>\n <p v-if=\"username\">Welcome, {{ username }}!</p>\n </div>\n\n <!-- Error state -->\n <div v-else-if=\"verificationStatus === 'error'\" class=\"text-center\">\n <v-icon color=\"error\" size=\"64\" class=\"mb-4\">mdi-alert-circle</v-icon>\n <h3 class=\"mb-2\">Verification Failed</h3>\n <p>{{ errorMessage }}</p>\n </div>\n\n <!-- No token state -->\n <div v-else class=\"text-center\">\n <v-icon color=\"warning\" size=\"64\" class=\"mb-4\">mdi-help-circle</v-icon>\n <h3 class=\"mb-2\">No Verification Token</h3>\n <p>Please use the link from your verification email.</p>\n </div>\n </v-card-text>\n\n <v-card-actions>\n <v-spacer></v-spacer>\n <slot name=\"actions\" :status=\"verificationStatus\" :username=\"username\">\n <!-- Default action - external apps can override with their own navigation -->\n <v-btn\n v-if=\"verificationStatus === 'success'\"\n color=\"primary\"\n @click=\"$emit('verified', username)\"\n >\n Continue\n </v-btn>\n <v-btn\n v-else-if=\"verificationStatus === 'error'\"\n variant=\"text\"\n @click=\"$emit('error', errorMessage)\"\n >\n Close\n </v-btn>\n </slot>\n </v-card-actions>\n </v-card>\n </v-col>\n </v-row>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { verifyEmail } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'VerifyEmail',\n\n emits: ['verified', 'error'],\n\n props: {\n /**\n * Verification token. If not provided, will try to read from URL query params.\n */\n token: {\n type: String,\n default: null,\n },\n },\n\n data() {\n return {\n isVerifying: false,\n verificationStatus: null as 'success' | 'error' | null,\n errorMessage: '',\n username: '',\n };\n },\n\n async mounted() {\n await this.performVerification();\n },\n\n methods: {\n async performVerification() {\n // Get token from prop or URL query params\n const token = this.token || this.getTokenFromURL();\n\n if (!token) {\n this.verificationStatus = 'error';\n this.errorMessage = 'No verification token provided';\n return;\n }\n\n this.isVerifying = true;\n\n try {\n const result = await verifyEmail(token);\n\n if (result.ok) {\n this.verificationStatus = 'success';\n this.username = result.username || '';\n alertUser({\n text: 'Email verified successfully!',\n status: Status.ok,\n });\n } else {\n this.verificationStatus = 'error';\n this.errorMessage = result.error || 'Verification failed';\n alertUser({\n text: result.error || 'Verification failed',\n status: Status.error,\n });\n }\n } catch (error) {\n this.verificationStatus = 'error';\n this.errorMessage = 'An unexpected error occurred';\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n } finally {\n this.isVerifying = false;\n }\n },\n\n getTokenFromURL(): string | null {\n if (typeof window === 'undefined') return null;\n\n const params = new URLSearchParams(window.location.search);\n return params.get('token');\n },\n },\n});\n</script>\n","<template>\n <v-container class=\"fill-height\" fluid>\n <v-row align=\"center\" justify=\"center\">\n <v-col cols=\"12\" sm=\"8\" md=\"6\">\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Email Verification\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <!-- Loading state -->\n <div v-if=\"isVerifying\" class=\"text-center\">\n <v-progress-circular\n indeterminate\n color=\"primary\"\n size=\"64\"\n class=\"mb-4\"\n ></v-progress-circular>\n <p>Verifying your email...</p>\n </div>\n\n <!-- Success state -->\n <div v-else-if=\"verificationStatus === 'success'\" class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-check-circle</v-icon>\n <h3 class=\"mb-2\">Email Verified!</h3>\n <p>Your account has been successfully verified.</p>\n <p v-if=\"username\">Welcome, {{ username }}!</p>\n </div>\n\n <!-- Error state -->\n <div v-else-if=\"verificationStatus === 'error'\" class=\"text-center\">\n <v-icon color=\"error\" size=\"64\" class=\"mb-4\">mdi-alert-circle</v-icon>\n <h3 class=\"mb-2\">Verification Failed</h3>\n <p>{{ errorMessage }}</p>\n </div>\n\n <!-- No token state -->\n <div v-else class=\"text-center\">\n <v-icon color=\"warning\" size=\"64\" class=\"mb-4\">mdi-help-circle</v-icon>\n <h3 class=\"mb-2\">No Verification Token</h3>\n <p>Please use the link from your verification email.</p>\n </div>\n </v-card-text>\n\n <v-card-actions>\n <v-spacer></v-spacer>\n <slot name=\"actions\" :status=\"verificationStatus\" :username=\"username\">\n <!-- Default action - external apps can override with their own navigation -->\n <v-btn\n v-if=\"verificationStatus === 'success'\"\n color=\"primary\"\n @click=\"$emit('verified', username)\"\n >\n Continue\n </v-btn>\n <v-btn\n v-else-if=\"verificationStatus === 'error'\"\n variant=\"text\"\n @click=\"$emit('error', errorMessage)\"\n >\n Close\n </v-btn>\n </slot>\n </v-card-actions>\n </v-card>\n </v-col>\n </v-row>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { verifyEmail } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'VerifyEmail',\n\n emits: ['verified', 'error'],\n\n props: {\n /**\n * Verification token. If not provided, will try to read from URL query params.\n */\n token: {\n type: String,\n default: null,\n },\n },\n\n data() {\n return {\n isVerifying: false,\n verificationStatus: null as 'success' | 'error' | null,\n errorMessage: '',\n username: '',\n };\n },\n\n async mounted() {\n await this.performVerification();\n },\n\n methods: {\n async performVerification() {\n // Get token from prop or URL query params\n const token = this.token || this.getTokenFromURL();\n\n if (!token) {\n this.verificationStatus = 'error';\n this.errorMessage = 'No verification token provided';\n return;\n }\n\n this.isVerifying = true;\n\n try {\n const result = await verifyEmail(token);\n\n if (result.ok) {\n this.verificationStatus = 'success';\n this.username = result.username || '';\n alertUser({\n text: 'Email verified successfully!',\n status: Status.ok,\n });\n } else {\n this.verificationStatus = 'error';\n this.errorMessage = result.error || 'Verification failed';\n alertUser({\n text: result.error || 'Verification failed',\n status: Status.error,\n });\n }\n } catch (error) {\n this.verificationStatus = 'error';\n this.errorMessage = 'An unexpected error occurred';\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n } finally {\n this.isVerifying = false;\n }\n },\n\n getTokenFromURL(): string | null {\n if (typeof window === 'undefined') return null;\n\n const params = new URLSearchParams(window.location.search);\n return params.get('token');\n },\n },\n});\n</script>\n","<template>\n <v-container class=\"fill-height\" fluid>\n <v-row align=\"center\" justify=\"center\">\n <v-col cols=\"12\" sm=\"8\" md=\"6\">\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Set New Password\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <v-form v-if=\"!isComplete\" @submit.prevent=\"handleSubmit\">\n <p class=\"mb-4\">Enter your new password below.</p>\n\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n label=\"New password\"\n min=\"4\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n :disabled=\"isSubmitting\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n\n <v-text-field\n v-model=\"confirmPassword\"\n prepend-icon=\"mdi-lock\"\n name=\"confirmPassword\"\n label=\"Confirm new password\"\n min=\"4\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n :error=\"confirmPasswordError\"\n :hint=\"confirmPasswordHint\"\n :disabled=\"isSubmitting\"\n @blur=\"validateConfirmPassword\"\n ></v-text-field>\n\n <v-btn\n type=\"submit\"\n color=\"primary\"\n class=\"mt-4\"\n :loading=\"isSubmitting\"\n :disabled=\"!canSubmit\"\n block\n >\n <v-icon start>mdi-lock-reset</v-icon>\n Reset Password\n </v-btn>\n </v-form>\n\n <!-- Success state -->\n <div v-else class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-check-circle</v-icon>\n <h3 class=\"mb-2\">Password Reset Successful!</h3>\n <p>Your password has been updated. You can now log in with your new password.</p>\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"isComplete\">\n <v-spacer></v-spacer>\n <slot name=\"success-action\">\n <!-- External apps can provide their own navigation -->\n <v-btn color=\"primary\" @click=\"$emit('complete')\">\n Continue to Login\n </v-btn>\n </slot>\n </v-card-actions>\n </v-card>\n </v-col>\n </v-row>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { resetPassword } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'ResetPassword',\n\n emits: ['complete', 'error'],\n\n props: {\n /**\n * Reset token. If not provided, will try to read from URL query params.\n */\n token: {\n type: String,\n default: null,\n },\n },\n\n data() {\n return {\n password: '',\n confirmPassword: '',\n passwordVisible: false,\n confirmPasswordError: false,\n confirmPasswordHint: '',\n isSubmitting: false,\n isComplete: false,\n };\n },\n\n computed: {\n canSubmit(): boolean {\n return (\n this.password.length >= 4 &&\n this.confirmPassword.length >= 4 &&\n this.password === this.confirmPassword &&\n !this.confirmPasswordError\n );\n },\n },\n\n methods: {\n validateConfirmPassword() {\n this.confirmPasswordError = false;\n this.confirmPasswordHint = '';\n\n if (!this.confirmPassword) return;\n\n if (this.password !== this.confirmPassword) {\n this.confirmPasswordError = true;\n this.confirmPasswordHint = 'Passwords do not match';\n }\n },\n\n async handleSubmit() {\n this.validateConfirmPassword();\n\n if (!this.canSubmit) {\n return;\n }\n\n // Get token from prop or URL query params\n const token = this.token || this.getTokenFromURL();\n\n if (!token) {\n alertUser({\n text: 'No reset token provided. Please use the link from your email.',\n status: Status.error,\n });\n this.$emit('error', 'No token');\n return;\n }\n\n this.isSubmitting = true;\n\n try {\n const result = await resetPassword(token, this.password);\n\n if (result.ok) {\n this.isComplete = true;\n alertUser({\n text: 'Password reset successfully!',\n status: Status.ok,\n });\n } else {\n alertUser({\n text: result.error || 'Failed to reset password',\n status: Status.error,\n });\n this.$emit('error', result.error);\n }\n } catch (error) {\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n this.$emit('error', 'Unexpected error');\n } finally {\n this.isSubmitting = false;\n }\n },\n\n getTokenFromURL(): string | null {\n if (typeof window === 'undefined') return null;\n\n const params = new URLSearchParams(window.location.search);\n return params.get('token');\n },\n },\n});\n</script>\n","<template>\n <v-container class=\"fill-height\" fluid>\n <v-row align=\"center\" justify=\"center\">\n <v-col cols=\"12\" sm=\"8\" md=\"6\">\n <v-card>\n <v-card-title class=\"text-h5 bg-grey-lighten-2\">\n Set New Password\n </v-card-title>\n\n <v-card-text class=\"pa-6\">\n <v-form v-if=\"!isComplete\" @submit.prevent=\"handleSubmit\">\n <p class=\"mb-4\">Enter your new password below.</p>\n\n <v-text-field\n v-model=\"password\"\n prepend-icon=\"mdi-lock\"\n name=\"password\"\n label=\"New password\"\n min=\"4\"\n :append-icon=\"passwordVisible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n :disabled=\"isSubmitting\"\n @click:append=\"() => (passwordVisible = !passwordVisible)\"\n ></v-text-field>\n\n <v-text-field\n v-model=\"confirmPassword\"\n prepend-icon=\"mdi-lock\"\n name=\"confirmPassword\"\n label=\"Confirm new password\"\n min=\"4\"\n :type=\"passwordVisible ? 'text' : 'password'\"\n :error=\"confirmPasswordError\"\n :hint=\"confirmPasswordHint\"\n :disabled=\"isSubmitting\"\n @blur=\"validateConfirmPassword\"\n ></v-text-field>\n\n <v-btn\n type=\"submit\"\n color=\"primary\"\n class=\"mt-4\"\n :loading=\"isSubmitting\"\n :disabled=\"!canSubmit\"\n block\n >\n <v-icon start>mdi-lock-reset</v-icon>\n Reset Password\n </v-btn>\n </v-form>\n\n <!-- Success state -->\n <div v-else class=\"text-center\">\n <v-icon color=\"success\" size=\"64\" class=\"mb-4\">mdi-check-circle</v-icon>\n <h3 class=\"mb-2\">Password Reset Successful!</h3>\n <p>Your password has been updated. You can now log in with your new password.</p>\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"isComplete\">\n <v-spacer></v-spacer>\n <slot name=\"success-action\">\n <!-- External apps can provide their own navigation -->\n <v-btn color=\"primary\" @click=\"$emit('complete')\">\n Continue to Login\n </v-btn>\n </slot>\n </v-card-actions>\n </v-card>\n </v-col>\n </v-row>\n </v-container>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { resetPassword } from '../../services/authAPI';\nimport { alertUser } from '../SnackbarService';\nimport { Status } from '@vue-skuilder/common';\n\nexport default defineComponent({\n name: 'ResetPassword',\n\n emits: ['complete', 'error'],\n\n props: {\n /**\n * Reset token. If not provided, will try to read from URL query params.\n */\n token: {\n type: String,\n default: null,\n },\n },\n\n data() {\n return {\n password: '',\n confirmPassword: '',\n passwordVisible: false,\n confirmPasswordError: false,\n confirmPasswordHint: '',\n isSubmitting: false,\n isComplete: false,\n };\n },\n\n computed: {\n canSubmit(): boolean {\n return (\n this.password.length >= 4 &&\n this.confirmPassword.length >= 4 &&\n this.password === this.confirmPassword &&\n !this.confirmPasswordError\n );\n },\n },\n\n methods: {\n validateConfirmPassword() {\n this.confirmPasswordError = false;\n this.confirmPasswordHint = '';\n\n if (!this.confirmPassword) return;\n\n if (this.password !== this.confirmPassword) {\n this.confirmPasswordError = true;\n this.confirmPasswordHint = 'Passwords do not match';\n }\n },\n\n async handleSubmit() {\n this.validateConfirmPassword();\n\n if (!this.canSubmit) {\n return;\n }\n\n // Get token from prop or URL query params\n const token = this.token || this.getTokenFromURL();\n\n if (!token) {\n alertUser({\n text: 'No reset token provided. Please use the link from your email.',\n status: Status.error,\n });\n this.$emit('error', 'No token');\n return;\n }\n\n this.isSubmitting = true;\n\n try {\n const result = await resetPassword(token, this.password);\n\n if (result.ok) {\n this.isComplete = true;\n alertUser({\n text: 'Password reset successfully!',\n status: Status.ok,\n });\n } else {\n alertUser({\n text: result.error || 'Failed to reset password',\n status: Status.error,\n });\n this.$emit('error', result.error);\n }\n } catch (error) {\n alertUser({\n text: 'An unexpected error occurred',\n status: Status.error,\n });\n this.$emit('error', 'Unexpected error');\n } finally {\n this.isSubmitting = false;\n }\n },\n\n getTokenFromURL(): string | null {\n if (typeof window === 'undefined') return null;\n\n const params = new URLSearchParams(window.location.search);\n return params.get('token');\n },\n },\n});\n</script>\n","<template>\n <div data-cy=\"tags-input\">\n <vue-tags-input\n v-model=\"tag\"\n :tags=\"tags\"\n :autocomplete-items=\"autoCompleteSuggestions\"\n :separators=\"separators\"\n :add-on-key=\"separators\"\n @tags-changed=\"tagsChanged\"\n >\n <template #autocomplete-item=\"props\">\n <div class=\"autocomplete-item\" :class=\"{ 'is-active': props.selected }\">\n <span class=\"tag-name\">{{ props.item.text }}</span>\n <span v-if=\"props.item.data && props.item.data.snippet\" class=\"tag-snippet\">\n - {{ props.item.data.snippet }}\n </span>\n </div>\n </template>\n </vue-tags-input>\n\n <v-btn v-if=\"!hideSubmit\" color=\"success\" :loading=\"loading\" @click=\"submit\">Save Changes</v-btn>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface } from '@vue-skuilder/db';\n// @ts-expect-error - suppress TS error for VueTagsInput - no types available\nimport { VueTagsInput } from '@vojtechlanka/vue-tags-input';\n\nexport interface TagObject {\n text: string;\n style?: string;\n classes?: string;\n data?: {\n snippet: string;\n };\n}\n\nexport interface TagsInputInstance {\n tags: Array<{\n text: string;\n style?: string;\n classes?: string;\n data?: {\n snippet: string;\n };\n }>;\n submit: () => Promise<void>;\n updateAvailableCourseTags: () => Promise<void>;\n}\n\nexport default defineComponent({\n name: 'SkTagsInput',\n\n components: {\n VueTagsInput,\n },\n\n props: {\n courseID: {\n type: String,\n required: true,\n default: '',\n },\n cardID: {\n type: String,\n required: false,\n default: '',\n },\n hideSubmit: {\n type: Boolean,\n required: false,\n default: false,\n },\n },\n\n data() {\n return {\n loading: true,\n tag: '',\n tags: [] as TagObject[],\n initialTags: [] as string[],\n availableCourseTags: [] as Tag[],\n separators: [';', ',', ' '] as string[],\n courseDB: null as CourseDBInterface | null,\n };\n },\n\n computed: {\n autoCompleteSuggestions(): TagObject[] {\n return this.availableCourseTags\n .filter((availableTag) => {\n return availableTag.name.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;\n })\n .map((availableTag) => {\n return {\n text: availableTag.name,\n data: {\n snippet: availableTag.snippet,\n },\n };\n });\n },\n },\n\n watch: {\n async cardID() {\n await this.getAppliedTags();\n },\n async courseID() {\n this.courseDB = getDataLayer().getCourseDB(this.courseID);\n await this.updateAvailableCourseTags();\n },\n },\n\n async created() {\n this.courseDB = getDataLayer().getCourseDB(this.courseID);\n\n await this.updateAvailableCourseTags();\n await this.getAppliedTags();\n },\n\n methods: {\n tagsChanged(newTags: TagObject[]) {\n console.log(`[TagsInput] Tags changing: ${JSON.stringify(newTags)}`);\n this.tags = newTags;\n },\n\n async getAppliedTags() {\n this.initialTags = [];\n this.tags = [];\n try {\n const appliedDocsFindResult = await this.courseDB!.getAppliedTags(this.cardID);\n appliedDocsFindResult.rows.forEach((row) => {\n console.log(`[TagsInput] The following tag is applied:\n\\t${JSON.stringify(row)}`);\n this.tags.push({\n text: row!.value.name,\n style: '',\n classes: '',\n });\n });\n this.initialTags = this.tags.map((tag) => tag.text);\n } catch (e) {\n console.error(`Error in init-getAppliedTags: ${JSON.stringify(e)}, ${e}`);\n } finally {\n this.loading = false;\n }\n },\n\n async updateAvailableCourseTags() {\n try {\n this.availableCourseTags = (await this.courseDB!.getCourseTagStubs()).rows.map((row) => {\n // console.log(`[TagsInput] available tag: ${JSON.stringify(row)}`);\n return row.doc! as Tag;\n });\n } catch (e) {\n console.error(`Error in init-availableCourseTags: ${JSON.stringify(e)}`);\n }\n },\n\n async submit() {\n console.log(`[TagsInput] tagsInput is submitting...`);\n this.loading = true;\n\n try {\n await Promise.all(\n this.tags.map(async (currentTag) => {\n if (!this.initialTags.includes(currentTag.text)) {\n try {\n await this.courseDB!.addTagToCard(this.cardID, currentTag.text);\n console.log(`[TagsInput] Successfully added tag: ${currentTag.text}`);\n } catch (error) {\n console.error(`Failed to add tag ${currentTag.text}:`, error);\n }\n }\n })\n );\n } catch (e) {\n console.error(`Exception adding tags: ${JSON.stringify(e)}`);\n }\n\n try {\n await Promise.all(\n this.initialTags.map(async (initialTag) => {\n if (\n this.tags.filter((tag) => {\n return tag.text === initialTag;\n }).length === 0\n ) {\n try {\n await this.courseDB!.removeTagFromCard(this.cardID, initialTag);\n console.log(`[TagsInput] Successfully removed tag: ${initialTag}`);\n } catch (error) {\n console.error(`Failed to remove tag ${initialTag}:`, error);\n }\n }\n })\n );\n } catch (e) {\n console.error(`Exception removing tags: ${JSON.stringify(e)}`);\n }\n this.loading = false;\n },\n },\n});\n</script>\n\n<style scoped>\n.vue-tags-input {\n max-width: 100%;\n}\n\n.autocomplete-item {\n display: flex;\n align-items: center;\n padding: 5px;\n}\n\n.autocomplete-item.is-active {\n background-color: #e8e8e8;\n}\n\n.autocomplete-item.is-active > .tag-name {\n background-color: #5c6bc0;\n color: white;\n}\n\n.tag-name {\n background-color: #e0e0e0;\n color: #333;\n padding: 2px 6px;\n border-radius: 3px;\n font-weight: bold;\n margin-right: 5px;\n}\n\n.tag-snippet {\n color: #666;\n font-size: 0.9em;\n}\n\n.autocomplete-item.is-active .tag-snippet {\n color: #333; /* Ensure snippet text is visible when item is active */\n}\n</style>\n","<template>\n <div data-cy=\"tags-input\">\n <vue-tags-input\n v-model=\"tag\"\n :tags=\"tags\"\n :autocomplete-items=\"autoCompleteSuggestions\"\n :separators=\"separators\"\n :add-on-key=\"separators\"\n @tags-changed=\"tagsChanged\"\n >\n <template #autocomplete-item=\"props\">\n <div class=\"autocomplete-item\" :class=\"{ 'is-active': props.selected }\">\n <span class=\"tag-name\">{{ props.item.text }}</span>\n <span v-if=\"props.item.data && props.item.data.snippet\" class=\"tag-snippet\">\n - {{ props.item.data.snippet }}\n </span>\n </div>\n </template>\n </vue-tags-input>\n\n <v-btn v-if=\"!hideSubmit\" color=\"success\" :loading=\"loading\" @click=\"submit\">Save Changes</v-btn>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface } from '@vue-skuilder/db';\n// @ts-expect-error - suppress TS error for VueTagsInput - no types available\nimport { VueTagsInput } from '@vojtechlanka/vue-tags-input';\n\nexport interface TagObject {\n text: string;\n style?: string;\n classes?: string;\n data?: {\n snippet: string;\n };\n}\n\nexport interface TagsInputInstance {\n tags: Array<{\n text: string;\n style?: string;\n classes?: string;\n data?: {\n snippet: string;\n };\n }>;\n submit: () => Promise<void>;\n updateAvailableCourseTags: () => Promise<void>;\n}\n\nexport default defineComponent({\n name: 'SkTagsInput',\n\n components: {\n VueTagsInput,\n },\n\n props: {\n courseID: {\n type: String,\n required: true,\n default: '',\n },\n cardID: {\n type: String,\n required: false,\n default: '',\n },\n hideSubmit: {\n type: Boolean,\n required: false,\n default: false,\n },\n },\n\n data() {\n return {\n loading: true,\n tag: '',\n tags: [] as TagObject[],\n initialTags: [] as string[],\n availableCourseTags: [] as Tag[],\n separators: [';', ',', ' '] as string[],\n courseDB: null as CourseDBInterface | null,\n };\n },\n\n computed: {\n autoCompleteSuggestions(): TagObject[] {\n return this.availableCourseTags\n .filter((availableTag) => {\n return availableTag.name.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;\n })\n .map((availableTag) => {\n return {\n text: availableTag.name,\n data: {\n snippet: availableTag.snippet,\n },\n };\n });\n },\n },\n\n watch: {\n async cardID() {\n await this.getAppliedTags();\n },\n async courseID() {\n this.courseDB = getDataLayer().getCourseDB(this.courseID);\n await this.updateAvailableCourseTags();\n },\n },\n\n async created() {\n this.courseDB = getDataLayer().getCourseDB(this.courseID);\n\n await this.updateAvailableCourseTags();\n await this.getAppliedTags();\n },\n\n methods: {\n tagsChanged(newTags: TagObject[]) {\n console.log(`[TagsInput] Tags changing: ${JSON.stringify(newTags)}`);\n this.tags = newTags;\n },\n\n async getAppliedTags() {\n this.initialTags = [];\n this.tags = [];\n try {\n const appliedDocsFindResult = await this.courseDB!.getAppliedTags(this.cardID);\n appliedDocsFindResult.rows.forEach((row) => {\n console.log(`[TagsInput] The following tag is applied:\n\\t${JSON.stringify(row)}`);\n this.tags.push({\n text: row!.value.name,\n style: '',\n classes: '',\n });\n });\n this.initialTags = this.tags.map((tag) => tag.text);\n } catch (e) {\n console.error(`Error in init-getAppliedTags: ${JSON.stringify(e)}, ${e}`);\n } finally {\n this.loading = false;\n }\n },\n\n async updateAvailableCourseTags() {\n try {\n this.availableCourseTags = (await this.courseDB!.getCourseTagStubs()).rows.map((row) => {\n // console.log(`[TagsInput] available tag: ${JSON.stringify(row)}`);\n return row.doc! as Tag;\n });\n } catch (e) {\n console.error(`Error in init-availableCourseTags: ${JSON.stringify(e)}`);\n }\n },\n\n async submit() {\n console.log(`[TagsInput] tagsInput is submitting...`);\n this.loading = true;\n\n try {\n await Promise.all(\n this.tags.map(async (currentTag) => {\n if (!this.initialTags.includes(currentTag.text)) {\n try {\n await this.courseDB!.addTagToCard(this.cardID, currentTag.text);\n console.log(`[TagsInput] Successfully added tag: ${currentTag.text}`);\n } catch (error) {\n console.error(`Failed to add tag ${currentTag.text}:`, error);\n }\n }\n })\n );\n } catch (e) {\n console.error(`Exception adding tags: ${JSON.stringify(e)}`);\n }\n\n try {\n await Promise.all(\n this.initialTags.map(async (initialTag) => {\n if (\n this.tags.filter((tag) => {\n return tag.text === initialTag;\n }).length === 0\n ) {\n try {\n await this.courseDB!.removeTagFromCard(this.cardID, initialTag);\n console.log(`[TagsInput] Successfully removed tag: ${initialTag}`);\n } catch (error) {\n console.error(`Failed to remove tag ${initialTag}:`, error);\n }\n }\n })\n );\n } catch (e) {\n console.error(`Exception removing tags: ${JSON.stringify(e)}`);\n }\n this.loading = false;\n },\n },\n});\n</script>\n\n<style scoped>\n.vue-tags-input {\n max-width: 100%;\n}\n\n.autocomplete-item {\n display: flex;\n align-items: center;\n padding: 5px;\n}\n\n.autocomplete-item.is-active {\n background-color: #e8e8e8;\n}\n\n.autocomplete-item.is-active > .tag-name {\n background-color: #5c6bc0;\n color: white;\n}\n\n.tag-name {\n background-color: #e0e0e0;\n color: #333;\n padding: 2px 6px;\n border-radius: 3px;\n font-weight: bold;\n margin-right: 5px;\n}\n\n.tag-snippet {\n color: #666;\n font-size: 0.9em;\n}\n\n.autocomplete-item.is-active .tag-snippet {\n color: #333; /* Ensure snippet text is visible when item is active */\n}\n</style>\n","<template>\n <v-card>\n <div v-if=\"updatePending\" class=\"d-flex justify-center align-center pa-6\">\n <v-progress-circular indeterminate color=\"primary\" />\n </div>\n <div v-else>\n <paginating-toolbar\n title=\"Exercises\"\n :page=\"page\"\n :pages=\"pages\"\n :subtitle=\"`(${questionCount})`\"\n @first=\"first\"\n @prev=\"prev\"\n @next=\"next\"\n @last=\"last\"\n @set-page=\"(n) => setPage(n)\"\n />\n\n <v-list>\n <template v-for=\"c in cards\" :key=\"c.card.cardID\">\n <v-list-item\n :class=\"{\n 'bg-blue-grey-lighten-5': c.isOpen,\n 'elevation-4': c.isOpen,\n }\"\n density=\"compact\"\n data-cy=\"course-card\"\n >\n <template #prepend>\n <div>\n <v-list-item-title :class=\"{ 'text-blue-grey-darken-1': c.isOpen }\" class=\"font-weight-medium\">\n {{ cardPreview[c.card.cardID] }}\n </v-list-item-title>\n <v-list-item-subtitle>\n ELO: {{ cardElos[c.card.cardID]?.global?.score || '(unknown)' }}\n </v-list-item-subtitle>\n </div>\n </template>\n\n <template #append>\n <v-speed-dial\n v-model=\"c.isOpen\"\n location=\"left center\"\n transition=\"slide-x-transition\"\n style=\"display: flex; flex-direction: row-reverse\"\n persistent\n >\n <template #activator=\"{ props }\">\n <v-btn\n v-bind=\"props\"\n :icon=\"c.isOpen ? 'mdi-close' : 'mdi-plus'\"\n size=\"small\"\n variant=\"text\"\n @click=\"clearSelections(c.card.cardID)\"\n />\n </template>\n\n <v-btn\n v-if=\"editMode === 'full'\"\n key=\"tags\"\n icon\n size=\"small\"\n :variant=\"internalEditMode !== 'tags' ? 'outlined' : 'elevated'\"\n :color=\"internalEditMode === 'tags' ? 'teal' : 'teal-darken-3'\"\n @click.stop=\"internalEditMode = 'tags'\"\n >\n <v-icon>mdi-bookmark</v-icon>\n </v-btn>\n\n <v-btn\n v-if=\"editMode === 'full'\"\n key=\"flag\"\n icon\n size=\"small\"\n :variant=\"internalEditMode !== 'flag' ? 'outlined' : 'elevated'\"\n :color=\"internalEditMode === 'flag' ? 'error' : 'error-darken-3'\"\n @click.stop=\"internalEditMode = 'flag'\"\n >\n <v-icon>mdi-flag</v-icon>\n </v-btn>\n </v-speed-dial>\n </template>\n </v-list-item>\n\n <div v-if=\"c.isOpen\" class=\"px-4 py-2 bg-blue-grey-lighten-5\">\n <card-loader :qualified_id=\"c.card\" :view-lookup=\"viewLookup\" class=\"elevation-1\" />\n\n <!-- Tags display for readonly mode -->\n <div v-if=\"editMode === 'readonly' && cardTags[c.card.cardID]\" class=\"mt-4\">\n <v-chip-group>\n <v-chip\n v-for=\"tag in cardTags[c.card.cardID]\"\n :key=\"tag.name\"\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n >\n {{ tag.name }}\n </v-chip>\n </v-chip-group>\n </div>\n\n <tags-input\n v-show=\"internalEditMode === 'tags' && editMode === 'full'\"\n :course-i-d=\"courseId\"\n :card-i-d=\"c.card.cardID\"\n class=\"mt-4\"\n />\n\n <div v-show=\"internalEditMode === 'flag' && editMode === 'full'\" class=\"mt-4\">\n <v-btn color=\"error\" variant=\"outlined\" @click=\"c.delBtn = true\"> Delete this card </v-btn>\n <span v-if=\"c.delBtn\" class=\"ml-4\">\n <span class=\"mr-2\">Are you sure?</span>\n <v-btn color=\"error\" variant=\"elevated\" @click=\"deleteCard(c.card.cardID)\"> Confirm </v-btn>\n </span>\n </div>\n </div>\n </template>\n </v-list>\n\n <paginating-toolbar\n class=\"elevation-0\"\n :page=\"page\"\n :pages=\"pages\"\n @first=\"first\"\n @prev=\"prev\"\n @next=\"next\"\n @last=\"last\"\n @set-page=\"(n) => setPage(n)\"\n />\n </div>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { displayableDataToViewData, Status, CourseElo } from '@vue-skuilder/common';\nimport {\n getDataLayer,\n CourseDBInterface,\n CardData,\n DisplayableData,\n Tag,\n TagStub,\n QualifiedCardID,\n} from '@vue-skuilder/db';\n// local imports\nimport TagsInput from './TagsInput.vue';\nimport PaginatingToolbar from './PaginatingToolbar.vue';\nimport { ViewComponent } from '../composables/Displayable';\nimport CardLoader from './cardRendering/CardLoader.vue';\nimport { alertUser } from './SnackbarService';\n\n// Legacy isConstructor function removed - no longer needed for Vue 3 components\n\nexport default defineComponent({\n name: 'CourseCardBrowser',\n\n components: {\n CardLoader,\n TagsInput,\n PaginatingToolbar,\n },\n\n props: {\n courseId: {\n type: String,\n required: true,\n },\n tagId: {\n type: String,\n required: false,\n default: '',\n },\n viewLookupFunction: {\n type: Function,\n required: true,\n default: () => {\n console.warn('No viewLookupFunction provided to CourseCardBrowser');\n return null;\n },\n },\n editMode: {\n type: String as PropType<'none' | 'readonly' | 'full'>,\n required: false,\n default: 'full',\n },\n },\n\n data() {\n return {\n courseDB: null as CourseDBInterface | null,\n page: 1,\n pages: [] as number[],\n cards: [] as { card: QualifiedCardID; isOpen: boolean; delBtn: boolean }[],\n cardData: {} as { [card: string]: string[] },\n cardPreview: {} as { [card: string]: string },\n cardElos: {} as Record<string, CourseElo>,\n cardTags: {} as Record<string, TagStub[]>,\n internalEditMode: 'none' as 'tags' | 'flag' | 'none',\n delBtn: false,\n updatePending: true,\n userIsRegistered: false,\n questionCount: 0,\n tags: [] as Tag[],\n viewLookup: this.viewLookupFunction,\n };\n },\n\n async created() {\n try {\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n\n if (this.tagId) {\n this.questionCount = (await this.courseDB.getTag(this.tagId)).taggedCards.length;\n } else {\n this.questionCount = (await this.courseDB!.getCourseInfo()).cardCount;\n }\n\n for (let i = 1; (i - 1) * 25 < this.questionCount; i++) {\n this.pages.push(i);\n }\n\n await this.populateTableData();\n } catch (error) {\n console.error('Error initializing CourseCardBrowser:', error);\n } finally {\n this.updatePending = false;\n }\n },\n\n methods: {\n idQualify(id: string): string {\n const delimiters = id.includes('-');\n if (delimiters) {\n return id;\n } else {\n return `${this.courseId}-${id}`;\n }\n },\n\n idToQualifiedObject(id: string): QualifiedCardID {\n const delimiters = id.includes('-');\n if (delimiters) {\n const parts = id.split('-');\n return {\n courseID: parts[0],\n cardID: parts[1],\n };\n } else {\n return {\n courseID: this.courseId,\n cardID: id,\n };\n }\n },\n\n first() {\n this.page = 1;\n this.populateTableData();\n },\n prev() {\n this.page--;\n this.populateTableData();\n },\n next() {\n this.page++;\n this.populateTableData();\n },\n last() {\n this.page = this.pages.length;\n this.populateTableData();\n },\n setPage(n: number) {\n this.page = n;\n this.populateTableData();\n },\n async loadCardTags(cardIds: string[]) {\n try {\n // Use the proper API method to get tags for each card\n await Promise.all(\n cardIds.map(async (cardId) => {\n const appliedTags = await this.courseDB!.getAppliedTags(cardId);\n\n // Convert to TagStub format\n this.cardTags[cardId] = appliedTags.rows.map((row) => ({\n name: row.value.name,\n snippet: row.value.snippet,\n count: row.value.count,\n }));\n })\n );\n } catch (error) {\n console.error('Error loading card tags:', error);\n }\n },\n clearSelections(exception: string = '') {\n this.cards.forEach((card) => {\n if (card.card.cardID !== exception) {\n card.isOpen = false;\n }\n });\n this.internalEditMode = 'none';\n this.delBtn = false;\n },\n async deleteCard(cID: string) {\n console.log(`Deleting card ${cID}`);\n const res = await this.courseDB!.removeCard(cID);\n if (res.ok) {\n this.cards = this.cards.filter((card) => card.card.cardID != cID);\n this.clearSelections();\n } else {\n console.error(`Failed to delete card:\\n\\n${JSON.stringify(res)}`);\n alertUser({\n text: 'Failed to delete card',\n status: Status.error,\n });\n }\n },\n async populateTableData() {\n this.updatePending = true;\n if (this.tagId) {\n const tag = await this.courseDB!.getTag(this.tagId);\n this.cards = tag.taggedCards.map((c) => {\n return { card: { cardID: c, courseID: this.courseId }, isOpen: false, delBtn: false };\n });\n } else {\n this.cards = (await this.courseDB!.getCardsByELO(0, 25)).map((c) => {\n return {\n card: c,\n isOpen: false,\n delBtn: false,\n };\n });\n }\n\n const toRemove: string[] = [];\n const hydratedCardData = (\n await this.courseDB!.getCourseDocs<CardData>(\n this.cards.map((c) => c.card.cardID),\n {\n include_docs: true,\n }\n )\n ).rows\n .filter((r) => {\n if (r.doc) {\n return true;\n } else {\n console.error(`Card ${r.id}.doc not found.\\ncard: ${JSON.stringify(r)}`);\n // toRemove.push(r.id);\n // if (this.tagId) {\n // this.courseDB!.removeTagFromCard(r.id, this.tagId);\n // }\n return false;\n }\n })\n .map((r) => r.doc!);\n\n this.cards = this.cards.filter((c) => !toRemove.includes(c.card.cardID));\n\n hydratedCardData.forEach((c) => {\n if (c && c.id_displayable_data) {\n this.cardData[c._id] = c.id_displayable_data;\n }\n });\n\n try {\n await Promise.all(\n this.cards.map(async (c) => {\n const _cardID: string = c.card.cardID;\n\n const tmpCardData = hydratedCardData.find((c) => c._id == _cardID);\n if (!tmpCardData || !tmpCardData.id_displayable_data) {\n console.error(`No valid data found for card ${_cardID}`);\n return;\n }\n const tmpView: ViewComponent = this.viewLookupFunction(\n tmpCardData.id_view || 'default.question.BlanksCard.FillInView'\n );\n\n const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {\n return this.courseDB!.getCourseDoc<DisplayableData>(id, {\n attachments: false,\n binary: true,\n });\n });\n\n const allDocs = await Promise.all(tmpDataDocs);\n await Promise.all(\n allDocs.map((doc) => {\n const tmpData = [];\n tmpData.unshift(displayableDataToViewData(doc));\n\n // Vue 3: Use component name for preview (legacy constructor code removed)\n this.cardPreview[c.card.cardID] = tmpView.name ? tmpView.name : 'Unknown';\n })\n );\n })\n );\n\n // Load ELO data for all cards\n const cardIds = this.cards.map((c) => c.card.cardID);\n const eloData = await this.courseDB!.getCardEloData(cardIds); // general case lookup\n\n // Store ELO data indexed by card ID\n cardIds.forEach((cardId, index) => {\n this.cardElos[cardId] = eloData[index];\n });\n\n // Load tags for each card\n await this.loadCardTags(cardIds);\n } catch (error) {\n console.error('Error populating table data:', error);\n } finally {\n this.updatePending = false;\n this.$forceUpdate();\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter, .component-fade-leave-to\n/* .component-fade-leave-active below version 2.1.8 */ {\n opacity: 0;\n}\n\n.component-scale-enter-active,\n.component-scale-leave-active {\n max-height: auto;\n transform: scale(1, 1);\n transform-origin: top;\n transition:\n transform 0.3s ease,\n max-height 0.3s ease;\n}\n.component-scale-enter,\n.component-fade-leave-to {\n max-height: 0px;\n transform: scale(1, 0);\n overflow: hidden;\n}\n</style>\n","<template>\n <v-card>\n <div v-if=\"updatePending\" class=\"d-flex justify-center align-center pa-6\">\n <v-progress-circular indeterminate color=\"primary\" />\n </div>\n <div v-else>\n <paginating-toolbar\n title=\"Exercises\"\n :page=\"page\"\n :pages=\"pages\"\n :subtitle=\"`(${questionCount})`\"\n @first=\"first\"\n @prev=\"prev\"\n @next=\"next\"\n @last=\"last\"\n @set-page=\"(n) => setPage(n)\"\n />\n\n <v-list>\n <template v-for=\"c in cards\" :key=\"c.card.cardID\">\n <v-list-item\n :class=\"{\n 'bg-blue-grey-lighten-5': c.isOpen,\n 'elevation-4': c.isOpen,\n }\"\n density=\"compact\"\n data-cy=\"course-card\"\n >\n <template #prepend>\n <div>\n <v-list-item-title :class=\"{ 'text-blue-grey-darken-1': c.isOpen }\" class=\"font-weight-medium\">\n {{ cardPreview[c.card.cardID] }}\n </v-list-item-title>\n <v-list-item-subtitle>\n ELO: {{ cardElos[c.card.cardID]?.global?.score || '(unknown)' }}\n </v-list-item-subtitle>\n </div>\n </template>\n\n <template #append>\n <v-speed-dial\n v-model=\"c.isOpen\"\n location=\"left center\"\n transition=\"slide-x-transition\"\n style=\"display: flex; flex-direction: row-reverse\"\n persistent\n >\n <template #activator=\"{ props }\">\n <v-btn\n v-bind=\"props\"\n :icon=\"c.isOpen ? 'mdi-close' : 'mdi-plus'\"\n size=\"small\"\n variant=\"text\"\n @click=\"clearSelections(c.card.cardID)\"\n />\n </template>\n\n <v-btn\n v-if=\"editMode === 'full'\"\n key=\"tags\"\n icon\n size=\"small\"\n :variant=\"internalEditMode !== 'tags' ? 'outlined' : 'elevated'\"\n :color=\"internalEditMode === 'tags' ? 'teal' : 'teal-darken-3'\"\n @click.stop=\"internalEditMode = 'tags'\"\n >\n <v-icon>mdi-bookmark</v-icon>\n </v-btn>\n\n <v-btn\n v-if=\"editMode === 'full'\"\n key=\"flag\"\n icon\n size=\"small\"\n :variant=\"internalEditMode !== 'flag' ? 'outlined' : 'elevated'\"\n :color=\"internalEditMode === 'flag' ? 'error' : 'error-darken-3'\"\n @click.stop=\"internalEditMode = 'flag'\"\n >\n <v-icon>mdi-flag</v-icon>\n </v-btn>\n </v-speed-dial>\n </template>\n </v-list-item>\n\n <div v-if=\"c.isOpen\" class=\"px-4 py-2 bg-blue-grey-lighten-5\">\n <card-loader :qualified_id=\"c.card\" :view-lookup=\"viewLookup\" class=\"elevation-1\" />\n\n <!-- Tags display for readonly mode -->\n <div v-if=\"editMode === 'readonly' && cardTags[c.card.cardID]\" class=\"mt-4\">\n <v-chip-group>\n <v-chip\n v-for=\"tag in cardTags[c.card.cardID]\"\n :key=\"tag.name\"\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n >\n {{ tag.name }}\n </v-chip>\n </v-chip-group>\n </div>\n\n <tags-input\n v-show=\"internalEditMode === 'tags' && editMode === 'full'\"\n :course-i-d=\"courseId\"\n :card-i-d=\"c.card.cardID\"\n class=\"mt-4\"\n />\n\n <div v-show=\"internalEditMode === 'flag' && editMode === 'full'\" class=\"mt-4\">\n <v-btn color=\"error\" variant=\"outlined\" @click=\"c.delBtn = true\"> Delete this card </v-btn>\n <span v-if=\"c.delBtn\" class=\"ml-4\">\n <span class=\"mr-2\">Are you sure?</span>\n <v-btn color=\"error\" variant=\"elevated\" @click=\"deleteCard(c.card.cardID)\"> Confirm </v-btn>\n </span>\n </div>\n </div>\n </template>\n </v-list>\n\n <paginating-toolbar\n class=\"elevation-0\"\n :page=\"page\"\n :pages=\"pages\"\n @first=\"first\"\n @prev=\"prev\"\n @next=\"next\"\n @last=\"last\"\n @set-page=\"(n) => setPage(n)\"\n />\n </div>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { displayableDataToViewData, Status, CourseElo } from '@vue-skuilder/common';\nimport {\n getDataLayer,\n CourseDBInterface,\n CardData,\n DisplayableData,\n Tag,\n TagStub,\n QualifiedCardID,\n} from '@vue-skuilder/db';\n// local imports\nimport TagsInput from './TagsInput.vue';\nimport PaginatingToolbar from './PaginatingToolbar.vue';\nimport { ViewComponent } from '../composables/Displayable';\nimport CardLoader from './cardRendering/CardLoader.vue';\nimport { alertUser } from './SnackbarService';\n\n// Legacy isConstructor function removed - no longer needed for Vue 3 components\n\nexport default defineComponent({\n name: 'CourseCardBrowser',\n\n components: {\n CardLoader,\n TagsInput,\n PaginatingToolbar,\n },\n\n props: {\n courseId: {\n type: String,\n required: true,\n },\n tagId: {\n type: String,\n required: false,\n default: '',\n },\n viewLookupFunction: {\n type: Function,\n required: true,\n default: () => {\n console.warn('No viewLookupFunction provided to CourseCardBrowser');\n return null;\n },\n },\n editMode: {\n type: String as PropType<'none' | 'readonly' | 'full'>,\n required: false,\n default: 'full',\n },\n },\n\n data() {\n return {\n courseDB: null as CourseDBInterface | null,\n page: 1,\n pages: [] as number[],\n cards: [] as { card: QualifiedCardID; isOpen: boolean; delBtn: boolean }[],\n cardData: {} as { [card: string]: string[] },\n cardPreview: {} as { [card: string]: string },\n cardElos: {} as Record<string, CourseElo>,\n cardTags: {} as Record<string, TagStub[]>,\n internalEditMode: 'none' as 'tags' | 'flag' | 'none',\n delBtn: false,\n updatePending: true,\n userIsRegistered: false,\n questionCount: 0,\n tags: [] as Tag[],\n viewLookup: this.viewLookupFunction,\n };\n },\n\n async created() {\n try {\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n\n if (this.tagId) {\n this.questionCount = (await this.courseDB.getTag(this.tagId)).taggedCards.length;\n } else {\n this.questionCount = (await this.courseDB!.getCourseInfo()).cardCount;\n }\n\n for (let i = 1; (i - 1) * 25 < this.questionCount; i++) {\n this.pages.push(i);\n }\n\n await this.populateTableData();\n } catch (error) {\n console.error('Error initializing CourseCardBrowser:', error);\n } finally {\n this.updatePending = false;\n }\n },\n\n methods: {\n idQualify(id: string): string {\n const delimiters = id.includes('-');\n if (delimiters) {\n return id;\n } else {\n return `${this.courseId}-${id}`;\n }\n },\n\n idToQualifiedObject(id: string): QualifiedCardID {\n const delimiters = id.includes('-');\n if (delimiters) {\n const parts = id.split('-');\n return {\n courseID: parts[0],\n cardID: parts[1],\n };\n } else {\n return {\n courseID: this.courseId,\n cardID: id,\n };\n }\n },\n\n first() {\n this.page = 1;\n this.populateTableData();\n },\n prev() {\n this.page--;\n this.populateTableData();\n },\n next() {\n this.page++;\n this.populateTableData();\n },\n last() {\n this.page = this.pages.length;\n this.populateTableData();\n },\n setPage(n: number) {\n this.page = n;\n this.populateTableData();\n },\n async loadCardTags(cardIds: string[]) {\n try {\n // Use the proper API method to get tags for each card\n await Promise.all(\n cardIds.map(async (cardId) => {\n const appliedTags = await this.courseDB!.getAppliedTags(cardId);\n\n // Convert to TagStub format\n this.cardTags[cardId] = appliedTags.rows.map((row) => ({\n name: row.value.name,\n snippet: row.value.snippet,\n count: row.value.count,\n }));\n })\n );\n } catch (error) {\n console.error('Error loading card tags:', error);\n }\n },\n clearSelections(exception: string = '') {\n this.cards.forEach((card) => {\n if (card.card.cardID !== exception) {\n card.isOpen = false;\n }\n });\n this.internalEditMode = 'none';\n this.delBtn = false;\n },\n async deleteCard(cID: string) {\n console.log(`Deleting card ${cID}`);\n const res = await this.courseDB!.removeCard(cID);\n if (res.ok) {\n this.cards = this.cards.filter((card) => card.card.cardID != cID);\n this.clearSelections();\n } else {\n console.error(`Failed to delete card:\\n\\n${JSON.stringify(res)}`);\n alertUser({\n text: 'Failed to delete card',\n status: Status.error,\n });\n }\n },\n async populateTableData() {\n this.updatePending = true;\n if (this.tagId) {\n const tag = await this.courseDB!.getTag(this.tagId);\n this.cards = tag.taggedCards.map((c) => {\n return { card: { cardID: c, courseID: this.courseId }, isOpen: false, delBtn: false };\n });\n } else {\n this.cards = (await this.courseDB!.getCardsByELO(0, 25)).map((c) => {\n return {\n card: c,\n isOpen: false,\n delBtn: false,\n };\n });\n }\n\n const toRemove: string[] = [];\n const hydratedCardData = (\n await this.courseDB!.getCourseDocs<CardData>(\n this.cards.map((c) => c.card.cardID),\n {\n include_docs: true,\n }\n )\n ).rows\n .filter((r) => {\n if (r.doc) {\n return true;\n } else {\n console.error(`Card ${r.id}.doc not found.\\ncard: ${JSON.stringify(r)}`);\n // toRemove.push(r.id);\n // if (this.tagId) {\n // this.courseDB!.removeTagFromCard(r.id, this.tagId);\n // }\n return false;\n }\n })\n .map((r) => r.doc!);\n\n this.cards = this.cards.filter((c) => !toRemove.includes(c.card.cardID));\n\n hydratedCardData.forEach((c) => {\n if (c && c.id_displayable_data) {\n this.cardData[c._id] = c.id_displayable_data;\n }\n });\n\n try {\n await Promise.all(\n this.cards.map(async (c) => {\n const _cardID: string = c.card.cardID;\n\n const tmpCardData = hydratedCardData.find((c) => c._id == _cardID);\n if (!tmpCardData || !tmpCardData.id_displayable_data) {\n console.error(`No valid data found for card ${_cardID}`);\n return;\n }\n const tmpView: ViewComponent = this.viewLookupFunction(\n tmpCardData.id_view || 'default.question.BlanksCard.FillInView'\n );\n\n const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {\n return this.courseDB!.getCourseDoc<DisplayableData>(id, {\n attachments: false,\n binary: true,\n });\n });\n\n const allDocs = await Promise.all(tmpDataDocs);\n await Promise.all(\n allDocs.map((doc) => {\n const tmpData = [];\n tmpData.unshift(displayableDataToViewData(doc));\n\n // Vue 3: Use component name for preview (legacy constructor code removed)\n this.cardPreview[c.card.cardID] = tmpView.name ? tmpView.name : 'Unknown';\n })\n );\n })\n );\n\n // Load ELO data for all cards\n const cardIds = this.cards.map((c) => c.card.cardID);\n const eloData = await this.courseDB!.getCardEloData(cardIds); // general case lookup\n\n // Store ELO data indexed by card ID\n cardIds.forEach((cardId, index) => {\n this.cardElos[cardId] = eloData[index];\n });\n\n // Load tags for each card\n await this.loadCardTags(cardIds);\n } catch (error) {\n console.error('Error populating table data:', error);\n } finally {\n this.updatePending = false;\n this.$forceUpdate();\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter, .component-fade-leave-to\n/* .component-fade-leave-active below version 2.1.8 */ {\n opacity: 0;\n}\n\n.component-scale-enter-active,\n.component-scale-leave-active {\n max-height: auto;\n transform: scale(1, 1);\n transform-origin: top;\n transition:\n transform 0.3s ease,\n max-height 0.3s ease;\n}\n.component-scale-enter,\n.component-fade-leave-to {\n max-height: 0px;\n transform: scale(1, 0);\n overflow: hidden;\n}\n</style>\n","<template>\n <div v-if=\"!updatePending\">\n <slot name=\"header\" :course-config=\"courseConfig\" :course-id=\"courseId\">\n <h1 class=\"text-h4 mb-2\">{{ courseConfig.name }}</h1>\n </slot>\n\n <p class=\"text-body-2\">\n {{ courseConfig.description }}\n </p>\n\n <slot\n name=\"actions\"\n :user-is-registered=\"userIsRegistered\"\n :course-id=\"courseId\"\n :edit-mode=\"editMode\"\n :register=\"register\"\n :drop=\"drop\"\n >\n <!-- Default fallback content if no actions slot provided -->\n <transition name=\"component-fade\" mode=\"out-in\">\n <div v-if=\"userIsRegistered\">\n <v-btn color=\"success\" data-cy=\"focused-study-session-btn\" class=\"me-2\">Start ad study session</v-btn>\n <v-btn v-if=\"editMode === 'full'\" data-cy=\"add-content-btn\" color=\"indigo-lighten-1\" class=\"me-2\">\n <v-icon start>mdi-plus</v-icon>\n Add content\n </v-btn>\n <v-btn\n v-if=\"editMode === 'full'\"\n color=\"green-darken-2\"\n title=\"Rank course content for difficulty\"\n class=\"me-2\"\n >\n <v-icon start>mdi-format-list-numbered</v-icon>\n Arrange\n </v-btn>\n <v-btn v-if=\"editMode === 'full'\" color=\"error\" size=\"small\" variant=\"outlined\" @click=\"drop\">\n Drop this course\n </v-btn>\n </div>\n <div v-else>\n <v-btn data-cy=\"register-btn\" color=\"primary\" class=\"me-2\" @click=\"register\">Register</v-btn>\n <v-btn variant=\"outlined\" color=\"primary\" class=\"me-2\">Start a trial study session</v-btn>\n </div>\n </transition>\n </slot>\n\n <slot name=\"additional-content\"></slot>\n \n <v-card class=\"my-2\">\n <v-toolbar density=\"compact\">\n <v-toolbar-title>Tags</v-toolbar-title>\n <v-toolbar-items>\n <v-btn variant=\"text\">({{ tags.length }})</v-btn>\n </v-toolbar-items>\n </v-toolbar>\n <v-card-text>\n <span v-for=\"(tag, i) in tags\" :key=\"i\">\n <slot name=\"tag-link\" :tag=\"tag\" :course-id=\"courseId\">\n <v-chip variant=\"tonal\" class=\"me-2 mb-2\">\n {{ tag.name }}\n </v-chip>\n </slot>\n </span>\n </v-card-text>\n </v-card>\n\n <course-card-browser\n class=\"my-3\"\n :course-id=\"courseId\"\n :view-lookup-function=\"viewLookupFunction\"\n :edit-mode=\"editMode\"\n />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\n// import { MidiConfig } from '@vue-skuilder/courseware'; // Removed to break circular dependency\nimport CourseCardBrowser from './CourseCardBrowser.vue';\nimport { log } from '@vue-skuilder/common';\nimport { CourseDBInterface, Tag, UserDBInterface, getDataLayer } from '@vue-skuilder/db';\nimport { CourseConfig } from '@vue-skuilder/common';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\nexport default defineComponent({\n name: 'CourseInformation',\n\n components: {\n // MidiConfig, // Removed to break circular dependency\n CourseCardBrowser,\n },\n\n props: {\n courseId: {\n type: String as PropType<string>,\n required: true,\n },\n viewLookupFunction: {\n type: Function,\n required: false,\n default: (x: unknown) => {\n console.warn('No viewLookupFunction provided to CourseInformation');\n return null;\n },\n },\n editMode: {\n type: String as PropType<'none' | 'readonly' | 'full'>,\n required: false,\n default: 'full',\n },\n },\n\n data() {\n return {\n courseDB: null as CourseDBInterface | null,\n nameRules: [\n (value: string): string | boolean => {\n const max = 30;\n return value.length > max ? `Course name must be ${max} characters or less` : true;\n },\n ],\n updatePending: true,\n courseConfig: {} as CourseConfig,\n userIsRegistered: false,\n tags: [] as Tag[],\n user: null as UserDBInterface | null,\n };\n },\n\n computed: {\n // isPianoCourse removed - piano-specific logic should be in wrapper component\n },\n\n async created() {\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n this.user = await getCurrentUser();\n\n const userCourses = await this.user.getCourseRegistrationsDoc();\n\n // Admin users always have edit access (for studio mode)\n if (this.user.getUsername() === 'admin') {\n this.userIsRegistered = true;\n } else {\n this.userIsRegistered =\n userCourses.courses.filter((c) => {\n return c.courseID === this.courseId && (c.status === 'active' || c.status === undefined);\n }).length === 1;\n }\n\n this.courseConfig = (await this.courseDB!.getCourseConfig())!;\n this.tags = (await this.courseDB!.getCourseTagStubs()).rows.map((r) => r.doc!);\n this.updatePending = false;\n },\n\n methods: {\n async register() {\n log(`Registering for ${this.courseId}`);\n const res = await this.user!.registerForCourse(this.courseId);\n if (res.ok) {\n this.userIsRegistered = true;\n }\n },\n\n async drop() {\n log(`Dropping course ${this.courseId}`);\n const res = await this.user!.dropCourse(this.courseId);\n if (res.ok) {\n this.userIsRegistered = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n\n.component-scale-enter-active,\n.component-scale-leave-active {\n max-height: auto;\n transform: scale(1, 1);\n transform-origin: top;\n transition:\n transform 0.3s ease,\n max-height 0.3s ease;\n}\n.component-scale-enter,\n.component-fade-leave-to {\n max-height: 0px;\n transform: scale(1, 0);\n overflow: hidden;\n}\n</style>\n","<template>\n <div v-if=\"!updatePending\">\n <slot name=\"header\" :course-config=\"courseConfig\" :course-id=\"courseId\">\n <h1 class=\"text-h4 mb-2\">{{ courseConfig.name }}</h1>\n </slot>\n\n <p class=\"text-body-2\">\n {{ courseConfig.description }}\n </p>\n\n <slot\n name=\"actions\"\n :user-is-registered=\"userIsRegistered\"\n :course-id=\"courseId\"\n :edit-mode=\"editMode\"\n :register=\"register\"\n :drop=\"drop\"\n >\n <!-- Default fallback content if no actions slot provided -->\n <transition name=\"component-fade\" mode=\"out-in\">\n <div v-if=\"userIsRegistered\">\n <v-btn color=\"success\" data-cy=\"focused-study-session-btn\" class=\"me-2\">Start ad study session</v-btn>\n <v-btn v-if=\"editMode === 'full'\" data-cy=\"add-content-btn\" color=\"indigo-lighten-1\" class=\"me-2\">\n <v-icon start>mdi-plus</v-icon>\n Add content\n </v-btn>\n <v-btn\n v-if=\"editMode === 'full'\"\n color=\"green-darken-2\"\n title=\"Rank course content for difficulty\"\n class=\"me-2\"\n >\n <v-icon start>mdi-format-list-numbered</v-icon>\n Arrange\n </v-btn>\n <v-btn v-if=\"editMode === 'full'\" color=\"error\" size=\"small\" variant=\"outlined\" @click=\"drop\">\n Drop this course\n </v-btn>\n </div>\n <div v-else>\n <v-btn data-cy=\"register-btn\" color=\"primary\" class=\"me-2\" @click=\"register\">Register</v-btn>\n <v-btn variant=\"outlined\" color=\"primary\" class=\"me-2\">Start a trial study session</v-btn>\n </div>\n </transition>\n </slot>\n\n <slot name=\"additional-content\"></slot>\n \n <v-card class=\"my-2\">\n <v-toolbar density=\"compact\">\n <v-toolbar-title>Tags</v-toolbar-title>\n <v-toolbar-items>\n <v-btn variant=\"text\">({{ tags.length }})</v-btn>\n </v-toolbar-items>\n </v-toolbar>\n <v-card-text>\n <span v-for=\"(tag, i) in tags\" :key=\"i\">\n <slot name=\"tag-link\" :tag=\"tag\" :course-id=\"courseId\">\n <v-chip variant=\"tonal\" class=\"me-2 mb-2\">\n {{ tag.name }}\n </v-chip>\n </slot>\n </span>\n </v-card-text>\n </v-card>\n\n <course-card-browser\n class=\"my-3\"\n :course-id=\"courseId\"\n :view-lookup-function=\"viewLookupFunction\"\n :edit-mode=\"editMode\"\n />\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\n// import { MidiConfig } from '@vue-skuilder/courseware'; // Removed to break circular dependency\nimport CourseCardBrowser from './CourseCardBrowser.vue';\nimport { log } from '@vue-skuilder/common';\nimport { CourseDBInterface, Tag, UserDBInterface, getDataLayer } from '@vue-skuilder/db';\nimport { CourseConfig } from '@vue-skuilder/common';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\nexport default defineComponent({\n name: 'CourseInformation',\n\n components: {\n // MidiConfig, // Removed to break circular dependency\n CourseCardBrowser,\n },\n\n props: {\n courseId: {\n type: String as PropType<string>,\n required: true,\n },\n viewLookupFunction: {\n type: Function,\n required: false,\n default: (x: unknown) => {\n console.warn('No viewLookupFunction provided to CourseInformation');\n return null;\n },\n },\n editMode: {\n type: String as PropType<'none' | 'readonly' | 'full'>,\n required: false,\n default: 'full',\n },\n },\n\n data() {\n return {\n courseDB: null as CourseDBInterface | null,\n nameRules: [\n (value: string): string | boolean => {\n const max = 30;\n return value.length > max ? `Course name must be ${max} characters or less` : true;\n },\n ],\n updatePending: true,\n courseConfig: {} as CourseConfig,\n userIsRegistered: false,\n tags: [] as Tag[],\n user: null as UserDBInterface | null,\n };\n },\n\n computed: {\n // isPianoCourse removed - piano-specific logic should be in wrapper component\n },\n\n async created() {\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n this.user = await getCurrentUser();\n\n const userCourses = await this.user.getCourseRegistrationsDoc();\n\n // Admin users always have edit access (for studio mode)\n if (this.user.getUsername() === 'admin') {\n this.userIsRegistered = true;\n } else {\n this.userIsRegistered =\n userCourses.courses.filter((c) => {\n return c.courseID === this.courseId && (c.status === 'active' || c.status === undefined);\n }).length === 1;\n }\n\n this.courseConfig = (await this.courseDB!.getCourseConfig())!;\n this.tags = (await this.courseDB!.getCourseTagStubs()).rows.map((r) => r.doc!);\n this.updatePending = false;\n },\n\n methods: {\n async register() {\n log(`Registering for ${this.courseId}`);\n const res = await this.user!.registerForCourse(this.courseId);\n if (res.ok) {\n this.userIsRegistered = true;\n }\n },\n\n async drop() {\n log(`Dropping course ${this.courseId}`);\n const res = await this.user!.dropCourse(this.courseId);\n if (res.ok) {\n this.userIsRegistered = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n.component-fade-enter-active,\n.component-fade-leave-active {\n transition: opacity 0.5s ease;\n}\n.component-fade-enter,\n.component-fade-leave-to {\n opacity: 0;\n}\n\n.component-scale-enter-active,\n.component-scale-leave-active {\n max-height: auto;\n transform: scale(1, 1);\n transform-origin: top;\n transition:\n transform 0.3s ease,\n max-height 0.3s ease;\n}\n.component-scale-enter,\n.component-fade-leave-to {\n max-height: 0px;\n transform: scale(1, 0);\n overflow: hidden;\n}\n</style>\n","<template>\n <v-row column align=\"center\" justify=\"center\">\n <CardViewer :view=\"views[viewIndex]\" :data=\"data\" :course_id=\"'[browsing]'\" :card_id=\"'[browsing]'\" />\n <br /><br />\n <div v-if=\"!suppressSpinner\" class=\"text-subtitle-1 pa-2\">\n <v-btn v-if=\"spinner\" icon variant=\"outlined\" color=\"primary\" @click=\"decrementView\">\n <v-icon>mdi-chevron-left</v-icon>\n </v-btn>\n {{ views[viewIndex].name }}\n <v-btn v-if=\"spinner\" icon variant=\"outlined\" color=\"primary\" @click=\"incrementView\">\n <v-icon>mdi-chevron-right</v-icon>\n </v-btn>\n </div>\n </v-row>\n</template>\n\n<script lang=\"ts\">\nimport { ViewData } from '@vue-skuilder/common';\nimport { defineComponent, PropType } from 'vue';\nimport { ViewComponent } from '../composables/Displayable';\nimport CardViewer from './cardRendering/CardViewer.vue';\nimport { useCardPreviewModeStore } from '../stores/useCardPreviewModeStore';\n\nexport default defineComponent({\n name: 'CardBrowser',\n\n components: {\n CardViewer,\n },\n\n props: {\n views: {\n type: Array as PropType<Array<ViewComponent>>,\n required: true,\n },\n data: {\n type: Array as PropType<ViewData[]>,\n required: true,\n },\n suppressSpinner: {\n type: Boolean,\n default: false,\n },\n },\n\n data() {\n return {\n viewIndex: 0,\n previewMode: useCardPreviewModeStore(),\n };\n },\n\n computed: {\n spinner(): boolean {\n return this.views.length > 1;\n },\n },\n\n created() {\n console.log(`[CardBrowser] Card browser created. Cards now in 'prewviewMode'`);\n this.previewMode.setPreviewMode(true);\n },\n\n unmounted() {\n console.log(`[CardBrowser] Card browser unmounted. Cards no longer in 'prewviewMode'`);\n this.previewMode.setPreviewMode(false);\n },\n\n methods: {\n incrementView() {\n this.viewIndex++;\n this.viewIndex = (this.viewIndex + this.views.length) % this.views.length;\n },\n\n decrementView() {\n this.viewIndex--;\n this.viewIndex = (this.viewIndex + this.views.length) % this.views.length;\n },\n },\n});\n</script>\n","<template>\n <v-row column align=\"center\" justify=\"center\">\n <CardViewer :view=\"views[viewIndex]\" :data=\"data\" :course_id=\"'[browsing]'\" :card_id=\"'[browsing]'\" />\n <br /><br />\n <div v-if=\"!suppressSpinner\" class=\"text-subtitle-1 pa-2\">\n <v-btn v-if=\"spinner\" icon variant=\"outlined\" color=\"primary\" @click=\"decrementView\">\n <v-icon>mdi-chevron-left</v-icon>\n </v-btn>\n {{ views[viewIndex].name }}\n <v-btn v-if=\"spinner\" icon variant=\"outlined\" color=\"primary\" @click=\"incrementView\">\n <v-icon>mdi-chevron-right</v-icon>\n </v-btn>\n </div>\n </v-row>\n</template>\n\n<script lang=\"ts\">\nimport { ViewData } from '@vue-skuilder/common';\nimport { defineComponent, PropType } from 'vue';\nimport { ViewComponent } from '../composables/Displayable';\nimport CardViewer from './cardRendering/CardViewer.vue';\nimport { useCardPreviewModeStore } from '../stores/useCardPreviewModeStore';\n\nexport default defineComponent({\n name: 'CardBrowser',\n\n components: {\n CardViewer,\n },\n\n props: {\n views: {\n type: Array as PropType<Array<ViewComponent>>,\n required: true,\n },\n data: {\n type: Array as PropType<ViewData[]>,\n required: true,\n },\n suppressSpinner: {\n type: Boolean,\n default: false,\n },\n },\n\n data() {\n return {\n viewIndex: 0,\n previewMode: useCardPreviewModeStore(),\n };\n },\n\n computed: {\n spinner(): boolean {\n return this.views.length > 1;\n },\n },\n\n created() {\n console.log(`[CardBrowser] Card browser created. Cards now in 'prewviewMode'`);\n this.previewMode.setPreviewMode(true);\n },\n\n unmounted() {\n console.log(`[CardBrowser] Card browser unmounted. Cards no longer in 'prewviewMode'`);\n this.previewMode.setPreviewMode(false);\n },\n\n methods: {\n incrementView() {\n this.viewIndex++;\n this.viewIndex = (this.viewIndex + this.views.length) % this.views.length;\n },\n\n decrementView() {\n this.viewIndex--;\n this.viewIndex = (this.viewIndex + this.views.length) % this.views.length;\n },\n },\n});\n</script>\n","<template>\n <div class=\"course-tag-filter-widget\">\n <v-autocomplete\n v-model=\"localFilter.include\"\n :items=\"availableTags\"\n :loading=\"loading\"\n :disabled=\"loading\"\n label=\"Include tags\"\n placeholder=\"Type to search tags...\"\n hint=\"Cards must have at least one of these tags\"\n persistent-hint\n multiple\n chips\n closable-chips\n clearable\n density=\"compact\"\n variant=\"outlined\"\n class=\"mb-2\"\n :custom-filter=\"fuzzyFilter\"\n no-data-text=\"No matching tags found\"\n >\n <template #chip=\"{ props, item }\">\n <v-chip\n v-bind=\"props\"\n color=\"primary\"\n variant=\"tonal\"\n size=\"small\"\n >\n {{ item.raw }}\n </v-chip>\n </template>\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\" :title=\"item.raw\">\n <template #subtitle v-if=\"tagSnippets[item.raw]\">\n {{ tagSnippets[item.raw] }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <v-autocomplete\n v-model=\"localFilter.exclude\"\n :items=\"availableTagsForExclude\"\n :loading=\"loading\"\n :disabled=\"loading\"\n label=\"Exclude tags\"\n placeholder=\"Type to search tags...\"\n hint=\"Cards with these tags will be excluded\"\n persistent-hint\n multiple\n chips\n closable-chips\n clearable\n density=\"compact\"\n variant=\"outlined\"\n :custom-filter=\"fuzzyFilter\"\n no-data-text=\"No matching tags found\"\n >\n <template #chip=\"{ props, item }\">\n <v-chip\n v-bind=\"props\"\n color=\"error\"\n variant=\"tonal\"\n size=\"small\"\n >\n {{ item.raw }}\n </v-chip>\n </template>\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\" :title=\"item.raw\">\n <template #subtitle v-if=\"tagSnippets[item.raw]\">\n {{ tagSnippets[item.raw] }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <div v-if=\"hasActiveFilter\" class=\"filter-summary text-caption mt-2\">\n <v-icon size=\"small\" color=\"info\" class=\"mr-1\">mdi-filter</v-icon>\n <span v-if=\"localFilter.include.length\">\n Including: {{ localFilter.include.join(', ') }}\n </span>\n <span v-if=\"localFilter.include.length && localFilter.exclude.length\"> · </span>\n <span v-if=\"localFilter.exclude.length\">\n Excluding: {{ localFilter.exclude.join(', ') }}\n </span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, ref, computed, watch, onMounted } from 'vue';\nimport { TagFilter, emptyTagFilter, hasActiveFilter as checkActiveFilter } from '@vue-skuilder/common';\nimport { getDataLayer, Tag } from '@vue-skuilder/db';\n\nexport default defineComponent({\n name: 'CourseTagFilterWidget',\n\n props: {\n courseId: {\n type: String,\n required: true,\n },\n modelValue: {\n type: Object as PropType<TagFilter | undefined>,\n default: undefined,\n },\n },\n\n emits: ['update:modelValue'],\n\n setup(props, { emit }) {\n const loading = ref(true);\n const availableTags = ref<string[]>([]);\n const tagSnippets = ref<Record<string, string>>({});\n\n // Local copy of the filter for editing\n const localFilter = ref<TagFilter>(\n props.modelValue ? { ...props.modelValue } : emptyTagFilter()\n );\n\n // Computed: tags available for exclude (not already in include)\n const availableTagsForExclude = computed(() => {\n return availableTags.value.filter(\n (tag) => !localFilter.value.include.includes(tag)\n );\n });\n\n // Computed: whether filter has any active constraints\n const hasActiveFilter = computed(() => {\n return checkActiveFilter(localFilter.value);\n });\n\n // Fuzzy filter function for autocomplete\n const fuzzyFilter = (itemText: string, queryText: string): boolean => {\n if (!queryText) return true;\n\n const query = queryText.toLowerCase();\n const text = itemText.toLowerCase();\n\n // Simple fuzzy match: all query characters appear in order\n let queryIndex = 0;\n for (let i = 0; i < text.length && queryIndex < query.length; i++) {\n if (text[i] === query[queryIndex]) {\n queryIndex++;\n }\n }\n return queryIndex === query.length;\n };\n\n // Watch for external changes to modelValue\n watch(\n () => props.modelValue,\n (newValue) => {\n if (newValue) {\n localFilter.value = { ...newValue };\n } else {\n localFilter.value = emptyTagFilter();\n }\n },\n { deep: true }\n );\n\n // Emit changes when local filter changes\n watch(\n localFilter,\n (newFilter) => {\n emit('update:modelValue', { ...newFilter });\n },\n { deep: true }\n );\n\n // Watch for courseId changes and reload tags\n watch(\n () => props.courseId,\n async (newCourseId) => {\n if (newCourseId) {\n await loadTags();\n }\n }\n );\n\n // Load tags for the course\n async function loadTags() {\n loading.value = true;\n try {\n const courseDB = getDataLayer().getCourseDB(props.courseId);\n const response = await courseDB.getCourseTagStubs();\n\n availableTags.value = [];\n tagSnippets.value = {};\n\n for (const row of response.rows) {\n if (row.doc) {\n const tag = row.doc as Tag;\n availableTags.value.push(tag.name);\n if (tag.snippet) {\n tagSnippets.value[tag.name] = tag.snippet;\n }\n }\n }\n\n // Sort alphabetically\n availableTags.value.sort((a, b) => a.localeCompare(b));\n } catch (error) {\n console.error('[CourseTagFilterWidget] Failed to load tags:', error);\n availableTags.value = [];\n tagSnippets.value = {};\n } finally {\n loading.value = false;\n }\n }\n\n onMounted(() => {\n if (props.courseId) {\n loadTags();\n }\n });\n\n return {\n loading,\n availableTags,\n availableTagsForExclude,\n tagSnippets,\n localFilter,\n hasActiveFilter,\n fuzzyFilter,\n };\n },\n});\n</script>\n\n<style scoped>\n.course-tag-filter-widget {\n width: 100%;\n}\n\n.filter-summary {\n color: rgba(var(--v-theme-on-surface), 0.7);\n}\n</style>\n","<template>\n <div class=\"course-tag-filter-widget\">\n <v-autocomplete\n v-model=\"localFilter.include\"\n :items=\"availableTags\"\n :loading=\"loading\"\n :disabled=\"loading\"\n label=\"Include tags\"\n placeholder=\"Type to search tags...\"\n hint=\"Cards must have at least one of these tags\"\n persistent-hint\n multiple\n chips\n closable-chips\n clearable\n density=\"compact\"\n variant=\"outlined\"\n class=\"mb-2\"\n :custom-filter=\"fuzzyFilter\"\n no-data-text=\"No matching tags found\"\n >\n <template #chip=\"{ props, item }\">\n <v-chip\n v-bind=\"props\"\n color=\"primary\"\n variant=\"tonal\"\n size=\"small\"\n >\n {{ item.raw }}\n </v-chip>\n </template>\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\" :title=\"item.raw\">\n <template #subtitle v-if=\"tagSnippets[item.raw]\">\n {{ tagSnippets[item.raw] }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <v-autocomplete\n v-model=\"localFilter.exclude\"\n :items=\"availableTagsForExclude\"\n :loading=\"loading\"\n :disabled=\"loading\"\n label=\"Exclude tags\"\n placeholder=\"Type to search tags...\"\n hint=\"Cards with these tags will be excluded\"\n persistent-hint\n multiple\n chips\n closable-chips\n clearable\n density=\"compact\"\n variant=\"outlined\"\n :custom-filter=\"fuzzyFilter\"\n no-data-text=\"No matching tags found\"\n >\n <template #chip=\"{ props, item }\">\n <v-chip\n v-bind=\"props\"\n color=\"error\"\n variant=\"tonal\"\n size=\"small\"\n >\n {{ item.raw }}\n </v-chip>\n </template>\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\" :title=\"item.raw\">\n <template #subtitle v-if=\"tagSnippets[item.raw]\">\n {{ tagSnippets[item.raw] }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <div v-if=\"hasActiveFilter\" class=\"filter-summary text-caption mt-2\">\n <v-icon size=\"small\" color=\"info\" class=\"mr-1\">mdi-filter</v-icon>\n <span v-if=\"localFilter.include.length\">\n Including: {{ localFilter.include.join(', ') }}\n </span>\n <span v-if=\"localFilter.include.length && localFilter.exclude.length\"> · </span>\n <span v-if=\"localFilter.exclude.length\">\n Excluding: {{ localFilter.exclude.join(', ') }}\n </span>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType, ref, computed, watch, onMounted } from 'vue';\nimport { TagFilter, emptyTagFilter, hasActiveFilter as checkActiveFilter } from '@vue-skuilder/common';\nimport { getDataLayer, Tag } from '@vue-skuilder/db';\n\nexport default defineComponent({\n name: 'CourseTagFilterWidget',\n\n props: {\n courseId: {\n type: String,\n required: true,\n },\n modelValue: {\n type: Object as PropType<TagFilter | undefined>,\n default: undefined,\n },\n },\n\n emits: ['update:modelValue'],\n\n setup(props, { emit }) {\n const loading = ref(true);\n const availableTags = ref<string[]>([]);\n const tagSnippets = ref<Record<string, string>>({});\n\n // Local copy of the filter for editing\n const localFilter = ref<TagFilter>(\n props.modelValue ? { ...props.modelValue } : emptyTagFilter()\n );\n\n // Computed: tags available for exclude (not already in include)\n const availableTagsForExclude = computed(() => {\n return availableTags.value.filter(\n (tag) => !localFilter.value.include.includes(tag)\n );\n });\n\n // Computed: whether filter has any active constraints\n const hasActiveFilter = computed(() => {\n return checkActiveFilter(localFilter.value);\n });\n\n // Fuzzy filter function for autocomplete\n const fuzzyFilter = (itemText: string, queryText: string): boolean => {\n if (!queryText) return true;\n\n const query = queryText.toLowerCase();\n const text = itemText.toLowerCase();\n\n // Simple fuzzy match: all query characters appear in order\n let queryIndex = 0;\n for (let i = 0; i < text.length && queryIndex < query.length; i++) {\n if (text[i] === query[queryIndex]) {\n queryIndex++;\n }\n }\n return queryIndex === query.length;\n };\n\n // Watch for external changes to modelValue\n watch(\n () => props.modelValue,\n (newValue) => {\n if (newValue) {\n localFilter.value = { ...newValue };\n } else {\n localFilter.value = emptyTagFilter();\n }\n },\n { deep: true }\n );\n\n // Emit changes when local filter changes\n watch(\n localFilter,\n (newFilter) => {\n emit('update:modelValue', { ...newFilter });\n },\n { deep: true }\n );\n\n // Watch for courseId changes and reload tags\n watch(\n () => props.courseId,\n async (newCourseId) => {\n if (newCourseId) {\n await loadTags();\n }\n }\n );\n\n // Load tags for the course\n async function loadTags() {\n loading.value = true;\n try {\n const courseDB = getDataLayer().getCourseDB(props.courseId);\n const response = await courseDB.getCourseTagStubs();\n\n availableTags.value = [];\n tagSnippets.value = {};\n\n for (const row of response.rows) {\n if (row.doc) {\n const tag = row.doc as Tag;\n availableTags.value.push(tag.name);\n if (tag.snippet) {\n tagSnippets.value[tag.name] = tag.snippet;\n }\n }\n }\n\n // Sort alphabetically\n availableTags.value.sort((a, b) => a.localeCompare(b));\n } catch (error) {\n console.error('[CourseTagFilterWidget] Failed to load tags:', error);\n availableTags.value = [];\n tagSnippets.value = {};\n } finally {\n loading.value = false;\n }\n }\n\n onMounted(() => {\n if (props.courseId) {\n loadTags();\n }\n });\n\n return {\n loading,\n availableTags,\n availableTagsForExclude,\n tagSnippets,\n localFilter,\n hasActiveFilter,\n fuzzyFilter,\n };\n },\n});\n</script>\n\n<style scoped>\n.course-tag-filter-widget {\n width: 100%;\n}\n\n.filter-summary {\n color: rgba(var(--v-theme-on-surface), 0.7);\n}\n</style>\n","<template>\n <v-card :loading=\"loading\">\n <v-card-title>\n <v-icon start>mdi-tune</v-icon>\n Learning Preferences\n </v-card-title>\n\n <v-card-subtitle v-if=\"courseId\">\n Customize how content is presented in this course\n </v-card-subtitle>\n\n <v-card-text>\n <v-alert v-if=\"!courseId\" type=\"info\" variant=\"tonal\" class=\"mb-4\">\n Select a course to configure your learning preferences.\n </v-alert>\n\n <div v-else-if=\"!loading\">\n <!-- Tag Preferences Section -->\n <div class=\"mb-6\">\n <h3 class=\"text-subtitle-1 font-weight-bold mb-2\">\n <v-icon start size=\"small\">mdi-tune-variant</v-icon>\n Tag Preferences\n </h3>\n <p class=\"text-body-2 text-medium-emphasis mb-3\">\n Adjust how much you want to see cards with specific tags. 0 = exclude, 1 = neutral, higher = prefer more.\n </p>\n\n <!-- Tag autocomplete -->\n <v-autocomplete\n v-model=\"tagToAdd\"\n :items=\"availableTagsToAdd\"\n item-title=\"name\"\n item-value=\"name\"\n label=\"Add tag preference\"\n placeholder=\"Search tags...\"\n density=\"compact\"\n variant=\"outlined\"\n clearable\n hide-details\n class=\"mb-4\"\n @update:model-value=\"addTag\"\n >\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\">\n <template #subtitle>\n {{ item.raw.snippet }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <!-- Tag slider list -->\n <v-list v-if=\"tagNames.length > 0\" class=\"mt-4\">\n <v-list-item\n v-for=\"tagName in tagNames\"\n :key=\"tagName\"\n class=\"px-0\"\n >\n <template #default>\n <div class=\"d-flex align-center ga-3 w-100\">\n <!-- Tag name -->\n <v-chip\n size=\"small\"\n variant=\"tonal\"\n class=\"flex-shrink-0\"\n style=\"min-width: 120px;\"\n >\n {{ tagName }}\n </v-chip>\n\n <!-- Slider -->\n <v-slider\n :model-value=\"preferences.boost[tagName]\"\n :min=\"sliderConfigResolved.min\"\n :max=\"globalSliderMax\"\n :step=\"0.01\"\n hide-details\n class=\"flex-grow-1\"\n thumb-label\n @update:model-value=\"(val: number) => updateBoost(tagName, val)\"\n >\n <template #thumb-label=\"{ modelValue }\">\n {{ formatMultiplier(modelValue) }}\n </template>\n </v-slider>\n\n <!-- Current value display -->\n <span class=\"text-body-2 flex-shrink-0\" style=\"min-width: 60px; text-align: right;\">\n {{ formatMultiplier(preferences.boost[tagName]) }}\n </span>\n\n <!-- Expand range button (always visible) -->\n <v-btn\n icon=\"mdi-plus\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n :disabled=\"globalSliderMax >= sliderConfigResolved.absoluteMax\"\n @click=\"expandSliderRange(tagName)\"\n />\n\n <!-- Delete button -->\n <v-btn\n icon=\"mdi-delete\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n @click=\"removeTag(tagName)\"\n />\n </div>\n </template>\n </v-list-item>\n </v-list>\n\n <v-alert v-else type=\"info\" variant=\"tonal\" class=\"mt-4\">\n No tag preferences configured yet. Add tags above to get started.\n </v-alert>\n </div>\n\n <!-- Status / Feedback -->\n <v-alert v-if=\"saveError\" type=\"error\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveError = ''\">\n {{ saveError }}\n </v-alert>\n\n <v-alert v-if=\"saveSuccess\" type=\"success\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveSuccess = false\">\n Preferences saved successfully\n </v-alert>\n </div>\n\n <div v-else class=\"d-flex justify-center py-8\">\n <v-progress-circular indeterminate color=\"primary\" />\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"courseId && !loading\">\n <v-spacer />\n <v-btn\n variant=\"text\"\n :disabled=\"!hasChanges\"\n @click=\"resetToSaved\"\n >\n Reset\n </v-btn>\n <v-btn\n color=\"primary\"\n variant=\"flat\"\n :loading=\"saving\"\n :disabled=\"!hasChanges\"\n @click=\"savePreferences\"\n >\n Save Preferences\n </v-btn>\n </v-card-actions>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface, UserDBInterface } from '@vue-skuilder/db';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\n/**\n * User's tag preference state, matching the backend schema.\n */\ninterface UserTagPreferenceState {\n boost: Record<string, number>;\n updatedAt: string;\n}\n\n/**\n * Slider configuration for tag preferences.\n */\ninterface SliderConfig {\n min?: number; // default: 0\n startingMax?: number; // default: 2\n absoluteMax?: number; // default: 10\n}\n\nconst DEFAULT_SLIDER_CONFIG: Required<SliderConfig> = {\n min: 0,\n startingMax: 2,\n absoluteMax: 10,\n};\n\nconst STRATEGY_KEY = 'UserTagPreferenceFilter';\n\nexport default defineComponent({\n name: 'UserTagPreferences',\n\n props: {\n /**\n * Course ID to configure preferences for.\n * If not provided, component shows a prompt to select a course.\n */\n courseId: {\n type: String as PropType<string>,\n required: false,\n default: '',\n },\n\n /**\n * Slider configuration (min, startingMax, absoluteMax).\n * All fields optional, defaults: { min: 0, startingMax: 2, absoluteMax: 10 }\n */\n sliderConfig: {\n type: Object as PropType<SliderConfig>,\n required: false,\n default: () => ({}),\n },\n },\n\n emits: ['preferences-saved', 'preferences-changed'],\n\n data() {\n return {\n loading: true,\n saving: false,\n saveError: '',\n saveSuccess: false,\n\n // Current working state\n preferences: {\n boost: {} as Record<string, number>,\n },\n\n // Saved state for comparison\n savedPreferences: {\n boost: {} as Record<string, number>,\n },\n\n // Per-tag current max range (for dynamic expansion)\n tagMaxRanges: {} as Record<string, number>,\n\n // Available tags from course\n availableTags: [] as Tag[],\n\n // Autocomplete model\n tagToAdd: null as string | null,\n\n // DB references\n courseDB: null as CourseDBInterface | null,\n user: null as UserDBInterface | null,\n };\n },\n\n computed: {\n /**\n * Resolved slider config with defaults\n */\n sliderConfigResolved(): Required<SliderConfig> {\n return {\n min: this.sliderConfig.min ?? DEFAULT_SLIDER_CONFIG.min,\n startingMax: this.sliderConfig.startingMax ?? DEFAULT_SLIDER_CONFIG.startingMax,\n absoluteMax: this.sliderConfig.absoluteMax ?? DEFAULT_SLIDER_CONFIG.absoluteMax,\n };\n },\n\n /**\n * List of tag names that have preferences (sorted alphabetically)\n */\n tagNames(): string[] {\n return Object.keys(this.preferences.boost).sort();\n },\n\n /**\n * Global max for all sliders (highest tagMaxRanges value)\n * Ensures all sliders have the same visual scale\n */\n globalSliderMax(): number {\n const maxValues = Object.values(this.tagMaxRanges);\n if (maxValues.length === 0) {\n return this.sliderConfigResolved.startingMax;\n }\n return Math.max(...maxValues);\n },\n\n /**\n * Tags available to add (not already in preferences)\n */\n availableTagsToAdd(): Tag[] {\n const usedTags = new Set(Object.keys(this.preferences.boost));\n return this.availableTags.filter((t) => !usedTags.has(t.name));\n },\n\n /**\n * Check if current preferences differ from saved state\n */\n hasChanges(): boolean {\n const currentTags = Object.keys(this.preferences.boost).sort();\n const savedTags = Object.keys(this.savedPreferences.boost).sort();\n\n if (currentTags.length !== savedTags.length) {\n return true;\n }\n\n if (currentTags.join(',') !== savedTags.join(',')) {\n return true;\n }\n\n return currentTags.some(\n (tag) => this.preferences.boost[tag] !== this.savedPreferences.boost[tag]\n );\n },\n },\n\n watch: {\n courseId: {\n immediate: true,\n async handler(newCourseId: string) {\n if (newCourseId) {\n await this.loadPreferences();\n } else {\n this.loading = false;\n }\n },\n },\n },\n\n methods: {\n /**\n * Load preferences from strategy state and available tags from course\n */\n async loadPreferences() {\n this.loading = true;\n this.saveError = '';\n\n try {\n // Get user and course DB\n this.user = (await getCurrentUser()) ?? null;\n if (!this.user) {\n throw new Error('User not available');\n }\n\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n\n // Load available tags\n const tagStubs = await this.courseDB.getCourseTagStubs();\n this.availableTags = tagStubs.rows.map((r) => r.doc!).filter(Boolean);\n\n // Load saved preferences\n const state = await this.user.getStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY\n );\n\n if (state) {\n this.preferences.boost = { ...state.boost };\n } else {\n // No preferences yet - start fresh\n this.preferences.boost = {};\n }\n\n // Store saved state for comparison\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n // Initialize tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n // If saved value exceeds startingMax, expand range to accommodate it\n // (capped at absoluteMax)\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n } catch (e) {\n console.error('Failed to load preferences:', e);\n this.saveError = 'Failed to load preferences. Please try again.';\n } finally {\n this.loading = false;\n }\n },\n\n /**\n * Add a tag to preferences with default multiplier of 1.0\n */\n addTag(tagName: string | null) {\n if (tagName && !(tagName in this.preferences.boost)) {\n this.preferences.boost[tagName] = 1.0;\n this.tagMaxRanges[tagName] = this.sliderConfigResolved.startingMax;\n this.$emit('preferences-changed', this.preferences);\n }\n // Clear the autocomplete\n this.$nextTick(() => {\n this.tagToAdd = null;\n });\n },\n\n /**\n * Remove a tag from preferences\n */\n removeTag(tagName: string) {\n delete this.preferences.boost[tagName];\n delete this.tagMaxRanges[tagName];\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Update boost multiplier for a tag\n */\n updateBoost(tagName: string, value: number) {\n this.preferences.boost[tagName] = value;\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Expand the global slider range by 1 and move the triggering tag's slider to new max\n */\n expandSliderRange(tagName: string) {\n const currentGlobalMax = this.globalSliderMax;\n if (currentGlobalMax < this.sliderConfigResolved.absoluteMax) {\n const newGlobalMax = currentGlobalMax + 1;\n\n // Expand all tag ranges to the new global max\n // (ensures all sliders stay synchronized)\n Object.keys(this.tagMaxRanges).forEach((tag) => {\n this.tagMaxRanges[tag] = Math.max(this.tagMaxRanges[tag], newGlobalMax);\n });\n\n // Move the triggering tag's slider to new max\n this.preferences.boost[tagName] = newGlobalMax;\n this.$emit('preferences-changed', this.preferences);\n }\n },\n\n /**\n * Format multiplier for display\n */\n formatMultiplier(value: number): string {\n if (value === 0) {\n return '0x';\n }\n if (value < 0.1) {\n return value.toFixed(2) + 'x';\n }\n return value.toFixed(1) + 'x';\n },\n\n /**\n * Reset to last saved state\n */\n resetToSaved() {\n this.preferences.boost = { ...this.savedPreferences.boost };\n this.saveSuccess = false;\n this.saveError = '';\n\n // Reset tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n },\n\n /**\n * Save preferences to strategy state\n */\n async savePreferences() {\n if (!this.user || !this.courseId) {\n this.saveError = 'Unable to save - user or course not available';\n return;\n }\n\n this.saving = true;\n this.saveError = '';\n this.saveSuccess = false;\n\n try {\n const state: UserTagPreferenceState = {\n boost: { ...this.preferences.boost },\n updatedAt: new Date().toISOString(),\n };\n\n await this.user.putStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY,\n state\n );\n\n // Update saved state\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n this.saveSuccess = true;\n this.$emit('preferences-saved', state);\n } catch (e) {\n console.error('Failed to save preferences:', e);\n this.saveError = 'Failed to save preferences. Please try again.';\n } finally {\n this.saving = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n/* Additional styles if needed */\n</style>\n","<template>\n <v-card :loading=\"loading\">\n <v-card-title>\n <v-icon start>mdi-tune</v-icon>\n Learning Preferences\n </v-card-title>\n\n <v-card-subtitle v-if=\"courseId\">\n Customize how content is presented in this course\n </v-card-subtitle>\n\n <v-card-text>\n <v-alert v-if=\"!courseId\" type=\"info\" variant=\"tonal\" class=\"mb-4\">\n Select a course to configure your learning preferences.\n </v-alert>\n\n <div v-else-if=\"!loading\">\n <!-- Tag Preferences Section -->\n <div class=\"mb-6\">\n <h3 class=\"text-subtitle-1 font-weight-bold mb-2\">\n <v-icon start size=\"small\">mdi-tune-variant</v-icon>\n Tag Preferences\n </h3>\n <p class=\"text-body-2 text-medium-emphasis mb-3\">\n Adjust how much you want to see cards with specific tags. 0 = exclude, 1 = neutral, higher = prefer more.\n </p>\n\n <!-- Tag autocomplete -->\n <v-autocomplete\n v-model=\"tagToAdd\"\n :items=\"availableTagsToAdd\"\n item-title=\"name\"\n item-value=\"name\"\n label=\"Add tag preference\"\n placeholder=\"Search tags...\"\n density=\"compact\"\n variant=\"outlined\"\n clearable\n hide-details\n class=\"mb-4\"\n @update:model-value=\"addTag\"\n >\n <template #item=\"{ props, item }\">\n <v-list-item v-bind=\"props\">\n <template #subtitle>\n {{ item.raw.snippet }}\n </template>\n </v-list-item>\n </template>\n </v-autocomplete>\n\n <!-- Tag slider list -->\n <v-list v-if=\"tagNames.length > 0\" class=\"mt-4\">\n <v-list-item\n v-for=\"tagName in tagNames\"\n :key=\"tagName\"\n class=\"px-0\"\n >\n <template #default>\n <div class=\"d-flex align-center ga-3 w-100\">\n <!-- Tag name -->\n <v-chip\n size=\"small\"\n variant=\"tonal\"\n class=\"flex-shrink-0\"\n style=\"min-width: 120px;\"\n >\n {{ tagName }}\n </v-chip>\n\n <!-- Slider -->\n <v-slider\n :model-value=\"preferences.boost[tagName]\"\n :min=\"sliderConfigResolved.min\"\n :max=\"globalSliderMax\"\n :step=\"0.01\"\n hide-details\n class=\"flex-grow-1\"\n thumb-label\n @update:model-value=\"(val: number) => updateBoost(tagName, val)\"\n >\n <template #thumb-label=\"{ modelValue }\">\n {{ formatMultiplier(modelValue) }}\n </template>\n </v-slider>\n\n <!-- Current value display -->\n <span class=\"text-body-2 flex-shrink-0\" style=\"min-width: 60px; text-align: right;\">\n {{ formatMultiplier(preferences.boost[tagName]) }}\n </span>\n\n <!-- Expand range button (always visible) -->\n <v-btn\n icon=\"mdi-plus\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n :disabled=\"globalSliderMax >= sliderConfigResolved.absoluteMax\"\n @click=\"expandSliderRange(tagName)\"\n />\n\n <!-- Delete button -->\n <v-btn\n icon=\"mdi-delete\"\n size=\"small\"\n variant=\"text\"\n density=\"compact\"\n @click=\"removeTag(tagName)\"\n />\n </div>\n </template>\n </v-list-item>\n </v-list>\n\n <v-alert v-else type=\"info\" variant=\"tonal\" class=\"mt-4\">\n No tag preferences configured yet. Add tags above to get started.\n </v-alert>\n </div>\n\n <!-- Status / Feedback -->\n <v-alert v-if=\"saveError\" type=\"error\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveError = ''\">\n {{ saveError }}\n </v-alert>\n\n <v-alert v-if=\"saveSuccess\" type=\"success\" variant=\"tonal\" class=\"mt-4\" closable @click:close=\"saveSuccess = false\">\n Preferences saved successfully\n </v-alert>\n </div>\n\n <div v-else class=\"d-flex justify-center py-8\">\n <v-progress-circular indeterminate color=\"primary\" />\n </div>\n </v-card-text>\n\n <v-card-actions v-if=\"courseId && !loading\">\n <v-spacer />\n <v-btn\n variant=\"text\"\n :disabled=\"!hasChanges\"\n @click=\"resetToSaved\"\n >\n Reset\n </v-btn>\n <v-btn\n color=\"primary\"\n variant=\"flat\"\n :loading=\"saving\"\n :disabled=\"!hasChanges\"\n @click=\"savePreferences\"\n >\n Save Preferences\n </v-btn>\n </v-card-actions>\n </v-card>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from 'vue';\nimport { getDataLayer, Tag, CourseDBInterface, UserDBInterface } from '@vue-skuilder/db';\nimport { getCurrentUser } from '../stores/useAuthStore';\n\n/**\n * User's tag preference state, matching the backend schema.\n */\ninterface UserTagPreferenceState {\n boost: Record<string, number>;\n updatedAt: string;\n}\n\n/**\n * Slider configuration for tag preferences.\n */\ninterface SliderConfig {\n min?: number; // default: 0\n startingMax?: number; // default: 2\n absoluteMax?: number; // default: 10\n}\n\nconst DEFAULT_SLIDER_CONFIG: Required<SliderConfig> = {\n min: 0,\n startingMax: 2,\n absoluteMax: 10,\n};\n\nconst STRATEGY_KEY = 'UserTagPreferenceFilter';\n\nexport default defineComponent({\n name: 'UserTagPreferences',\n\n props: {\n /**\n * Course ID to configure preferences for.\n * If not provided, component shows a prompt to select a course.\n */\n courseId: {\n type: String as PropType<string>,\n required: false,\n default: '',\n },\n\n /**\n * Slider configuration (min, startingMax, absoluteMax).\n * All fields optional, defaults: { min: 0, startingMax: 2, absoluteMax: 10 }\n */\n sliderConfig: {\n type: Object as PropType<SliderConfig>,\n required: false,\n default: () => ({}),\n },\n },\n\n emits: ['preferences-saved', 'preferences-changed'],\n\n data() {\n return {\n loading: true,\n saving: false,\n saveError: '',\n saveSuccess: false,\n\n // Current working state\n preferences: {\n boost: {} as Record<string, number>,\n },\n\n // Saved state for comparison\n savedPreferences: {\n boost: {} as Record<string, number>,\n },\n\n // Per-tag current max range (for dynamic expansion)\n tagMaxRanges: {} as Record<string, number>,\n\n // Available tags from course\n availableTags: [] as Tag[],\n\n // Autocomplete model\n tagToAdd: null as string | null,\n\n // DB references\n courseDB: null as CourseDBInterface | null,\n user: null as UserDBInterface | null,\n };\n },\n\n computed: {\n /**\n * Resolved slider config with defaults\n */\n sliderConfigResolved(): Required<SliderConfig> {\n return {\n min: this.sliderConfig.min ?? DEFAULT_SLIDER_CONFIG.min,\n startingMax: this.sliderConfig.startingMax ?? DEFAULT_SLIDER_CONFIG.startingMax,\n absoluteMax: this.sliderConfig.absoluteMax ?? DEFAULT_SLIDER_CONFIG.absoluteMax,\n };\n },\n\n /**\n * List of tag names that have preferences (sorted alphabetically)\n */\n tagNames(): string[] {\n return Object.keys(this.preferences.boost).sort();\n },\n\n /**\n * Global max for all sliders (highest tagMaxRanges value)\n * Ensures all sliders have the same visual scale\n */\n globalSliderMax(): number {\n const maxValues = Object.values(this.tagMaxRanges);\n if (maxValues.length === 0) {\n return this.sliderConfigResolved.startingMax;\n }\n return Math.max(...maxValues);\n },\n\n /**\n * Tags available to add (not already in preferences)\n */\n availableTagsToAdd(): Tag[] {\n const usedTags = new Set(Object.keys(this.preferences.boost));\n return this.availableTags.filter((t) => !usedTags.has(t.name));\n },\n\n /**\n * Check if current preferences differ from saved state\n */\n hasChanges(): boolean {\n const currentTags = Object.keys(this.preferences.boost).sort();\n const savedTags = Object.keys(this.savedPreferences.boost).sort();\n\n if (currentTags.length !== savedTags.length) {\n return true;\n }\n\n if (currentTags.join(',') !== savedTags.join(',')) {\n return true;\n }\n\n return currentTags.some(\n (tag) => this.preferences.boost[tag] !== this.savedPreferences.boost[tag]\n );\n },\n },\n\n watch: {\n courseId: {\n immediate: true,\n async handler(newCourseId: string) {\n if (newCourseId) {\n await this.loadPreferences();\n } else {\n this.loading = false;\n }\n },\n },\n },\n\n methods: {\n /**\n * Load preferences from strategy state and available tags from course\n */\n async loadPreferences() {\n this.loading = true;\n this.saveError = '';\n\n try {\n // Get user and course DB\n this.user = (await getCurrentUser()) ?? null;\n if (!this.user) {\n throw new Error('User not available');\n }\n\n this.courseDB = getDataLayer().getCourseDB(this.courseId);\n\n // Load available tags\n const tagStubs = await this.courseDB.getCourseTagStubs();\n this.availableTags = tagStubs.rows.map((r) => r.doc!).filter(Boolean);\n\n // Load saved preferences\n const state = await this.user.getStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY\n );\n\n if (state) {\n this.preferences.boost = { ...state.boost };\n } else {\n // No preferences yet - start fresh\n this.preferences.boost = {};\n }\n\n // Store saved state for comparison\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n // Initialize tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n // If saved value exceeds startingMax, expand range to accommodate it\n // (capped at absoluteMax)\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n } catch (e) {\n console.error('Failed to load preferences:', e);\n this.saveError = 'Failed to load preferences. Please try again.';\n } finally {\n this.loading = false;\n }\n },\n\n /**\n * Add a tag to preferences with default multiplier of 1.0\n */\n addTag(tagName: string | null) {\n if (tagName && !(tagName in this.preferences.boost)) {\n this.preferences.boost[tagName] = 1.0;\n this.tagMaxRanges[tagName] = this.sliderConfigResolved.startingMax;\n this.$emit('preferences-changed', this.preferences);\n }\n // Clear the autocomplete\n this.$nextTick(() => {\n this.tagToAdd = null;\n });\n },\n\n /**\n * Remove a tag from preferences\n */\n removeTag(tagName: string) {\n delete this.preferences.boost[tagName];\n delete this.tagMaxRanges[tagName];\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Update boost multiplier for a tag\n */\n updateBoost(tagName: string, value: number) {\n this.preferences.boost[tagName] = value;\n this.$emit('preferences-changed', this.preferences);\n },\n\n /**\n * Expand the global slider range by 1 and move the triggering tag's slider to new max\n */\n expandSliderRange(tagName: string) {\n const currentGlobalMax = this.globalSliderMax;\n if (currentGlobalMax < this.sliderConfigResolved.absoluteMax) {\n const newGlobalMax = currentGlobalMax + 1;\n\n // Expand all tag ranges to the new global max\n // (ensures all sliders stay synchronized)\n Object.keys(this.tagMaxRanges).forEach((tag) => {\n this.tagMaxRanges[tag] = Math.max(this.tagMaxRanges[tag], newGlobalMax);\n });\n\n // Move the triggering tag's slider to new max\n this.preferences.boost[tagName] = newGlobalMax;\n this.$emit('preferences-changed', this.preferences);\n }\n },\n\n /**\n * Format multiplier for display\n */\n formatMultiplier(value: number): string {\n if (value === 0) {\n return '0x';\n }\n if (value < 0.1) {\n return value.toFixed(2) + 'x';\n }\n return value.toFixed(1) + 'x';\n },\n\n /**\n * Reset to last saved state\n */\n resetToSaved() {\n this.preferences.boost = { ...this.savedPreferences.boost };\n this.saveSuccess = false;\n this.saveError = '';\n\n // Reset tag max ranges, auto-expanding if saved value exceeds startingMax\n this.tagMaxRanges = {};\n Object.keys(this.preferences.boost).forEach((tag) => {\n const savedValue = this.preferences.boost[tag];\n const startingMax = this.sliderConfigResolved.startingMax;\n\n if (savedValue > startingMax) {\n this.tagMaxRanges[tag] = Math.min(\n Math.ceil(savedValue),\n this.sliderConfigResolved.absoluteMax\n );\n } else {\n this.tagMaxRanges[tag] = startingMax;\n }\n });\n },\n\n /**\n * Save preferences to strategy state\n */\n async savePreferences() {\n if (!this.user || !this.courseId) {\n this.saveError = 'Unable to save - user or course not available';\n return;\n }\n\n this.saving = true;\n this.saveError = '';\n this.saveSuccess = false;\n\n try {\n const state: UserTagPreferenceState = {\n boost: { ...this.preferences.boost },\n updatedAt: new Date().toISOString(),\n };\n\n await this.user.putStrategyState<UserTagPreferenceState>(\n this.courseId,\n STRATEGY_KEY,\n state\n );\n\n // Update saved state\n this.savedPreferences.boost = { ...this.preferences.boost };\n\n this.saveSuccess = true;\n this.$emit('preferences-saved', state);\n } catch (e) {\n console.error('Failed to save preferences:', e);\n this.saveError = 'Failed to save preferences. Please try again.';\n } finally {\n this.saving = false;\n }\n },\n },\n});\n</script>\n\n<style scoped>\n/* Additional styles if needed */\n</style>\n"],"x_google_ignoreList":[26],"mappings":"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"}