Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions messages/en/classroom/en_classroom.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions messages/vi/classroom/vi_classroom.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
69 changes: 58 additions & 11 deletions src/features/classroom/components/upsert/ClassroomBasicInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -42,18 +60,28 @@ 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')
const formStartDate = form.getFieldValue('startDate')
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)
Expand Down Expand Up @@ -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 (
<div className='animate-fadeIn mx-auto max-w-6xl space-y-6'>
<div className='rounded-lg border bg-white p-6 shadow-sm'>
Expand All @@ -127,12 +161,25 @@ export default function ClassroomBasicInfo({ form, gradeOptions, minDate, maxDat
<Label htmlFor='classCode'>
{tClassroom('classCode')} <span className='text-red-500'>*</span>
</Label>
<Input
id='classCode'
placeholder='e.g., STEM-1A-2025'
value={classCode}
onChange={(e) => setClassCode(e.target.value)}
/>
<div className='flex gap-2'>
<Input
id='classCode'
placeholder='e.g., abc-defg-hij'
value={classCode}
readOnly
className='flex-1 bg-gray-50 font-mono'
/>
<Button
type='button'
variant='outline'
size='icon'
onClick={handleRegenerateCode}
title='Generate new code'
>
<RefreshCw className='h-4 w-4' />
</Button>
</div>
<p className='text-xs text-gray-500'>Auto-generated code • Click refresh to generate new</p>
</div>

<div className='space-y-2'>
Expand Down
5 changes: 0 additions & 5 deletions src/features/classroom/components/upsert/UpsertClassroom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -230,7 +226,6 @@ export default function UpsertClassroom({ classroomId, onSuccess }: UpsertClassr
<ClassroomBasicInfo
form={form}
organizationSubscriptionData={organizationSubscriptionData}
gradeOptions={gradeOptions}
minDate={minDate}
maxDate={maxDate}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,87 +19,102 @@ export default function TrueFalseQuestion({ question }: TrueFalseQuestionProps)
const currentSelected = userAnswers[question.id]?.[0]

return (
<div className='flex flex-col gap-4 sm:flex-row'>
{[trueAnswer, falseAnswer].map((ans) => {
if (!ans) return null
const isChosen = currentSelected === ans.id
const isCorrect = ans.isCorrect
const isTrue = ans.content === 'True'
<div className='space-y-6'>
{/* Question Content */}
<div className='space-y-4'>
<div className='flex items-start gap-3'>
<div className='flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 text-sm font-semibold text-blue-700'>
{question.orderIndex || 1}
</div>
<div className='flex-1'>
<h3 className='text-lg font-medium text-gray-900'>{question.content}</h3>
</div>
</div>
</div>

let containerClass = 'group relative overflow-hidden transition-all duration-300'
{/* True/False Options */}
<div className='flex flex-col gap-4 sm:flex-row'>
{[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 (
<Button
key={ans.id}
onClick={() => {
if (!isSubmitted) {
dispatch(setUserAnswer({ questionId: question.id, answer: ans.id }))
}
}}
variant='outline'
disabled={isSubmitted}
className={`${containerClass} flex-1 px-8 py-8 transition-all duration-300`}
>
<div className='flex flex-col items-center gap-3 text-center'>
{/* Icon */}
<div
className={`flex h-16 w-16 items-center justify-center rounded-full transition-all ${
isChosen || (isSubmitted && isCorrect)
? 'bg-white/30 shadow-lg'
: isTrue
? 'bg-green-200 text-green-700'
: 'bg-red-200 text-red-700'
}`}
>
{isTrue ? <Check className='h-10 w-10' /> : <X className='h-10 w-10' />}
</div>

{/* Label */}
<span
className={`text-xl font-bold ${
isChosen || (isSubmitted && isCorrect) ? 'text-white' : isTrue ? 'text-green-700' : 'text-red-700'
}`}
>
{isTrue ? 'ĐÚNG' : 'SAI'}
</span>
return (
<Button
key={ans.id}
onClick={() => {
if (!isSubmitted) {
dispatch(setUserAnswer({ questionId: question.id, answer: ans.id }))
}
}}
variant='outline'
disabled={isSubmitted}
className={`${containerClass} flex-1 px-8 py-20 transition-all duration-300`}
>
<div className='flex flex-col items-center gap-3 text-center'>
{/* Icon */}
<div
className={`flex h-16 w-16 items-center justify-center rounded-full transition-all ${
isChosen || (isSubmitted && isCorrect)
? 'bg-white/30 shadow-lg'
: isTrue
? 'bg-green-200 text-green-700'
: 'bg-red-200 text-red-700'
}`}
>
{isTrue ? <Check className='h-10 w-10' /> : <X className='h-10 w-10' />}
</div>

{/* Status */}
{isSubmitted && isCorrect && (
<span className='rounded-full bg-white/90 px-3 py-1 text-xs font-semibold text-gray-700'>
✓ Đáp án đúng
</span>
)}
{isSubmitted && isChosen && !isCorrect && (
<span className='rounded-full bg-white/90 px-3 py-1 text-xs font-semibold text-gray-700'>
✗ Bạn đã chọn
{/* Label */}
<span
className={`text-xl font-bold ${
isChosen || (isSubmitted && isCorrect) ? 'text-white' : isTrue ? 'text-green-700' : 'text-red-700'
}`}
>
{isTrue ? 'ĐÚNG' : 'SAI'}
</span>
)}
</div>
</Button>
)
})}

{/* Status */}
{isSubmitted && isCorrect && (
<span className='rounded-full bg-white/90 px-3 py-1 text-xs font-semibold text-gray-700'>
✓ Đáp án đúng
</span>
)}
{isSubmitted && isChosen && !isCorrect && (
<span className='rounded-full bg-white/90 px-3 py-1 text-xs font-semibold text-gray-700'>
✗ Bạn đã chọn
</span>
)}
</div>
</Button>
)
})}
</div>
</div>
)
}