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..9b9e0eb9 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 @@ -360,7 +360,6 @@ export default function LessonDetailPage({ params }: PageProps) { console.warn('⚠️ Backend did not fully award exp, attempting direct user-service call...'); try { await userApi.addExperience({ exp: awardedExp }); - console.log('✅ Successfully awarded exp via fallback mechanism'); } catch (fallbackError) { console.error('❌ Fallback exp award also failed:', fallbackError); showToast.error('Không thể cộng điểm kinh nghiệm. Vui lòng kiểm tra lại sau.'); @@ -761,16 +760,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..7b94b34e 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([ @@ -193,10 +194,6 @@ export default function QuizPlayPage({ params }: PageProps) { const responseData = userInfoResp?.data as { Info?: { current_exp?: number; subscription?: number }; info?: { current_exp?: number; subscription?: number } } | undefined; // Try both Info (capital) and info (lowercase) for compatibility const userInfo = responseData?.Info || responseData?.info; - console.log('User info response:', responseData); - console.log('Extracted user info:', userInfo); - console.log('Current EXP:', userInfo?.current_exp); - console.log('Subscription:', userInfo?.subscription); const exp = userInfo?.current_exp || 0; const subscription = userInfo?.subscription || 0; @@ -233,10 +230,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 +266,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 +277,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 +293,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(() => { @@ -576,13 +590,11 @@ export default function QuizPlayPage({ params }: PageProps) { // Calculate average score for saving as previous_score const avgScore = quiz ? Math.round(actualTotalScore / quiz.cards.length) : 0; - console.log('Average score for quiz:', avgScore); // Update previous_score if this score is higher if (avgScore > (quiz.previous_score || 0)) { try { await quizApi.updatePreviousScore(quizId, avgScore); - console.log('Updated previous_score to:', avgScore); } catch (updateError) { console.error('Failed to update previous_score:', updateError); // Don't show error to user, just log it @@ -618,10 +630,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 +877,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 +900,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 +909,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..3f8be2ff 100644 --- a/frontend/src/app/user/tracking/page.tsx +++ b/frontend/src/app/user/tracking/page.tsx @@ -73,6 +73,7 @@ function TrackingInner() { progress_percentage: typeof r.progress_percentage === 'number' ? r.progress_percentage : undefined, current_step: r.current_step ? String(r.current_step) : undefined, current_step_detail: r.current_step_detail ? String(r.current_step_detail) : undefined, + vectorized: r.vectorized !== undefined ? Boolean(r.vectorized) : undefined, plan_ready: r.plan_ready !== undefined ? Boolean(r.plan_ready) : undefined, questions_ready: r.questions_ready !== undefined ? Boolean(r.questions_ready) : undefined, title: r.title ? String(r.title) : undefined, @@ -132,41 +133,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 +175,13 @@ function TrackingInner() { ) : ( <> - - - - @@ -191,10 +192,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" > - +
      -
      -
      +