feat: Initialize e-learning frontend with course browsing, landing, and authentication pages, along with core layouts and composables.
This commit is contained in:
parent
a0b93978a7
commit
a201c4285b
8 changed files with 129 additions and 198 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue