update flash card, test

This commit is contained in:
2026-04-15 00:41:02 +07:00
parent 4bc39225ab
commit 088c555515
32 changed files with 1988 additions and 415 deletions

View File

@@ -1,35 +1,96 @@
import { useQuery } from "@tanstack/react-query"
import { supabase } from "@/lib/supabase"
import type { Question } from "@/types"
import type { Question, SessionPart } from "@/types"
const ANSWER_INDEX: Record<string, number> = { A: 0, B: 1, C: 2, D: 3 }
type AnswerChoiceRow = { value: string; label_text: string | null; is_correct: boolean }
type QuestionRow = { id: number; question_text: string | null; explanation: string | null; group_id: number; answer_choice: AnswerChoiceRow[] }
type GroupRow = { id: number; part_id: number; audio_url: string | null; image_url: string | null; passage_text: string | null }
type PartRow = { id: number; part_number: number }
// Maps a Supabase row to the shared Question interface.
// DB uses `content` + `answer` ('A''D'); interface uses `text` + `correctAnswer` (03).
function rowToQuestion(row: Record<string, unknown>): Question {
function buildOptions(choices: AnswerChoiceRow[]): string[] {
return [...choices].sort((a, b) => a.value.localeCompare(b.value)).map(c => c.label_text ?? '')
}
function getCorrectIndex(choices: AnswerChoiceRow[]): number {
const sorted = [...choices].sort((a, b) => a.value.localeCompare(b.value))
const idx = sorted.findIndex(c => c.is_correct)
return idx >= 0 ? idx : 0
}
function rowToQuestion(row: QuestionRow, group: GroupRow, partNumber: number): Question {
return {
id: row.id as string,
part: row.part as number,
text: row.content as string,
options: row.options as string[],
correctAnswer: ANSWER_INDEX[(row.answer as string).toUpperCase()] ?? 0,
explanation: (row.explanation as string) ?? '',
id: row.id,
partNumber,
text: row.question_text,
options: buildOptions(row.answer_choice),
correctAnswer: getCorrectIndex(row.answer_choice),
explanation: row.explanation,
groupId: row.group_id,
audioUrl: group.audio_url ?? undefined,
imageUrl: group.image_url ?? undefined,
passageText: group.passage_text ?? undefined,
}
}
// Exported for imperative use (e.g. ToeicPractice click handler).
// part=0 fetches all parts (Full Test).
export async function fetchQuestions(part: number, limit = 10): Promise<Question[]> {
let query = supabase.from('questions').select('*').limit(limit)
if (part > 0) query = query.eq('part', part)
const { data, error } = await query
if (error) throw error
return (data ?? []).map(rowToQuestion)
}
/**
* Fetch all questions for a test, optionally filtered to specific part numbers.
* partNumbers=[] or undefined → fetch all parts of the test.
* Returns questions grouped into SessionPart[] ordered by part_number.
*/
export async function fetchQuestionsForTest(
testId: number,
partNumbers?: number[],
): Promise<SessionPart[]> {
// Step 1: Get parts for this test
let partsQuery = supabase.from('part').select('id, part_number, title').eq('test_id', testId).order('part_number')
if (partNumbers?.length) partsQuery = partsQuery.in('part_number', partNumbers)
const { data: parts, error: partsError } = await partsQuery
if (partsError) throw partsError
if (!parts?.length) return []
export function useQuestions(part: number, limit = 10) {
return useQuery({
queryKey: ['questions', part, limit],
queryFn: () => fetchQuestions(part, limit),
})
const partRows = parts as (PartRow & { title: string })[]
const partIds = partRows.map(p => p.id)
const partNumberById = new Map(partRows.map(p => [p.id, p.part_number]))
const partTitleByNumber = new Map(partRows.map(p => [p.part_number, p.title]))
// Step 2: Get question_groups for those parts
const { data: groups, error: groupsError } = await supabase
.from('question_group')
.select('id, part_id, audio_url, image_url, passage_text')
.in('part_id', partIds)
if (groupsError) throw groupsError
if (!groups?.length) return []
const groupMap = new Map<number, GroupRow>((groups as GroupRow[]).map(g => [g.id, g]))
const groupIds = (groups as GroupRow[]).map(g => g.id)
// Step 3: Get questions with answer choices
const { data: rows, error } = await supabase
.from('question')
.select('id, question_text, explanation, group_id, answer_choice(value, label_text, is_correct)')
.in('group_id', groupIds)
.order('question_number')
if (error) throw error
const questions = (rows as QuestionRow[] ?? [])
.map(row => {
const group = groupMap.get(row.group_id)!
const partNumber = partNumberById.get(group.part_id)!
return rowToQuestion(row, group, partNumber)
})
.filter(q => q.options.length > 0)
// Group into SessionPart[] ordered by partNumber
const byPart = new Map<number, Question[]>()
for (const q of questions) {
if (!byPart.has(q.partNumber)) byPart.set(q.partNumber, [])
byPart.get(q.partNumber)!.push(q)
}
return partRows
.filter(p => byPart.has(p.part_number))
.map(p => ({
partNumber: p.part_number,
partName: partTitleByNumber.get(p.part_number) ?? `Part ${p.part_number}`,
questions: byPart.get(p.part_number)!,
}))
}