diff --git a/Frontend-Learner/assets/css/main.css b/Frontend-Learner/assets/css/main.css index 46420eed..61b96a06 100644 --- a/Frontend-Learner/assets/css/main.css +++ b/Frontend-Learner/assets/css/main.css @@ -62,10 +62,15 @@ --bg-body: #020617; /* Slate 950: Deep Sea */ --bg-surface: #0f172a; /* Slate 900: Sidebar/Main Cards */ --bg-elevated: #1e293b; /* Slate 800: Inner Cards/Highlights */ - + --text-main: #f8fafc; /* text-slate-50: Brighter white for main text */ --text-secondary: #94a3b8; /* text-slate-300: Lighter grey for secondary text */ - --border-color: rgba(255, 255, 255, 0.08); /* White with low opacity for subtle borders */ + --border-color: rgba( + 255, + 255, + 255, + 0.08 + ); /* White with low opacity for subtle borders */ --neutral-50: #1e293b; --neutral-100: #334155; --neutral-200: #475569; @@ -226,22 +231,17 @@ ul { color: #f1f5f9; } -/* Auth Layout */ +/* Auth Layout - Always Light for Auth Pages */ .auth-shell { min-height: 100vh; display: flex; align-items: center; justify-content: center; - background-color: var(--bg-body); - color: var(--text-main); + background-color: #ffffff; /* เปลี่ยนเป็นสีขาวล้วน */ + color: #0f172a; /* สีข้อความเข้ม */ padding: 24px; } -.dark .auth-shell { - background-color: #0f172a; - color: #f1f5f9; -} - /* =========================== Components =========================== */ @@ -1168,6 +1168,3 @@ html:not(.dark) .q-item__label, html:not(.dark) .q-select__dropdown .q-item { color: #0f172a !important; } - - - diff --git a/Frontend-Learner/composables/useAuth.ts b/Frontend-Learner/composables/useAuth.ts index 2ad6eebc..b7bff874 100644 --- a/Frontend-Learner/composables/useAuth.ts +++ b/Frontend-Learner/composables/useAuth.ts @@ -452,9 +452,14 @@ export const useAuth = () => { refreshToken.value = null // ลบ Refresh Token user.value = null - // Reset client-side storage + // Reset client-side storage (Keep remembered_email) if (import.meta.client) { + // ลบเฉพาะข้อมูลที่ไม่ใช่อีเมลที่จำไว้ + const rememberedEmail = localStorage.getItem('remembered_email') localStorage.clear() + if (rememberedEmail) { + localStorage.setItem('remembered_email', rememberedEmail) + } } const router = useRouter() diff --git a/Frontend-Learner/composables/useQuizRunner.ts b/Frontend-Learner/composables/useQuizRunner.ts index 1ef68b4c..840aaa60 100644 --- a/Frontend-Learner/composables/useQuizRunner.ts +++ b/Frontend-Learner/composables/useQuizRunner.ts @@ -2,67 +2,27 @@ export type QuestionStatus = 'not_started' | 'in_progress' | 'completed' | 'skip export interface QuizQuestion { id: number; - title: string; + question: string | { th: string; en: string }; is_skippable: boolean; - type: 'single' | 'multiple' | 'text'; - options?: { id: string; label: string }[]; + type: string; + choices?: { id: number; text: string | { th: string; en: string } }[]; } export interface AnswerState { questionId: number; - value: string | string[] | null; + value: any; is_saved: boolean; status: QuestionStatus; touched: boolean; last_saved_at?: string; } -// Mock Data -const MOCK_QUESTIONS: QuizQuestion[] = [ - { - id: 1, - title: 'What is the capital of France?', - is_skippable: true, - type: 'single', - options: [ - { id: 'london', label: 'London' }, - { id: 'paris', label: 'Paris' }, - { id: 'berlin', label: 'Berlin' }, - ], - }, - { - id: 2, - title: 'Explain the concept of closure in JavaScript.', - is_skippable: false, - type: 'text', - }, - { - id: 3, - title: 'Which of the following are Vue lifecycle hooks? (Select all that apply)', - is_skippable: true, - type: 'multiple', - options: [ - { id: 'created', label: 'created' }, - { id: 'mounted', label: 'mounted' }, - { id: 'render', label: 'render' }, - { id: 'compute', label: 'compute' }, - ], - }, - { - id: 4, - title: 'What is 2 + 2?', - is_skippable: false, - type: 'single', - options: [ - { id: '3', label: '3' }, - { id: '4', label: '4' }, - { id: '5', label: '5' }, - ], - } -]; - +/** + * @composable useQuizRunner + * @description Manages the state and logic for running a quiz activity. + */ export const useQuizRunner = () => { - // State (using useState for Nuxt SSR safety and persistence across component reloads if needed) + // State const questions = useState('quiz-questions', () => []); const answers = useState>('quiz-answers', () => ({})); const currentQuestionIndex = useState('quiz-current-index', () => 0); @@ -82,8 +42,10 @@ export const useQuizRunner = () => { const isFirstQuestion = computed(() => currentQuestionIndex.value === 0); // Actions - function initQuiz(quizId: string) { - questions.value = [...MOCK_QUESTIONS]; + function initQuiz(quizData: any) { + if (!quizData || !quizData.questions) return; + + questions.value = quizData.questions; currentQuestionIndex.value = 0; answers.value = {}; lastError.value = null; @@ -91,7 +53,7 @@ export const useQuizRunner = () => { questions.value.forEach(q => { answers.value[q.id] = { questionId: q.id, - value: q.type === 'multiple' ? [] : null, + value: null, is_saved: false, status: 'not_started', touched: false, @@ -107,7 +69,6 @@ export const useQuizRunner = () => { const ans = answers.value[qId]; if (ans) { ans.touched = true; - // Mark as in_progress if not final state if (ans.status === 'not_started' || ans.status === 'skipped') { ans.status = 'in_progress'; } @@ -122,21 +83,18 @@ export const useQuizRunner = () => { if (a.status === 'completed' || a.is_saved) return { allowed: true }; if (q.is_skippable) return { allowed: true }; - // Required and unsaved - if (!a.is_saved) { - return { allowed: false, reason: 'This question is required and must be saved.' }; + if (!a.is_saved && a.value === null) { + return { allowed: false, reason: 'This question is required.' }; } return { allowed: true }; } - function updateAnswer(val: string | string[] | null) { + function updateAnswer(val: any) { if (!currentQuestion.value) return; const qId = currentQuestion.value.id; answers.value[qId].value = val; - // If modifying a completed answer, revert to in_progress until saved again? - // Yes, to enforce "Green = Saved Successfully" implies current state matches saved state. if (answers.value[qId].is_saved) { answers.value[qId].is_saved = false; answers.value[qId].status = 'in_progress'; @@ -148,24 +106,15 @@ export const useQuizRunner = () => { const qId = currentQuestion.value.id; const ans = answers.value[qId]; - // Validation - if (currentQuestion.value.type === 'multiple') { - if (!ans.value || (ans.value as string[]).length === 0) { - lastError.value = "Please select at least one option."; - return false; - } - } else { - if (!ans.value || (ans.value as string).trim() === '') { - lastError.value = "Please provide an answer."; - return false; - } + if (ans.value === null) { + lastError.value = "Please provide an answer."; + return false; } loading.value = true; lastError.value = null; try { - await new Promise(resolve => setTimeout(resolve, 800)); ans.is_saved = true; ans.status = 'completed'; ans.last_saved_at = new Date().toISOString(); @@ -187,12 +136,12 @@ export const useQuizRunner = () => { return false; } - // Mark current as skipped if leaving without completion const currQ = currentQuestion.value; - const currAns = answers.value[currQ.id]; - // If we leave and it is NOT completed (and implicit skippable check passed), set SKIPPED - if (currAns.status !== 'completed' && !currAns.is_saved) { - currAns.status = 'skipped'; + if (currQ) { + const currAns = answers.value[currQ.id]; + if (currAns.status !== 'completed' && !currAns.is_saved) { + currAns.status = 'skipped'; + } } currentQuestionIndex.value = targetIndex; @@ -205,51 +154,22 @@ export const useQuizRunner = () => { return true; } - async function nextQuestion() { - if (isLastQuestion.value) return; - handleLeaveLogic(currentQuestionIndex.value + 1); - } - - async function prevQuestion() { - if (isFirstQuestion.value) return; - handleLeaveLogic(currentQuestionIndex.value - 1); - } - - async function goToQuestion(index: number) { - if (index < 0 || index >= questions.value.length) return; - handleLeaveLogic(index); - } - - async function skipQuestion() { - if (isLastQuestion.value) { - // If last question and skip... maybe just mark skipped? - const currQ = currentQuestion.value; - const currAns = answers.value[currQ.id]; - currAns.status = 'skipped'; - return; - } - handleLeaveLogic(currentQuestionIndex.value + 1); - } - return { questions, answers, currentQuestionIndex, loading, lastError, - currentQuestion, currentAnswer, totalQuestions, isFirstQuestion, isLastQuestion, - initQuiz, updateAnswer, saveCurrentAnswer, - nextQuestion, - prevQuestion, - goToQuestion, - skipQuestion + nextQuestion: () => handleLeaveLogic(currentQuestionIndex.value + 1), + prevQuestion: () => handleLeaveLogic(currentQuestionIndex.value - 1), + goToQuestion: (index: number) => handleLeaveLogic(index) }; }; diff --git a/Frontend-Learner/layouts/auth.vue b/Frontend-Learner/layouts/auth.vue index 5118eb43..57de3a1f 100644 --- a/Frontend-Learner/layouts/auth.vue +++ b/Frontend-Learner/layouts/auth.vue @@ -1,6 +1,26 @@ + + diff --git a/Frontend-Learner/layouts/landing.vue b/Frontend-Learner/layouts/landing.vue index e822d2d8..f33dc157 100644 --- a/Frontend-Learner/layouts/landing.vue +++ b/Frontend-Learner/layouts/landing.vue @@ -1,13 +1,27 @@ +