From 0235ea7fa7c5323397131972fbd033aff78af259 Mon Sep 17 00:00:00 2001 From: ngochoa203 Date: Fri, 9 Jan 2026 05:34:59 +0700 Subject: [PATCH 1/8] fix ui responsive --- .../[courseId]/lessons/[lessonId]/page.tsx | 10 -- frontend/src/app/user/my-courses/page.tsx | 16 +- .../user/my-quizzes/[quizId]/play/page.tsx | 57 ++++--- frontend/src/app/user/my-quizzes/page.tsx | 26 ++-- frontend/src/app/user/ranking/page.tsx | 142 +++++++++--------- frontend/src/app/user/tracking/page.tsx | 38 ++--- frontend/src/components/auth/SignInForm.tsx | 16 +- frontend/src/components/auth/SignUpForm.tsx | 8 +- frontend/src/components/layout/AuthLayout.tsx | 14 +- frontend/src/components/user/Dashboard.tsx | 96 ++++++------ frontend/src/components/user/ProfilePage.tsx | 51 ++++--- .../components/user/courses/CourseCard.tsx | 52 +++---- .../components/user/create-course/Stepper.tsx | 8 +- .../user/dashboard/ActivityHeatmap.tsx | 12 +- .../components/user/dashboard/Leaderboard.tsx | 38 ++--- .../components/user/dashboard/ProfileCard.tsx | 8 +- .../components/user/dashboard/StatsGrid.tsx | 12 +- .../user/generation/GenerationCard.tsx | 18 +-- .../quiz-generation/QuizGenerationCard.tsx | 18 +-- .../src/components/user/quizzes/QuizCard.tsx | 60 +++++--- 20 files changed, 358 insertions(+), 342 deletions(-) diff --git a/frontend/src/app/user/my-courses/[courseId]/lessons/[lessonId]/page.tsx b/frontend/src/app/user/my-courses/[courseId]/lessons/[lessonId]/page.tsx index f1a9261d..cb388a32 100644 --- a/frontend/src/app/user/my-courses/[courseId]/lessons/[lessonId]/page.tsx +++ b/frontend/src/app/user/my-courses/[courseId]/lessons/[lessonId]/page.tsx @@ -761,16 +761,6 @@ export default function LessonDetailPage({ params }: PageProps) { ); })} - - {nextLessonId && ( - - )} diff --git a/frontend/src/app/user/my-courses/page.tsx b/frontend/src/app/user/my-courses/page.tsx index e971f821..5817d241 100644 --- a/frontend/src/app/user/my-courses/page.tsx +++ b/frontend/src/app/user/my-courses/page.tsx @@ -396,8 +396,10 @@ function MyCoursesContent() { -
-
+ {/* Mobile-friendly layout */} +
+ {/* Search bar - full width on mobile */} +
setSearch(e.target.value)} @@ -409,8 +411,8 @@ function MyCoursesContent() {
- {/* View Mode Toggle */} -
+ {/* View Mode Toggle - Hide on mobile */} +
)} {courses.length === 0 && ( -
+
diff --git a/frontend/src/app/user/my-quizzes/[quizId]/play/page.tsx b/frontend/src/app/user/my-quizzes/[quizId]/play/page.tsx index 277dd2de..f36886cd 100644 --- a/frontend/src/app/user/my-quizzes/[quizId]/play/page.tsx +++ b/frontend/src/app/user/my-quizzes/[quizId]/play/page.tsx @@ -98,6 +98,7 @@ export default function QuizPlayPage({ params }: PageProps) { const timerRef = useRef(null); const [timerFrozen, setTimerFrozen] = useState(false); const freezeTimeoutRef = useRef(null); + const isProcessingTimeout = useRef(false); // Power-ups const [powerUps, setPowerUps] = useState([ @@ -233,10 +234,22 @@ export default function QuizPlayPage({ params }: PageProps) { }, [quizId]); const handleTimeUp = useCallback(() => { - if (timerRef.current) clearInterval(timerRef.current); + // Prevent duplicate execution + if (isProcessingTimeout.current) { + return; + } + isProcessingTimeout.current = true; + + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } const currentCard = quiz?.cards[currentIndex]; - if (!currentCard) return; + if (!currentCard) { + isProcessingTimeout.current = false; + return; + } // Show toast after state updates to avoid setState in render setTimeout(() => { @@ -257,6 +270,8 @@ export default function QuizPlayPage({ params }: PageProps) { // Auto submit on last question handleSubmit(); } + // Reset flag after transition + isProcessingTimeout.current = false; }, 1500); }, [quiz, currentIndex, answers, timePerQuestion]); @@ -266,9 +281,9 @@ export default function QuizPlayPage({ params }: PageProps) { timerRef.current = setInterval(() => { setTimeLeft((prev) => { - if (prev <= 0) { - // Time's up for this question - handleTimeUp(); + if (prev <= 1) { + // Time's up for this question - use ref to avoid dependency issues + handleTimeUpRef.current(); return 0; } @@ -282,9 +297,12 @@ export default function QuizPlayPage({ params }: PageProps) { }, 1000); return () => { - if (timerRef.current) clearInterval(timerRef.current); + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } }; - }, [quiz, submitted, loading, currentIndex, timePerQuestion, handleTimeUp, timerFrozen]); + }, [quiz, submitted, loading, currentIndex, timePerQuestion, timerFrozen]); const handleTimeUpRef = useRef(handleTimeUp); useEffect(() => { @@ -618,10 +636,15 @@ export default function QuizPlayPage({ params }: PageProps) { setEliminatedOptions({}); setShowHint({}); setPowerUps(prev => prev.map(p => ({ ...p, usesLeft: p.maxUses, active: false }))); + isProcessingTimeout.current = false; if (freezeTimeoutRef.current) { clearTimeout(freezeTimeoutRef.current); } + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } // Reload to fetch fresh user EXP window.location.reload(); @@ -860,12 +883,12 @@ export default function QuizPlayPage({ params }: PageProps) {
{/* Power-ups */} -
-
- -

Items (EXP: {userExp})

+
+
+ +

Items (EXP: {userExp})

-
+
{powerUps.map((powerUp) => { const isActive = powerUp.active; const canUse = powerUp.usesLeft > 0 && userExp >= powerUp.cost; @@ -883,7 +906,7 @@ export default function QuizPlayPage({ params }: PageProps) { key={powerUp.id} onClick={() => activatePowerUp(powerUp.id)} disabled={!canUse || isActive} - className={`group relative flex items-center gap-2 px-4 py-2.5 rounded-lg border-2 font-semibold text-sm transition-all ${ + className={`group relative flex flex-col lg:flex-row items-center justify-center lg:justify-start gap-1 sm:gap-1.5 lg:gap-2 px-2 sm:px-3 lg:px-4 py-2 sm:py-2.5 rounded-lg border-2 font-semibold text-[10px] sm:text-xs lg:text-sm transition-all ${ isActive ? 'border-green-500 bg-green-50 text-green-700' : canUse @@ -892,11 +915,11 @@ export default function QuizPlayPage({ params }: PageProps) { }`} title={powerUp.description} > - - {powerUp.name} - ({powerUp.usesLeft}/{powerUp.maxUses}) + + {powerUp.name} + ({powerUp.usesLeft}/{powerUp.maxUses}) {canUse && !isActive && ( - + -{powerUp.cost} )} diff --git a/frontend/src/app/user/my-quizzes/page.tsx b/frontend/src/app/user/my-quizzes/page.tsx index 39785a05..a3b28b52 100644 --- a/frontend/src/app/user/my-quizzes/page.tsx +++ b/frontend/src/app/user/my-quizzes/page.tsx @@ -239,9 +239,9 @@ function MyQuizzesContent() {
-
-
-
+
+
+
setSearch(e.target.value)} @@ -253,14 +253,14 @@ function MyQuizzesContent() {
-
-
+
+
- + setSort(e.target.value as SortOption)} - className="appearance-none h-10 pl-4 pr-10 rounded-lg bg-white border border-gray-200 shadow-sm text-sm focus:ring-2 focus:ring-sky-500/40 focus:border-sky-400 cursor-pointer" + className="appearance-none w-full h-10 pl-4 pr-10 rounded-lg bg-white border border-gray-200 shadow-sm text-sm focus:ring-2 focus:ring-sky-500/40 focus:border-sky-400 cursor-pointer" > @@ -318,8 +318,8 @@ function MyQuizzesContent() {
{paginatedQuizzes.length === 0 ? ( -
-

+

+

{filteredMy.length === 0 && (activeTab === 'my' ? myQuizzes : publicQuizzes).length === 0 ? (activeTab === 'my' ? 'Bạn chưa có quiz nào.' : 'Chưa có quiz công khai nào.') : 'Không tìm thấy quiz phù hợp.'} @@ -335,7 +335,7 @@ function MyQuizzesContent() {

) : ( <> -
+
{paginatedQuizzes.map((quiz) => ( ))} diff --git a/frontend/src/app/user/ranking/page.tsx b/frontend/src/app/user/ranking/page.tsx index 4d5ae52b..b7d87a0a 100644 --- a/frontend/src/app/user/ranking/page.tsx +++ b/frontend/src/app/user/ranking/page.tsx @@ -33,57 +33,57 @@ export default function RankingPage() { : 0; return ( -
+
{/* Header */} -
-

🏆 Bảng Xếp Hạng

-

Xem thứ hạng của bạn trong cộng đồng học tập

+
+

🏆 Bảng Xếp Hạng

+

Xem thứ hạng của bạn trong cộng đồng học tập

{/* Loading State */} {loading && ( -
+
-

Đang tải bảng xếp hạng...

+

Đang tải bảng xếp hạng...

)} {/* Current User Stats Card */} {!loading && user && ( -
-
+
+
{/* User Info */} -
-
+
+
#{user.rank || '—'}
-
Thứ hạng của bạn
-
+
Thứ hạng của bạn
+
trong {user.total_users || 0} người dùng
{/* Level & Exp Progress */} -
-
-
-

{user.name}

-

{user.email}

+
+
+
+

{user.name}

+

{user.email}

-
Lv {user.level || 1}
+
Lv {user.level || 1}
Level
{/* Progress Bar */}
-
+
Tiến độ lên level {expPercent}%
-
+
{/* Quick Stats */} -
-
-
{user.completed_courses || 0}
+
+
+
{user.completed_courses || 0}
Khóa học
-
-
{user.current_exp || 0}
+
+
{user.current_exp || 0}
Kinh nghiệm
-
-
{user.total_lessons || 0}
+
+
{user.total_lessons || 0}
Bài học
@@ -117,45 +117,45 @@ export default function RankingPage() { {/* Competitor Comparison Widget */} {!loading && competitors && ( -
-
- -

Đối thủ xung quanh bạn

+
+
+ +

Đối thủ xung quanh bạn

-
+
{competitors.above && ( -
-
-
-
#{competitors.above.rank}
-
-
{competitors.above.name}
-
Lv {competitors.above.level} • {(competitors.above.experience || 0).toLocaleString()} EXP
+
+
+
+
#{competitors.above.rank}
+
+
{competitors.above.name}
+
Lv {competitors.above.level} • {(competitors.above.experience || 0).toLocaleString()} EXP
- +
)} -
-
-
-
#{competitors.current.rank}
-
-
{competitors.current.name} (Bạn)
-
Lv {competitors.current.level} • {(competitors.current.experience || 0).toLocaleString()} EXP
+
+
+
+
#{competitors.current.rank}
+
+
{competitors.current.name} (Bạn)
+
Lv {competitors.current.level} • {(competitors.current.experience || 0).toLocaleString()} EXP
{competitors.below && ( -
-
-
-
#{competitors.below.rank}
-
-
{competitors.below.name}
-
Lv {competitors.below.level} • {(competitors.below.experience || 0).toLocaleString()} EXP
+
+
+
+
#{competitors.below.rank}
+
+
{competitors.below.name}
+
Lv {competitors.below.level} • {(competitors.below.experience || 0).toLocaleString()} EXP
@@ -163,8 +163,8 @@ export default function RankingPage() { )}
{competitors.above && competitors.expGap > 0 && ( -
-

+

+

💪 Bạn chỉ còn cách {competitors.above.name} {competitors.expGap} EXP nữa thôi! Học ngay 1 bài để vượt mặt nào!

@@ -174,21 +174,21 @@ export default function RankingPage() { {/* Ranking Display */} {!loading && users.length > 0 && ( -
+
{/* Top 3 Podium */} -
-
- 👑 -

Top 3 Xuất Sắc

+
+
+ 👑 +

Top 3 Xuất Sắc

{/* Full Ranking Table */} -
-
-

📊 Bảng Xếp Hạng

-
+
+
+

📊 Bảng Xếp Hạng

+
Tổng: {users.length} người
@@ -196,12 +196,12 @@ export default function RankingPage() {
{/* Info Box */} -
-
-
💡
-
-

Cách tăng thứ hạng

-
    +
    +
    +
    💡
    +
    +

    Cách tăng thứ hạng

    +
    • ✅ Hoàn thành bài học và quiz để nhận EXP
    • 🎯 Đạt điểm cao trong các bài kiểm tra
    • 🔥 Duy trì streak học tập mỗi ngày
    • diff --git a/frontend/src/app/user/tracking/page.tsx b/frontend/src/app/user/tracking/page.tsx index f9066a86..60d078f8 100644 --- a/frontend/src/app/user/tracking/page.tsx +++ b/frontend/src/app/user/tracking/page.tsx @@ -132,41 +132,41 @@ function TrackingInner() { }; return ( -
      +
      {/* Header */} -
      -
      +
      +
      - - + + Generation Tracking
      -

      Theo dõi tiến trình tạo nội dung

      -

      +

      Theo dõi tiến trình tạo nội dung

      +

      Theo dõi chi tiết quá trình AI tạo khóa học và quiz tự động của bạn.

      -
      +
      {activeTab === 'courses' ? ( <> - - - - @@ -174,13 +174,13 @@ function TrackingInner() { ) : ( <> - - - - @@ -191,10 +191,10 @@ function TrackingInner() {
      {/* Tabs */} -
      +
      -
      +
      Đăng Nhập Với
      diff --git a/frontend/src/components/auth/SignUpForm.tsx b/frontend/src/components/auth/SignUpForm.tsx index 98f3fbcd..38692b3e 100644 --- a/frontend/src/components/auth/SignUpForm.tsx +++ b/frontend/src/components/auth/SignUpForm.tsx @@ -124,23 +124,23 @@ export default function SignUpForm() { imageAlt="Signup illustration" headerVariant="auth" > - +
      -
      -
      +