From d0873b0814374d7929ac406db067e7a02ec52327 Mon Sep 17 00:00:00 2001 From: meewaldor Date: Thu, 27 Nov 2025 21:27:48 +0700 Subject: [PATCH 1/2] refactor(quiz): clean up unused variables and improve layout for True/False questions --- .../components/player/QuizPlayerContainer.tsx | 5 - .../player/question/card/QuestionCard.tsx | 26 --- .../question/types/TrueFalseQuestion.tsx | 163 ++++++++++-------- 3 files changed, 89 insertions(+), 105 deletions(-) diff --git a/src/features/resource/quiz/components/player/QuizPlayerContainer.tsx b/src/features/resource/quiz/components/player/QuizPlayerContainer.tsx index 60e2acc3b..f093410ad 100644 --- a/src/features/resource/quiz/components/player/QuizPlayerContainer.tsx +++ b/src/features/resource/quiz/components/player/QuizPlayerContainer.tsx @@ -3,18 +3,13 @@ import { useEffect } from 'react' import { useIsMobile } from '@/hooks/use-mobile' import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks' -import QuizResult from '@/features/resource/quiz/components/player/QuizResult' import QuizSidebar from '@/features/resource/quiz/components/player/QuizSidebar' import QuizMainContent from '@/features/resource/quiz/components/player/QuizMainContent' -import { useGetQuizByIdQuery } from '@/features/resource/quiz/api/quizApi' import { initializeQuiz } from '@/features/resource/quiz/slice/quiz-player-slice' -import LoadingComponent from '@/components/shared/loading/LoadingComponent' import SEmpty from '@/components/shared/empty/SEmpty' export default function QuizPlayerContainer() { const dispatch = useAppDispatch() - const { isSubmitted } = useAppSelector((state) => state.quizPlayer) - const isMobile = useIsMobile() const selectedQuiz = useAppSelector((state) => state.quizPlayer.selectedQuiz) useEffect(() => { diff --git a/src/features/resource/quiz/components/player/question/card/QuestionCard.tsx b/src/features/resource/quiz/components/player/question/card/QuestionCard.tsx index c51012ab4..1ac175d3a 100644 --- a/src/features/resource/quiz/components/player/question/card/QuestionCard.tsx +++ b/src/features/resource/quiz/components/player/question/card/QuestionCard.tsx @@ -17,32 +17,6 @@ type QuestionCardProps = { export default function QuestionCard({ question }: QuestionCardProps) { const { isSubmitted, userAnswers, currentQuestionIndex } = useAppSelector((state) => state.quizPlayer) - const getQuestionTypeLabel = (type: QuestionType) => { - switch (type) { - case QuestionType.TRUE_FALSE: - return 'Đúng/Sai' - case QuestionType.SINGLE_CHOICE: - return 'Một đáp án' - case QuestionType.MULTIPLE_CHOICE: - return 'Nhiều đáp án' - default: - return type - } - } - - const getQuestionTypeColor = (type: QuestionType) => { - switch (type) { - case QuestionType.TRUE_FALSE: - return 'bg-gradient-to-r from-blue-500 to-cyan-500' - case QuestionType.SINGLE_CHOICE: - return 'bg-gradient-to-r from-green-500 to-emerald-500' - case QuestionType.MULTIPLE_CHOICE: - return 'bg-gradient-to-r from-purple-500 to-pink-500' - default: - return 'bg-gradient-to-r from-gray-500 to-slate-500' - } - } - const isAnswered = userAnswers[question.id] !== undefined return ( diff --git a/src/features/resource/quiz/components/player/question/types/TrueFalseQuestion.tsx b/src/features/resource/quiz/components/player/question/types/TrueFalseQuestion.tsx index 0f946be4c..71351c290 100644 --- a/src/features/resource/quiz/components/player/question/types/TrueFalseQuestion.tsx +++ b/src/features/resource/quiz/components/player/question/types/TrueFalseQuestion.tsx @@ -19,87 +19,102 @@ export default function TrueFalseQuestion({ question }: TrueFalseQuestionProps) const currentSelected = userAnswers[question.id]?.[0] return ( -
- {[trueAnswer, falseAnswer].map((ans) => { - if (!ans) return null - const isChosen = currentSelected === ans.id - const isCorrect = ans.isCorrect - const isTrue = ans.content === 'True' +
+ {/* Question Content */} +
+
+
+ {question.orderIndex || 1} +
+
+

{question.content}

+
+
+
- let containerClass = 'group relative overflow-hidden transition-all duration-300' + {/* True/False Options */} +
+ {[trueAnswer, falseAnswer].map((ans) => { + if (!ans) return null + const isChosen = currentSelected === ans.id + const isCorrect = ans.isCorrect + const isTrue = ans.content === 'True' - if (isSubmitted) { - if (isCorrect) { - containerClass += isTrue - ? ' border-2 border-green-500 bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-xl' - : ' border-2 border-red-500 bg-gradient-to-br from-red-500 to-pink-600 text-white shadow-xl' - } else if (isChosen && !isCorrect) { - containerClass += ' border-2 border-gray-400 bg-gray-100 opacity-70' - } else { - containerClass += ' border-2 border-gray-200 bg-white opacity-50' - } - } else { - if (isChosen) { - containerClass += isTrue - ? ' border-2 border-green-600 bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-xl scale-105' - : ' border-2 border-red-600 bg-gradient-to-br from-red-500 to-pink-600 text-white shadow-xl scale-105' + let containerClass = 'group relative overflow-hidden transition-all duration-300' + + if (isSubmitted) { + if (isCorrect) { + containerClass += isTrue + ? ' border-2 border-green-500 bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-xl' + : ' border-2 border-red-500 bg-gradient-to-br from-red-500 to-pink-600 text-white shadow-xl' + } else if (isChosen && !isCorrect) { + containerClass += ' border-2 border-gray-400 bg-gray-100 opacity-70' + } else { + containerClass += ' border-2 border-gray-200 bg-white opacity-50' + } } else { - containerClass += isTrue - ? ' border-2 border-green-200 bg-gradient-to-br from-green-50 to-emerald-50 hover:border-green-400 hover:shadow-lg hover:scale-105' - : ' border-2 border-red-200 bg-gradient-to-br from-red-50 to-pink-50 hover:border-red-400 hover:shadow-lg hover:scale-105' + if (isChosen) { + containerClass += isTrue + ? ' border-2 border-green-600 bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-xl scale-105' + : ' border-2 border-red-600 bg-gradient-to-br from-red-500 to-pink-600 text-white shadow-xl scale-105' + } else { + containerClass += isTrue + ? ' border-2 border-green-200 bg-gradient-to-br from-green-50 to-emerald-50 hover:border-green-400 hover:shadow-lg hover:scale-105' + : ' border-2 border-red-200 bg-gradient-to-br from-red-50 to-pink-50 hover:border-red-400 hover:shadow-lg hover:scale-105' + } } - } - - return ( - - ) - })} + + {/* Status */} + {isSubmitted && isCorrect && ( + + ✓ Đáp án đúng + + )} + {isSubmitted && isChosen && !isCorrect && ( + + ✗ Bạn đã chọn + + )} +
+ + ) + })} +
) } From c62f982b103d8b054703550f3e204c5c4d7fa082 Mon Sep 17 00:00:00 2001 From: meewaldor Date: Thu, 27 Nov 2025 21:42:31 +0700 Subject: [PATCH 2/2] feat(classroom): add grade field to classroom forms and enhance class code generation --- messages/en/classroom/en_classroom.json | 1 + messages/vi/classroom/vi_classroom.json | 1 + .../components/upsert/ClassroomBasicInfo.tsx | 69 ++++++++++++++++--- .../components/upsert/UpsertClassroom.tsx | 5 -- 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/messages/en/classroom/en_classroom.json b/messages/en/classroom/en_classroom.json index b31f79e36..c2f5c6c77 100644 --- a/messages/en/classroom/en_classroom.json +++ b/messages/en/classroom/en_classroom.json @@ -16,6 +16,7 @@ "description": "Description", "descriptionPlaceholder": "Brief description of this classroom...", "gradeLevel": "Grade Level", + "grade": "Grade", "duration": "Duration", "selectDuration": "Select Duration", "startDate": "Start Date", diff --git a/messages/vi/classroom/vi_classroom.json b/messages/vi/classroom/vi_classroom.json index 12991308e..a255c54d3 100644 --- a/messages/vi/classroom/vi_classroom.json +++ b/messages/vi/classroom/vi_classroom.json @@ -79,6 +79,7 @@ "description": "Mô tả", "descriptionPlaceholder": "Mô tả ngắn gọn về lớp học này...", "gradeLevel": "Cấp lớp", + "grade": "Lớp", "selectGrade": "Chọn cấp lớp", "duration": "Thời lượng", "selectDuration": "Chọn thời lượng", diff --git a/src/features/classroom/components/upsert/ClassroomBasicInfo.tsx b/src/features/classroom/components/upsert/ClassroomBasicInfo.tsx index 421e5e16b..8365e6497 100644 --- a/src/features/classroom/components/upsert/ClassroomBasicInfo.tsx +++ b/src/features/classroom/components/upsert/ClassroomBasicInfo.tsx @@ -8,22 +8,40 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Calendar } from '@/components/shadcn/calendar' import { Popover, PopoverContent, PopoverTrigger } from '@/components/shadcn/popover' import { Button } from '@/components/shadcn/button' -import { CalendarIcon } from 'lucide-react' +import { CalendarIcon, RefreshCw } from 'lucide-react' import { format } from 'date-fns' import { cn } from '@/utils/shadcn/utils' import { useTranslations } from 'next-intl' +import { Grade } from '@/features/classroom/types/classroom.type' type ClassroomBasicInfoProps = { form: any organizationSubscriptionData: any - gradeOptions: { label: string; value: string }[] minDate: Date | undefined maxDate: Date | undefined } -export default function ClassroomBasicInfo({ form, gradeOptions, minDate, maxDate }: ClassroomBasicInfoProps) { +// Generate random class code like Google Meet (xxx-yyyy-zzz) +const generateClassCode = () => { + const chars = 'abcdefghijklmnopqrstuvwxyz' + const randomString = (length: number) => { + let result = '' + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return result + } + return `${randomString(3)}-${randomString(4)}-${randomString(3)}` +} + +export default function ClassroomBasicInfo({ form, minDate, maxDate }: ClassroomBasicInfoProps) { const tClassroom = useTranslations('classroom.create.step1') + const gradeOptions = Object.values(Grade).map((g) => ({ + label: g.replace('Grade', 'Lớp'), + value: g.replace('Grade', 'Lớp') + })) + const DURATION_OPTIONS = [ { label: `4 ${tClassroom('weeks')}`, value: '4' }, { label: `6 ${tClassroom('weeks')}`, value: '6' }, @@ -42,10 +60,21 @@ export default function ClassroomBasicInfo({ form, gradeOptions, minDate, maxDat const isCustomDuration = durationWeeks === 'custom' + // Generate initial class code + useEffect(() => { + const formClassCode = form.getFieldValue('classCode') + if (!formClassCode) { + const newCode = generateClassCode() + setClassCode(newCode) + form.setFieldValue('classCode', newCode) + } else { + setClassCode(formClassCode) + } + }, []) + // Sync với form khi mount (cho trường hợp edit) useEffect(() => { const formName = form.getFieldValue('name') - const formClassCode = form.getFieldValue('classCode') const formGrade = form.getFieldValue('grade') const formDescription = form.getFieldValue('description') const formDuration = form.getFieldValue('durationWeeks') @@ -53,7 +82,6 @@ export default function ClassroomBasicInfo({ form, gradeOptions, minDate, maxDat const formEndDate = form.getFieldValue('endDate') if (formName) setName(formName) - if (formClassCode) setClassCode(formClassCode) if (formGrade) setGrade(formGrade) if (formDescription) setDescription(formDescription) if (formDuration) setDurationWeeks(formDuration) @@ -107,6 +135,12 @@ export default function ClassroomBasicInfo({ form, gradeOptions, minDate, maxDat } }, [endDate]) + const handleRegenerateCode = () => { + const newCode = generateClassCode() + setClassCode(newCode) + form.setFieldValue('classCode', newCode) + } + return (
@@ -127,12 +161,25 @@ export default function ClassroomBasicInfo({ form, gradeOptions, minDate, maxDat - setClassCode(e.target.value)} - /> +
+ + +
+

Auto-generated code • Click refresh to generate new

diff --git a/src/features/classroom/components/upsert/UpsertClassroom.tsx b/src/features/classroom/components/upsert/UpsertClassroom.tsx index ef2e2ba9a..c39ea0f61 100644 --- a/src/features/classroom/components/upsert/UpsertClassroom.tsx +++ b/src/features/classroom/components/upsert/UpsertClassroom.tsx @@ -140,10 +140,6 @@ export default function UpsertClassroom({ classroomId, onSuccess }: UpsertClassr ) const teacherOptions = getOptions(teacherData?.data.items, 'userName', 'imageUrl', 'email') - const gradeOptions = Object.entries(Grade).map(([key, value]) => ({ - label: value, - value: value - })) const [createClassroom, { isLoading: isCreating }] = useCreateClassroomMutation() const [updateClassroom, { isLoading: isUpdating }] = useUpdateClassroomMutation() @@ -230,7 +226,6 @@ export default function UpsertClassroom({ classroomId, onSuccess }: UpsertClassr