update flash card, test
This commit is contained in:
@@ -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` (0–3).
|
||||
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)!,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user