phase 2
This commit is contained in:
@@ -1,25 +1,108 @@
|
||||
const PARTS = [
|
||||
{ id: 1, name: "Part 1", desc: "Photographs" },
|
||||
{ id: 2, name: "Part 2", desc: "Question-Response" },
|
||||
{ id: 3, name: "Part 3", desc: "Conversations" },
|
||||
{ id: 4, name: "Part 4", desc: "Short Talks" },
|
||||
{ id: 5, name: "Part 5", desc: "Incomplete Sentences" },
|
||||
{ id: 6, name: "Part 6", desc: "Text Completion" },
|
||||
{ id: 7, name: "Part 7", desc: "Reading Comprehension" },
|
||||
]
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { CircularProgress } from '@/components/CircularProgress'
|
||||
import { useTestStore } from '@/store/test-store'
|
||||
import { TOEIC_PARTS } from '@/data/mock-data'
|
||||
import { fetchQuestions } from '@/hooks/use-questions'
|
||||
import { useRequireAuth } from '@/hooks/use-require-auth'
|
||||
|
||||
export function ToeicPractice() {
|
||||
const navigate = useNavigate()
|
||||
const { startExam } = useTestStore()
|
||||
const [loadingPartId, setLoadingPartId] = useState<number | null>(null)
|
||||
const { requireAuth } = useRequireAuth()
|
||||
|
||||
async function handleSelectPart(partId: number, partName: string) {
|
||||
if (!requireAuth()) return
|
||||
setLoadingPartId(partId)
|
||||
try {
|
||||
const questions = await fetchQuestions(partId, 10)
|
||||
startExam(partId, partName, questions)
|
||||
navigate({ to: '/toeic/session' })
|
||||
} catch (err) {
|
||||
console.error('Failed to load questions:', err)
|
||||
} finally {
|
||||
setLoadingPartId(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold">Luyện đề TOEIC</h1>
|
||||
<p className="text-gray-600">Chọn Part để bắt đầu luyện tập</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{PARTS.map((part) => (
|
||||
<div key={part.id} className="rounded-lg border p-4 cursor-pointer hover:bg-gray-50">
|
||||
<div className="font-semibold">{part.name}</div>
|
||||
<div className="text-sm text-gray-500">{part.desc}</div>
|
||||
</div>
|
||||
<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">Chọn Part TOEIC</h1>
|
||||
<p className="text-slate-500">
|
||||
Hệ thống ôn luyện theo cấu trúc bài thi TOEIC thực tế. Chọn phần cụ thể để bắt đầu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
{TOEIC_PARTS.map((part) => (
|
||||
<button
|
||||
key={part.id}
|
||||
onClick={() => handleSelectPart(part.id, part.nameVi)}
|
||||
disabled={loadingPartId !== null}
|
||||
className="bg-white rounded-2xl p-5 border border-slate-200 text-left hover:-translate-y-1 hover:shadow-md transition-all duration-200 group disabled:opacity-70 disabled:cursor-wait"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-5">
|
||||
<div className="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center group-hover:bg-blue-600 transition-colors">
|
||||
{loadingPartId === part.id ? (
|
||||
<span className="w-4 h-4 border-2 border-blue-300 border-t-blue-600 rounded-full animate-spin" />
|
||||
) : (
|
||||
<span
|
||||
className="material-symbols-outlined text-blue-600 group-hover:text-white transition-colors"
|
||||
style={{ fontSize: 18 }}
|
||||
>
|
||||
{part.icon}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<CircularProgress percent={part.progressPercent} size={44} />
|
||||
</div>
|
||||
<div className="font-extrabold text-lg text-slate-800 mb-0.5">{part.name}</div>
|
||||
<div className="text-sm font-semibold text-slate-700 mb-2">{part.nameVi}</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-400">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: 14 }}>list_alt</span>
|
||||
{part.questionCount} câu hỏi
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Full Test card */}
|
||||
<button
|
||||
onClick={() => handleSelectPart(0, 'Full Test')}
|
||||
className="relative rounded-2xl p-5 text-left overflow-hidden hover:-translate-y-1 hover:shadow-xl transition-all duration-200"
|
||||
style={{ background: 'linear-gradient(135deg, #f59e0b, #d97706)' }}
|
||||
>
|
||||
<div className="absolute top-0 right-0 opacity-10">
|
||||
<span className="material-symbols-outlined text-white" style={{ fontSize: 80 }}>
|
||||
workspace_premium
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center mb-5">
|
||||
<span className="material-symbols-outlined text-white" style={{ fontSize: 18 }}>
|
||||
military_tech
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-extrabold text-2xl text-white mb-0.5">Full Test</div>
|
||||
<div className="text-sm font-semibold text-amber-50 mb-2">Mô phỏng thi thật 2h</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-amber-100">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: 14 }}>timer</span>
|
||||
120 phút · 200 câu
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tip */}
|
||||
<div className="mt-8 bg-blue-50 border border-blue-100 rounded-2xl p-5 flex items-start gap-4">
|
||||
<span className="material-symbols-outlined text-blue-600 flex-shrink-0 mt-0.5">tips_and_updates</span>
|
||||
<div>
|
||||
<div className="font-semibold text-blue-700 text-sm mb-1">Mẹo luyện thi</div>
|
||||
<p className="text-slate-500 text-sm">
|
||||
Bắt đầu từ <strong>Part 5 (Điền từ)</strong> — phần mang lại điểm nhanh nhất vì không phụ thuộc kỹ năng nghe. Mỗi ngày 20 câu, sau 2 tuần bạn sẽ thấy cải thiện rõ rệt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user