diff --git a/Frontend-Learner/pages/classroom/learning.vue b/Frontend-Learner/pages/classroom/learning.vue index f6c06675..3a68f2b7 100644 --- a/Frontend-Learner/pages/classroom/learning.vue +++ b/Frontend-Learner/pages/classroom/learning.vue @@ -195,9 +195,9 @@ const resetAndNavigate = (path: string) => { // 2. Clear all localStorage localStorage.clear() - // 3. Restore whitelisted keys - Object.entries(whitelist).forEach(([key, value]) => { - localStorage.setItem(key, value) + // 3. Restore ONLY whitelisted keys + Object.keys(whitelist).forEach(key => { + localStorage.setItem(key, whitelist[key]) }) // 4. Force hard reload to the new path @@ -278,6 +278,12 @@ const loadLesson = async (lessonId: number) => { isPlaying.value = false videoProgress.value = 0 currentTime.value = 0 + initialSeekTime.value = 0 + maxWatchedTime.value = 0 + lastSavedTime.value = -1 + lastSavedTimestamp.value = 0 + lastLocalSaveTimestamp.value = 0 + currentDuration.value = 0 currentLesson.value = null // This will unmount VideoPlayer and hide content isLessonLoading.value = true @@ -332,27 +338,36 @@ const loadLesson = async (lessonId: number) => { // 2. Fetch Initial Progress (Resume Playback) if (currentLesson.value.type === 'VIDEO') { - // A. Server Progress - const progressRes = await fetchVideoProgress(lessonId) - let serverProgress = 0 - if (progressRes.success && progressRes.data?.video_progress_seconds) { - serverProgress = progressRes.data.video_progress_seconds - } - - // B. Local Progress (Buffer) - const localProgress = getLocalProgress(lessonId) + // If already completed, clear local resume point to allow fresh re-watch + const isCompleted = currentLesson.value.progress?.is_completed || false - // C. Hybrid Resume (Max Wins) - const resumeTime = Math.max(serverProgress, localProgress) - - if (resumeTime > 0) { - - initialSeekTime.value = resumeTime - maxWatchedTime.value = resumeTime - currentTime.value = resumeTime - } else { + if (isCompleted) { + const key = getLocalProgressKey(lessonId) + if (key && typeof window !== 'undefined') { + localStorage.removeItem(key) + } initialSeekTime.value = 0 maxWatchedTime.value = 0 + currentTime.value = 0 + } else { + // Not completed? Resume from where we left off + const progressRes = await fetchVideoProgress(lessonId) + let serverProgress = 0 + if (progressRes.success && progressRes.data?.video_progress_seconds) { + serverProgress = progressRes.data.video_progress_seconds + } + + const localProgress = getLocalProgress(lessonId) + const resumeTime = Math.max(serverProgress, localProgress) + + if (resumeTime > 0) { + initialSeekTime.value = resumeTime + maxWatchedTime.value = resumeTime + currentTime.value = resumeTime + } else { + initialSeekTime.value = 0 + maxWatchedTime.value = 0 + } } } } @@ -556,13 +571,19 @@ const videoSrc = computed(() => { // เมื่อวิดีโอจบ ให้บันทึกว่าเรียนจบ (Complete) const onVideoEnded = async () => { - // Safety check before saving const lesson = currentLesson.value - if (!lesson || !lesson.progress || lesson.progress.is_completed || isCompleting.value) return + if (!lesson) return + + // Clear local storage on end since it's completed + const key = getLocalProgressKey(lesson.id) + if (key && typeof window !== 'undefined') { + localStorage.removeItem(key) + } + + if (lesson.progress?.is_completed || isCompleting.value) return isCompleting.value = true try { - // Force save progress at 100% to trigger backend completion await performSaveProgress(true, false) } catch (err) { console.error('Failed to save progress on end:', err) diff --git a/Frontend-Learner/คู่มืออธิบาย/web-dev-details.md b/Frontend-Learner/คู่มืออธิบาย/web-dev-details.md index 7d65ecd1..af4494f7 100644 --- a/Frontend-Learner/คู่มืออธิบาย/web-dev-details.md +++ b/Frontend-Learner/คู่มืออธิบาย/web-dev-details.md @@ -10,9 +10,9 @@ ### 1.1 Tech Stack -- **Core:** [Nuxt 3](https://nuxt.com) (Vue 3 Composition API), TypeScript `^5.0` -- **UI Framework:** Quasar Framework (via `nuxt-quasar-ui`) -- **Styling:** Tailwind CSS (Utility) + Vanilla CSS Variables (Theming/Dark Mode) +- **Core:** [Nuxt 3](https://nuxt.com) (v`^3.11.2`), TypeScript `^5.4.5` +- **UI Framework:** Quasar Framework `^2.15.2` (via `nuxt-quasar-ui ^3.0.0`) +- **Styling:** Tailwind CSS `^6.12.0` (Utility) + Vanilla CSS Variables (Theming/Dark Mode) - **State Management:** `ref`/`reactive` (Local) + `useState` (Global/Shared State) - **Localization:** `@nuxtjs/i18n` (Supports JSON locales in `i18n/locales/`) - **Media Control:** `useMediaPrefs` (Command Pattern for global volume/mute state) @@ -57,17 +57,20 @@ - **Common (`components/common/`):** - `GlobalLoader.vue`: Loading indicator ทั่วทั้งแอป - `LanguageSwitcher.vue`: ปุ่มเปลี่ยนภาษา (TH/EN) - - `AppHeader.vue`, `MobileNav.vue`: Navigation หลัก + - `AppHeader.vue`, `MobileNav.vue`: Navigation หลัก (ใช้ร่วมกับ AppSidebar) - `FormInput.vue`: Input field มาตรฐาน +- **Layout (`components/layout/`):** + - `AppSidebar.vue`: Sidebar หลักสำหรับ Dashboard (Collapsible) + - `LandingHeader.vue`: Header เฉพาะสำหรับหน้า Landing Page - **Course (`components/course/`):** - - `CourseCard.vue`: การ์ดแสดงผลคอร์ส (ใช้ซ้ำหลายหน้า) + - `CourseCard.vue`: การ์ดแสดงผลคอร์ส รองรับ Progress และ **Glassmorphism** ในโหมดมืด - **Discovery (`components/discovery/`):** - `CategorySidebar.vue`: Sidebar ตัวกรองหมวดหมู่แบบย่อ/ขยายได้ - `CourseDetailView.vue`: หน้ารายละเอียดคอร์สขนาดใหญ่ (Video Preview + Syllabus) - **Classroom (`components/classroom/`):** - `CurriculumSidebar.vue`: Sidebar บทเรียนและสถานะการเรียน - `AnnouncementModal.vue`: Modal แสดงประกาศของคอร์ส - - `VideoPlayer.vue`: Video Player พร้อม Custom Controls + - `VideoPlayer.vue`: Video Player พร้อม Custom Controls และ YouTube Support - **User / Profile (`components/user/`, `components/profile/`):** - `UserAvatar.vue`: แสดงรูปโปรไฟล์ (รองรับ Fallback) - `ProfileEditForm.vue`: ฟอร์มแก้ไขข้อมูลส่วนตัว @@ -94,6 +97,7 @@ - **Classroom:** - `fetchCourseLearningInfo`: โครงสร้างบทเรียน (Chapters/Lessons) - `fetchLessonContent`: เนื้อหาวิดีโอ/Quiz/Attachments + - `fetchCourseAnnouncements`: ดึงข้อมูลประกาศของคอร์ส - `saveVideoProgress`: บันทึกเวลาเรียน (Sync Server) - **i18n Support:** `getLocalizedText` ตัวช่วยในการเลือกแสดงผลภาษา (TH/EN) ตาม Locale ปัจจุบันที่ผู้ใช้เลือก อัตโนมัติทั่วทั้งแอป @@ -164,6 +168,20 @@ - **Hardcoded Removal:** ทยอยลบข้อความภาษาไทยที่พิมพ์ค้างไว้ในโค้ด (เช่น ใน Sidebar หมวดหมู่) และแทนที่ด้วย i18n keys - **Boot Sequence Fix:** แก้ไขปัญหาเว็บค้าง (Error 500) ที่เกิดจากการเรียกใช้ภาษาเร็วเกินไปก่อนที่ระบบจะพร้อม (`initialization error`) -7. **Landing Page & Header Refinement:** - - **Login Button:** อัปเกรดปุ่ม "เข้าสู่ระบบ" จากลิงก์ข้อความธรรมดา ให้เป็นปุ่มแบบ Secondary ที่โดดเด่นและชัดเจนขึ้น เพื่อดึงดูดผู้ใช้งาน - - **Visual Hierarchy:** จัดลำดับความสำคัญของปุ่ม Get Started และ Login ให้สมดุลกันมากขึ้นในโหมดสว่างและโหมดมืด (Scrolled Header) +7. **Classroom & UX Optimization (Mid-February 2026):** + - **SPA Navigation for Learning:** เปลี่ยนระบบเลือกบทเรียนจากการ Reload หน้าเป็น SPA Navigation ทำให้เปลี่ยนวิดีโอ/บทเรียนได้ทันทีโดยไม่ต้อง Refresh หน้าจอ + - **Announcement Persistence:** เพิ่มระบบเช็กสถานะการอ่านประกาศ (Unread Badge) โดยบันทึกสถานะล่าสุดลง LocalStorage แยกตามผู้ใช้และคอร์ส + - **YouTube Resume:** รองรับการเรียนต่อจากจุดเดิมสำหรับวิดีโอ YouTube (Time Seeking via URL parameter) + +8. **Quiz System Enhancements:** + - **Answer Review Mode:** เพิ่มโหมดเฉลยข้อสอบหลังทำเสร็จ พร้อมการไฮไลท์สีที่ชัดเจน (เขียว = ถูก, แดง = ตอบผิด) + - **Shuffle Logic:** เพิ่มการสลับคำถามและตัวเลือก (Shuffle) เพื่อความโปร่งใสในการสอบ + - **Enhanced Feedback:** ปรับปรุง UI ผลลัพธ์การสอบให้มีความ Premium และเข้าใจง่ายขึ้น + +9. **Security & Registration Polish:** + - **Phone Validation:** เพิ่มระบบตรวจสอบเบอร์โทรศัพท์ในหน้าสมัครสมาชิก (ต้องเป็นตัวเลขและยาวไม่เกิน 10 หลัก) + - **Enrollment Alert Logic:** ปรับปรุง Logic การสมัครเรียนให้ตรวจสอบสถานะ Enrollment เดิมก่อน เพื่อป้องกัน API Error และการเรียก request ซ้ำซ้อน + +10. **Profile & Certificates:** + - **Verification Badge:** เพิ่มการแสดงผลสถานะการยืนยันอีเมลในหน้าโปรไฟล์ พร้อมปุ่มส่งอีเมลยืนยันหากยังไม่ได้ทำ + - **Certificate Flow:** ปรับปรุงระบบดาวน์โหลดใบประกาศนียบัตรให้รองรับทั้งการดึงไฟล์เดิมและสั่ง Generate ใหม่หากยังไม่มี