This commit is contained in:
2026-04-18 23:16:52 +07:00
parent 3e0b3f6a6d
commit 309609fccb
32 changed files with 2261 additions and 1030 deletions

View File

@@ -3,7 +3,7 @@ import { useNavigate } from '@tanstack/react-router'
import { CircularProgress } from '@/components/CircularProgress'
import { useTestStore } from '@/store/test-store'
import { TOEIC_PARTS } from '@/temp/local-data'
import { fetchQuestions } from '@/hooks/use-questions'
import { fetchQuestionsForTest } from '@/hooks/use-questions'
import { useRequireAuth } from '@/hooks/use-require-auth'
export function ToeicPractice() {
@@ -16,8 +16,9 @@ export function ToeicPractice() {
if (!requireAuth()) return
setLoadingPartId(partId)
try {
const questions = await fetchQuestions(partId, 10)
startExam(partId, partName, questions)
// TODO: replace hardcoded testId=1 with real test selection
const parts = await fetchQuestionsForTest(1, [partId])
startExam({ testId: 1, testName: partName, parts, totalSeconds: 0 })
navigate({ to: '/toeic/session' })
} catch (err) {
console.error('Failed to load questions:', err)

View File

@@ -10,66 +10,96 @@ export function ToeicTestList() {
})
return (
<div className="px-6 py-8 max-w-6xl mx-auto page-enter">
<div className="mb-8">
<h1 className="text-3xl font-extrabold text-slate-800 mb-2">Đ Thi TOEIC</h1>
<p className="text-slate-500">Chọn đ thi đ bắt đu luyện tập hoặc thi thử toàn bộ.</p>
<div className="px-6 lg:px-10 py-10 max-w-6xl mx-auto page-enter">
{/* Editorial head */}
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6 mb-10">
<div>
<div className="at-eyebrow mb-3">Luyện đ</div>
<h1 className="at-title text-4xl lg:text-[44px]">
TOEIC <i>Mock Tests</i>
</h1>
<p className="mt-4 text-sm" style={{ color: 'var(--at-mute)' }}>
Chọn đ đ bắt đu luyện tập {tests.length} đ thi
</p>
</div>
</div>
{isLoading && (
<div className="grid grid-cols-2 lg:grid-cols-3 gap-5">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="bg-white rounded-2xl border border-slate-200 p-5 animate-pulse h-44" />
<div
key={i}
className="rounded-2xl h-44 animate-pulse"
style={{ background: 'var(--at-surface)', border: '1px solid var(--at-line)' }}
/>
))}
</div>
)}
{error && (
<div className="bg-red-50 border border-red-100 rounded-2xl p-6 text-red-600 text-sm">
<div
className="rounded-2xl p-6 text-sm"
style={{ background: 'var(--at-bad-soft)', border: '1px solid rgba(193,68,62,0.2)', color: 'var(--at-bad)' }}
>
Không thể tải danh sách đ thi. Vui lòng thử lại.
</div>
)}
{!isLoading && !error && tests.length === 0 && (
<div className="text-center py-20 text-slate-400">
<span className="material-symbols-outlined" style={{ fontSize: 48 }}>library_books</span>
<p className="mt-3 font-medium">Chưa đ thi nào. Dữ liệu đang đưc cập nhật.</p>
<div
className="rounded-2xl p-16 text-center"
style={{ background: 'var(--at-surface)', border: '1px solid var(--at-line)' }}
>
<span className="material-symbols-outlined mb-3 block" style={{ fontSize: 48, color: 'var(--at-mute-2)' }}>
library_books
</span>
<p className="at-serif text-lg" style={{ color: 'var(--at-ink)' }}>Chưa đ thi nào.</p>
<p className="text-sm mt-1" style={{ color: 'var(--at-mute)' }}>Dữ liệu đang đưc cập nhật.</p>
</div>
)}
{tests.length > 0 && (
<div className="grid grid-cols-2 lg:grid-cols-3 gap-5">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{tests.map((test) => (
<div
key={test.id}
className="bg-white rounded-2xl border border-slate-200 p-5 flex flex-col shadow-sm hover:-translate-y-1 hover:shadow-md transition-all duration-200"
className="rounded-2xl p-6 flex flex-col transition-all hover:-translate-y-1"
style={{ background: 'var(--at-surface)', border: '1px solid var(--at-line)' }}
>
{/* Category badge */}
{test.categoryName && (
<span className="self-start text-xs font-bold px-2.5 py-1 rounded-full bg-blue-50 text-blue-600 border border-blue-100 mb-3">
<span className="at-chip at-chip-brand self-start mb-3">
<span className="at-chip-dot" />
{test.categoryName}
</span>
)}
<h3 className="font-extrabold text-lg text-slate-800 mb-1 leading-snug">{test.title}</h3>
<h3
className="at-serif text-[20px] leading-[1.2] tracking-tight mb-2"
style={{ color: 'var(--at-ink)', fontWeight: 500 }}
>
{test.title}
</h3>
{test.description && (
<p className="text-xs text-slate-400 mb-3 line-clamp-2">{test.description}</p>
<p className="text-xs leading-[1.5] mb-3 line-clamp-2" style={{ color: 'var(--at-mute)' }}>
{test.description}
</p>
)}
<div className="flex items-center gap-3 text-xs text-slate-500 mt-auto mb-4">
<div className="flex items-center gap-4 text-xs mt-auto mb-4" style={{ color: 'var(--at-mute)' }}>
<span className="flex items-center gap-1">
<span className="material-symbols-outlined" style={{ fontSize: 14 }}>list_alt</span>
{test.totalQuestions} câu
<span className="material-symbols-outlined" style={{ fontSize: 13 }}>list_alt</span>
<b className="tabular-nums" style={{ color: 'var(--at-ink)' }}>{test.totalQuestions}</b> câu
</span>
<span className="flex items-center gap-1">
<span className="material-symbols-outlined" style={{ fontSize: 14 }}>timer</span>
{test.durationMinutes} phút
<span className="material-symbols-outlined" style={{ fontSize: 13 }}>timer</span>
<b className="tabular-nums" style={{ color: 'var(--at-ink)' }}>{test.durationMinutes}</b> phút
</span>
</div>
<button
onClick={() => navigate({ to: '/toeic/$testId', params: { testId: String(test.id) } })}
className="w-full py-2.5 bg-blue-600 text-white rounded-xl text-sm font-semibold hover:bg-blue-700 transition-colors"
className="w-full py-2.5 rounded-xl text-[13px] font-semibold transition-opacity hover:opacity-90"
style={{ background: 'var(--at-ink)', color: 'var(--at-paper)' }}
>
Bắt đu
</button>