299 lines
14 KiB
TypeScript
299 lines
14 KiB
TypeScript
import { Link } from '@tanstack/react-router'
|
|
import { useUser } from '@/hooks/use-auth'
|
|
import { useAuthModalStore } from '@/store/auth-modal-store'
|
|
|
|
const FEATURES = [
|
|
{
|
|
to: '/toeic',
|
|
icon: 'assignment',
|
|
title: 'Luyện đề',
|
|
accent: 'TOEIC',
|
|
desc: 'Kho đề thi cập nhật theo cấu trúc mới nhất. Phân tích điểm yếu từng Part.',
|
|
stat: '350+ câu hỏi',
|
|
},
|
|
{
|
|
to: '/writing',
|
|
icon: 'auto_fix_high',
|
|
title: 'AI chấm',
|
|
accent: 'Writing',
|
|
desc: 'Phản hồi tức thì về ngữ pháp, từ vựng, cấu trúc và bài viết mẫu.',
|
|
stat: '3 lượt / ngày',
|
|
},
|
|
{
|
|
to: '/flash-card',
|
|
icon: 'menu_book',
|
|
title: 'Từ vựng',
|
|
accent: 'thông minh',
|
|
desc: 'Bộ thẻ TOEIC với spaced-repetition, lật 3D, ảnh minh hoạ.',
|
|
stat: '18 000+ từ',
|
|
},
|
|
]
|
|
|
|
export function Home() {
|
|
const user = useUser()
|
|
const openModal = useAuthModalStore((s) => s.open)
|
|
const firstName = user?.name ?? 'bạn'
|
|
|
|
return (
|
|
<div className="px-6 lg:px-10 py-10 max-w-6xl mx-auto page-enter">
|
|
{/* Page head — editorial */}
|
|
<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">Học TOEIC cùng AI</div>
|
|
<h1 className="at-title text-4xl lg:text-[44px]">
|
|
Chào <i>{firstName}</i>,<br />
|
|
hôm nay học <i>15 phút</i>?
|
|
</h1>
|
|
<div className="mt-4 text-sm" style={{ color: 'var(--at-mute)' }}>
|
|
Mục tiêu <b style={{ color: 'var(--at-ink)' }}>850</b>
|
|
<span className="mx-2 inline-block w-[3px] h-[3px] rounded-full align-middle" style={{ background: 'var(--at-mute-2)' }} />
|
|
hiện tại <b style={{ color: 'var(--at-ink)' }}>720</b>
|
|
<span className="mx-2 inline-block w-[3px] h-[3px] rounded-full align-middle" style={{ background: 'var(--at-mute-2)' }} />
|
|
còn <b style={{ color: 'var(--at-brand)' }}>130 điểm</b> nữa
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2.5">
|
|
<Link
|
|
to="/flash-card"
|
|
className="inline-flex items-center gap-2 px-5 py-3 rounded-xl text-[13.5px] font-semibold transition-colors"
|
|
style={{ background: 'var(--at-surface)', border: '1px solid var(--at-line)', color: 'var(--at-ink-2)' }}
|
|
>
|
|
Học từ vựng
|
|
</Link>
|
|
<Link
|
|
to="/toeic"
|
|
className="inline-flex items-center gap-2 px-5 py-3 rounded-xl text-[13.5px] font-semibold transition-colors hover:opacity-90"
|
|
style={{ background: 'var(--at-ink)', color: 'var(--at-paper)', border: '1px solid var(--at-ink)' }}
|
|
>
|
|
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>play_arrow</span>
|
|
Tiếp tục học
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid lg:grid-cols-[2fr_1fr] gap-5">
|
|
{/* MAIN COL */}
|
|
<div className="flex flex-col gap-5 min-w-0">
|
|
{/* Progress hero */}
|
|
<div className="rounded-2xl p-7 flex flex-wrap items-center gap-7" style={{ background: 'var(--at-surface)', border: '1px solid var(--at-line)' }}>
|
|
<ProgressRing value={85} />
|
|
<div className="flex-1 min-w-[240px]">
|
|
<div className="at-eyebrow mb-1">Lộ trình</div>
|
|
<div className="at-serif text-[22px] leading-[1.2] tracking-tight mb-3" style={{ color: 'var(--at-ink)' }}>
|
|
Bạn đang đi <i style={{ color: 'var(--at-brand)', fontStyle: 'italic' }}>đúng hướng</i> — tuần này 4/7 ngày.
|
|
</div>
|
|
<div className="flex flex-wrap gap-4 items-stretch">
|
|
<Stat num="24" label="ngày còn lại" />
|
|
<div className="w-px self-stretch" style={{ background: 'var(--at-line)' }} />
|
|
<Stat num="+46" label="điểm tháng này" />
|
|
<div className="w-px self-stretch" style={{ background: 'var(--at-line)' }} />
|
|
<Stat num="68%" label="tỷ lệ đúng" color="var(--at-good)" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Feature cards */}
|
|
<div className="rounded-2xl p-6" style={{ background: 'var(--at-surface)', border: '1px solid var(--at-line)' }}>
|
|
<div className="at-eyebrow mb-1">Khám phá</div>
|
|
<h2 className="at-serif text-[22px] tracking-tight mb-5" style={{ color: 'var(--at-ink)', fontWeight: 500 }}>
|
|
Tính năng <i style={{ color: 'var(--at-brand)', fontStyle: 'italic' }}>nổi bật</i>
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
{FEATURES.map((f) => (
|
|
<Link
|
|
key={f.to}
|
|
to={f.to}
|
|
className="rounded-xl p-4 transition-all hover:-translate-y-0.5"
|
|
style={{ background: 'var(--at-paper-2)', border: '1px solid var(--at-line)' }}
|
|
>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div
|
|
className="w-9 h-9 rounded-lg grid place-items-center"
|
|
style={{ background: 'var(--at-brand-soft)', color: 'var(--at-brand)' }}
|
|
>
|
|
<span className="material-symbols-outlined" style={{ fontSize: 18 }}>{f.icon}</span>
|
|
</div>
|
|
<span className="at-chip at-chip-brand">
|
|
<span className="at-chip-dot" />
|
|
{f.stat}
|
|
</span>
|
|
</div>
|
|
<div className="at-serif text-[17px] leading-[1.15] tracking-tight mb-1" style={{ color: 'var(--at-ink)', fontWeight: 500 }}>
|
|
{f.title} <i style={{ color: 'var(--at-brand)', fontStyle: 'italic' }}>{f.accent}</i>
|
|
</div>
|
|
<div className="text-[12.5px] leading-[1.5]" style={{ color: 'var(--at-mute)' }}>{f.desc}</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 7-day journey */}
|
|
<div className="rounded-2xl p-6" style={{ background: 'var(--at-surface)', border: '1px solid var(--at-line)' }}>
|
|
<div className="flex justify-between items-end mb-4">
|
|
<div>
|
|
<div className="at-eyebrow mb-1">Tuần này</div>
|
|
<div className="at-serif text-[20px] tracking-tight" style={{ color: 'var(--at-ink)', fontWeight: 500 }}>
|
|
Lộ trình <i style={{ color: 'var(--at-brand)', fontStyle: 'italic' }}>7 ngày</i>
|
|
</div>
|
|
</div>
|
|
<span className="at-chip at-chip-good">
|
|
<span className="at-chip-dot" />
|
|
+24% so với tuần trước
|
|
</span>
|
|
</div>
|
|
<div className="grid grid-cols-7 gap-2.5">
|
|
{['T2', 'T3', 'T4', 'T5', 'T6', 'T7', 'CN'].map((d, i) => {
|
|
const h = [60, 85, 40, 90, 75, 0, 0][i]
|
|
const done = h > 0
|
|
const today = i === 4
|
|
return (
|
|
<div key={d} className="flex flex-col items-center gap-2">
|
|
<div className="w-full relative overflow-hidden rounded-[10px]" style={{ height: 96, background: 'var(--at-line-2)' }}>
|
|
<div
|
|
className="absolute left-0 right-0 bottom-0 rounded-[10px] transition-[height] duration-500"
|
|
style={{
|
|
height: `${h}%`,
|
|
background: today ? 'var(--at-brand)' : done ? 'var(--at-brand-soft)' : 'var(--at-line-2)',
|
|
}}
|
|
/>
|
|
</div>
|
|
<div
|
|
className={today ? 'at-serif italic' : ''}
|
|
style={{ fontSize: 11, color: today ? 'var(--at-brand)' : 'var(--at-mute)', fontWeight: today ? 700 : 500 }}
|
|
>
|
|
{d}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SIDE COL */}
|
|
<div className="flex flex-col gap-5">
|
|
{/* Streak card (inky) */}
|
|
<div className="rounded-2xl p-5" style={{ background: 'var(--at-ink)', color: 'var(--at-paper)' }}>
|
|
<div className="flex items-center justify-between mb-3.5">
|
|
<div style={{ fontSize: 10, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'rgba(250,248,243,0.55)', fontWeight: 600 }}>
|
|
Streak
|
|
</div>
|
|
<div className="w-10 h-10 rounded-xl grid place-items-center" style={{ background: 'rgba(255,255,255,0.08)', color: '#FFC27A' }}>
|
|
<span className="material-symbols-outlined" style={{ fontSize: 20, fontVariationSettings: "'FILL' 1" }}>local_fire_department</span>
|
|
</div>
|
|
</div>
|
|
<div className="at-serif" style={{ fontSize: 44, fontWeight: 400, letterSpacing: '-0.03em', lineHeight: 1, marginBottom: 4 }}>
|
|
7 <span className="italic opacity-65" style={{ fontSize: 18 }}>ngày</span>
|
|
</div>
|
|
<div style={{ fontSize: 12, color: 'rgba(250,248,243,0.55)', marginBottom: 14 }}>Kỷ lục: 21 ngày</div>
|
|
<div className="flex gap-1.5">
|
|
{['T2', 'T3', 'T4', 'T5', 'T6', 'T7', 'CN'].map((d, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex-1 rounded-md grid place-items-center"
|
|
style={{
|
|
height: 24,
|
|
background: i < 5 ? '#C15A34' : 'rgba(255,255,255,0.08)',
|
|
color: i < 5 ? 'white' : 'rgba(255,255,255,0.4)',
|
|
fontSize: 9,
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
{d}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* AI nudge */}
|
|
<div className="at-pullquote">
|
|
<div className="flex items-center gap-2 mb-2.5">
|
|
<div className="w-6 h-6 rounded-lg grid place-items-center" style={{ background: 'var(--at-brand)', color: 'white' }}>
|
|
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>auto_awesome</span>
|
|
</div>
|
|
<div style={{ fontSize: 10, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--at-brand-ink)', fontWeight: 700 }}>
|
|
AI gợi ý
|
|
</div>
|
|
</div>
|
|
<div className="at-pullquote-q">
|
|
"Bạn yếu nhất <b style={{ fontWeight: 600 }}>Part 3</b> — dành 10 phút hôm nay có thể tăng <b style={{ fontWeight: 600 }}>30+ điểm</b>."
|
|
</div>
|
|
<div className="mt-2.5 text-[11px] opacity-70" style={{ color: 'var(--at-brand-ink)' }}>— EnglishAI Coach</div>
|
|
</div>
|
|
|
|
{/* Pro tip */}
|
|
<div className="at-tip">
|
|
<div className="at-tip-label">Pro tip</div>
|
|
<div className="text-[12.5px] leading-[1.55]" style={{ color: 'var(--at-ink-2)' }}>
|
|
Học theo <b style={{ color: 'var(--at-warm)', fontWeight: 700 }}>cụm từ</b> (collocations) giúp bạn ghi nhớ nhanh hơn{' '}
|
|
<b style={{ color: 'var(--at-warm)', fontWeight: 700 }}>40%</b> so với học từ đơn lẻ.
|
|
</div>
|
|
</div>
|
|
|
|
{/* Guest CTA (only if not logged in) */}
|
|
{!user && (
|
|
<div className="rounded-2xl p-5" style={{ background: 'var(--at-surface)', border: '1px solid var(--at-line)' }}>
|
|
<div className="at-eyebrow mb-2">Khách</div>
|
|
<div className="at-serif text-[17px] leading-[1.2] tracking-tight mb-3" style={{ color: 'var(--at-ink)', fontWeight: 500 }}>
|
|
Đăng ký để <i style={{ color: 'var(--at-brand)', fontStyle: 'italic' }}>lưu tiến độ</i>.
|
|
</div>
|
|
<button
|
|
onClick={() => openModal('register')}
|
|
className="w-full py-2.5 rounded-xl text-[13.5px] font-semibold transition-opacity hover:opacity-90"
|
|
style={{ background: 'var(--at-ink)', color: 'var(--at-paper)' }}
|
|
>
|
|
Đăng ký miễn phí
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Stat({ num, label, color }: { num: string; label: string; color?: string }) {
|
|
return (
|
|
<div>
|
|
<div className="at-serif" style={{ fontSize: 26, fontWeight: 400, letterSpacing: '-0.02em', lineHeight: 1, color: color ?? 'var(--at-ink)' }}>
|
|
{num}
|
|
</div>
|
|
<div style={{ fontSize: 11, color: 'var(--at-mute)', marginTop: 4 }}>{label}</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ProgressRing({ value }: { value: number }) {
|
|
const r = 58
|
|
const c = 2 * Math.PI * r
|
|
const offset = c - (value / 100) * c
|
|
return (
|
|
<div className="relative grid place-items-center" style={{ width: 132, height: 132 }}>
|
|
<svg width="132" height="132">
|
|
<circle cx="66" cy="66" r={r} fill="none" stroke="var(--at-line-2)" strokeWidth="7" />
|
|
<circle
|
|
cx="66"
|
|
cy="66"
|
|
r={r}
|
|
fill="none"
|
|
stroke="var(--at-brand)"
|
|
strokeWidth="7"
|
|
strokeDasharray={c}
|
|
strokeDashoffset={offset}
|
|
strokeLinecap="round"
|
|
transform="rotate(-90 66 66)"
|
|
style={{ transition: 'stroke-dashoffset 0.6s cubic-bezier(0.2, 0.7, 0.2, 1)' }}
|
|
/>
|
|
</svg>
|
|
<div className="absolute text-center">
|
|
<div className="at-serif" style={{ fontSize: 34, fontWeight: 400, letterSpacing: '-0.025em', lineHeight: 1, color: 'var(--at-ink)' }}>
|
|
720
|
|
</div>
|
|
<div style={{ fontSize: 10, color: 'var(--at-mute)', textTransform: 'uppercase', letterSpacing: '0.12em', marginTop: 4, fontWeight: 600 }}>
|
|
/ 850
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|