feat: Initialize e-learning frontend with course browsing, landing, and authentication pages, along with core layouts and composables.

This commit is contained in:
supalerk-ar66 2026-02-04 16:57:25 +07:00
parent a0b93978a7
commit a201c4285b
8 changed files with 129 additions and 198 deletions

View file

@ -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()

View file

@ -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<QuizQuestion[]>('quiz-questions', () => []);
const answers = useState<Record<number, AnswerState>>('quiz-answers', () => ({}));
const currentQuestionIndex = useState<number>('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)
};
};