Public Access
- Implemented collection repository with CRUD operations for user collections. - Created routes for managing collections, including listing, creating, updating, and deleting collections. - Added sharing functionality for exams, allowing users to share and clone exams. - Developed a study coach service that analyzes exam results and provides study tips based on weak areas. - Introduced frontend components for managing collections, sharing exams, and displaying study coach insights. - Enhanced user experience with loading states and error handling in shared components.
189 lines
6.4 KiB
TypeScript
189 lines
6.4 KiB
TypeScript
import { getDb } from '../db/database.js'
|
|
|
|
export interface DbExam {
|
|
id: number
|
|
user_id: number
|
|
title: string
|
|
subtitle: string | null
|
|
exam_json: string
|
|
created_at: number
|
|
updated_at: number
|
|
share_token: string | null
|
|
is_public: number
|
|
is_favorite: number
|
|
tags: string | null
|
|
share_mode: string | null
|
|
cloned_from_share_token: string | null
|
|
}
|
|
|
|
export interface ExamListItem {
|
|
id: number
|
|
title: string
|
|
subtitle: string | null
|
|
created_at: number
|
|
updated_at: number
|
|
question_count: number
|
|
share_token: string | null
|
|
is_favorite: number
|
|
tags: string
|
|
collection_id: number | null
|
|
}
|
|
|
|
export function createExam(userId: number, title: string, subtitle: string | null, examJson: string, tagsJson: string = '[]'): { id: number } {
|
|
const db = getDb()
|
|
const result = db.prepare(`
|
|
INSERT INTO saved_exams (user_id, title, subtitle, exam_json, tags)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(userId, title, subtitle, examJson, tagsJson)
|
|
return { id: result.lastInsertRowid as number }
|
|
}
|
|
|
|
export function getExamById(id: number, userId: number): DbExam | undefined {
|
|
const db = getDb()
|
|
return db.prepare('SELECT * FROM saved_exams WHERE id = ? AND user_id = ?').get(id, userId) as DbExam | undefined
|
|
}
|
|
|
|
export function listExamsByUser(userId: number): ExamListItem[] {
|
|
const db = getDb()
|
|
return db.prepare(`
|
|
SELECT id, title, subtitle, created_at, updated_at, share_token, is_favorite, tags, collection_id,
|
|
json_array_length(json_extract(exam_json, '$.questions')) AS question_count
|
|
FROM saved_exams
|
|
WHERE user_id = ?
|
|
ORDER BY is_favorite DESC, updated_at DESC
|
|
`).all(userId) as ExamListItem[]
|
|
}
|
|
|
|
export function updateExam(id: number, userId: number, title: string, subtitle: string | null, examJson: string, tagsJson: string | null = null): boolean {
|
|
const db = getDb()
|
|
if (tagsJson !== null) {
|
|
const result = db.prepare(`
|
|
UPDATE saved_exams
|
|
SET title = ?, subtitle = ?, exam_json = ?, tags = ?, updated_at = unixepoch()
|
|
WHERE id = ? AND user_id = ?
|
|
`).run(title, subtitle, examJson, tagsJson, id, userId)
|
|
return result.changes > 0
|
|
}
|
|
const result = db.prepare(`
|
|
UPDATE saved_exams
|
|
SET title = ?, subtitle = ?, exam_json = ?, updated_at = unixepoch()
|
|
WHERE id = ? AND user_id = ?
|
|
`).run(title, subtitle, examJson, id, userId)
|
|
return result.changes > 0
|
|
}
|
|
|
|
export function deleteExam(id: number, userId: number): boolean {
|
|
const db = getDb()
|
|
const result = db.prepare('DELETE FROM saved_exams WHERE id = ? AND user_id = ?').run(id, userId)
|
|
return result.changes > 0
|
|
}
|
|
|
|
export function countExams(): number {
|
|
const db = getDb()
|
|
const row = db.prepare('SELECT COUNT(*) AS cnt FROM saved_exams').get() as { cnt: number }
|
|
return row.cnt
|
|
}
|
|
|
|
export function setShareToken(id: number, userId: number, token: string, mode: string = 'clone'): boolean {
|
|
const db = getDb()
|
|
const result = db.prepare(`
|
|
UPDATE saved_exams
|
|
SET share_token = ?, is_public = 1, share_mode = ?, updated_at = unixepoch()
|
|
WHERE id = ? AND user_id = ?
|
|
`).run(token, mode, id, userId)
|
|
return result.changes > 0
|
|
}
|
|
|
|
export function clearShareToken(id: number, userId: number): boolean {
|
|
const db = getDb()
|
|
const result = db.prepare(`
|
|
UPDATE saved_exams
|
|
SET share_token = NULL, is_public = 0, share_mode = 'clone', updated_at = unixepoch()
|
|
WHERE id = ? AND user_id = ?
|
|
`).run(id, userId)
|
|
return result.changes > 0
|
|
}
|
|
|
|
export function getExamByShareToken(token: string): DbExam | undefined {
|
|
const db = getDb()
|
|
return db.prepare('SELECT * FROM saved_exams WHERE share_token = ? AND is_public = 1').get(token) as DbExam | undefined
|
|
}
|
|
|
|
export function setShareMode(id: number, userId: number, mode: string): boolean {
|
|
const db = getDb()
|
|
const result = db.prepare(`
|
|
UPDATE saved_exams
|
|
SET share_mode = ?, updated_at = unixepoch()
|
|
WHERE id = ? AND user_id = ?
|
|
`).run(mode, id, userId)
|
|
return result.changes > 0
|
|
}
|
|
|
|
export function cloneExam(userId: number, sourceExam: DbExam, shareToken: string): { id: number } {
|
|
const db = getDb()
|
|
const parsed = JSON.parse(sourceExam.exam_json)
|
|
parsed.id = undefined
|
|
const result = db.prepare(`
|
|
INSERT INTO saved_exams (user_id, title, subtitle, exam_json, cloned_from_share_token)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(userId, sourceExam.title + ' (copy)', sourceExam.subtitle, JSON.stringify(parsed), shareToken)
|
|
return { id: result.lastInsertRowid as number }
|
|
}
|
|
|
|
export interface ClonedExamItem {
|
|
id: number
|
|
title: string
|
|
subtitle: string | null
|
|
created_at: number
|
|
updated_at: number
|
|
question_count: number
|
|
cloned_from_share_token: string
|
|
}
|
|
|
|
export function listClonedExams(userId: number): ClonedExamItem[] {
|
|
const db = getDb()
|
|
return db.prepare(`
|
|
SELECT id, title, subtitle, created_at, updated_at, cloned_from_share_token,
|
|
json_array_length(json_extract(exam_json, '$.questions')) AS question_count
|
|
FROM saved_exams
|
|
WHERE user_id = ? AND cloned_from_share_token IS NOT NULL
|
|
ORDER BY updated_at DESC
|
|
`).all(userId) as ClonedExamItem[]
|
|
}
|
|
|
|
export function setFavorite(id: number, userId: number, isFavorite: boolean): boolean {
|
|
const db = getDb()
|
|
const result = db.prepare(`
|
|
UPDATE saved_exams
|
|
SET is_favorite = ?, updated_at = unixepoch()
|
|
WHERE id = ? AND user_id = ?
|
|
`).run(isFavorite ? 1 : 0, id, userId)
|
|
return result.changes > 0
|
|
}
|
|
|
|
export function getExamRawJson(id: number, userId: number): { title: string; exam_json: string } | undefined {
|
|
const db = getDb()
|
|
return db.prepare('SELECT title, exam_json FROM saved_exams WHERE id = ? AND user_id = ?').get(id, userId) as { title: string; exam_json: string } | undefined
|
|
}
|
|
|
|
export function duplicateOwnExam(userId: number, sourceExam: DbExam): { id: number } {
|
|
const db = getDb()
|
|
const parsed = JSON.parse(sourceExam.exam_json)
|
|
parsed.id = undefined
|
|
const result = db.prepare(`
|
|
INSERT INTO saved_exams (user_id, title, subtitle, exam_json, tags)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(userId, sourceExam.title + ' (copy)', sourceExam.subtitle, JSON.stringify(parsed), sourceExam.tags ?? '[]')
|
|
return { id: result.lastInsertRowid as number }
|
|
}
|
|
|
|
export function setExamCollection(id: number, userId: number, collectionId: number | null): boolean {
|
|
const db = getDb()
|
|
const result = db.prepare(`
|
|
UPDATE saved_exams
|
|
SET collection_id = ?, updated_at = unixepoch()
|
|
WHERE id = ? AND user_id = ?
|
|
`).run(collectionId, id, userId)
|
|
return result.changes > 0
|
|
}
|