diff --git a/apps/upskii/messages/en.json b/apps/upskii/messages/en.json index 1d80dd92f6..858e5d67a7 100644 --- a/apps/upskii/messages/en.json +++ b/apps/upskii/messages/en.json @@ -3832,26 +3832,169 @@ "ws-quiz-sets": { "plural": "Quiz Sets", "singular": "Quiz Set", + "name": "Quiz set name", "description": "Quiz sets are used to group quizzes together for learning purposes.", - "create": "Create quiz set", "create_description": "Create a new quiz set", - "name": "Set name", - "edit": "Edit set", "link-quizzes": "Link Quizzes", - "link-modules": "Link modules" + "link-modules": "Link modules", + "editing": "Updating...", + "creating": "Creating...", + "create": "Create Quiz Set", + "edit": "Edit Quiz Set", + "form-sections": { + "basic": { + "title": "Basic Information", + "subtitle": "Set up the fundamental details of your quiz" + }, + "timing-limit": { + "title": "Basic Information", + "subtitle": "Set up the fundamental details of your quiz" + }, + "schedule": { + "title": "Schedule", + "subtitle": "Set availability and due dates for the quiz" + }, + "settings": { + "title": "Quiz Settings", + "subtitle": "Configure quiz behavior and student permissions" + } + }, + "form-description": "Configure your quiz settings, timing, and permissions to create an engaging learning experience for your students.", + "required-badge": "Required", + "form-fields": { + "name": { + "title": "Set Name", + "description": "Choose a clear, descriptive name for your quiz", + "placeholder": "e.g., Chapter 5 Assessment" + }, + "attempt_limit": { + "title": "Attempt Limit", + "description": "Maximum number of times a user can attempt the quiz. Leave 0 for unlimited.", + "placeholder": "e.g., 3" + }, + "time_limit_minutes": { + "title": "Time Limit (minutes)", + "description": "The time allowed to complete the quiz in minutes. Leave 0 for no time limit.", + "placeholder": "e.g, 60" + }, + "available_date": { + "title": "Available From", + "description": "Date and time when the quiz becomes available to participants." + }, + "due_date": { + "title": "Due Date", + "description": "Deadline for submitting the quiz." + }, + "explanation_mode": { + "title": "Explanation Mode", + "description": "Choose when and how students can see answer explanations", + "placeholder": "Select explanation timing", + "select_never": "Never show answer explanations", + "select_correct_answer": "Only show explanation of correct answers", + "select_all_answer": "Show explanation of all answers" + }, + "instruction": { + "title": "Instructions", + "description": "Provide detailed instructions for students using the rich text editor, displayed before starting the quiz.", + "save": "Save Instruction", + "saving": "Saving..." + }, + "allow_view_results": { + "title": "Allow Immediate Results", + "description": "Enable users to see their score immediately upon submission." + }, + "results_released": { + "title": "Results Released", + "description": "Mark quiz results as released, allowing detailed feedback and explanations." + }, + "allow_view_old_attempts": { + "title": "Allow Review Old Attempts", + "description": "Revisit users' previous quiz attempts. Before results are released, users can view only their submitted answers. Once results are released, they'll see their answers along with detailed results and feedback." + } + } }, "ws-quizzes": { "plural": "Quizzes", "singular": "Quiz", "create": "Create quiz", + "create_multiple": "Add questions", "create_description": "Create a new quiz", "description": "Manage quizzes in your workspace that have multiple choice questions for learning purposes.", + "edit_description": "Edit a quiz", "edit": "Edit quiz", + "edit_all": "Edit all quizzes", "question": "Question", "answer": "Answer", "points": "Points", "submitting": "Submitting...", "submit": "Submit", + "form": { + "edit-title": "Edit Quiz Questions", + "create-title": "Create Quiz Questions", + "create-description": "Create multiple quiz questions at once. Each question can have multiple options with explanations. The user can also copy and paste quiz questions", + "edit-description": "Edit or add more multiple quiz questions at once. Each question can have multiple options with explanations. The user can also copy and paste quiz questions", + "question-no": "Question {no}", + "option-no": "Option {no}", + "untitled-question": "Untitled Question", + "content": { + "options": { + "label": "Answer Options", + "add-button": "Add Option", + "placeholder": "Option {no}", + "total-options-text": "Total Options", + "correct-label": "Correct", + "explanation-label": "Explanation", + "generate-explanation-button": "Generate", + "explanation-placeholder": "Explain why this option is correct or incorrect..." + }, + "questions": { + "label": "Question", + "placeholder": "Enter your question here...", + "count-many": "{count} Questions", + "count-one": "{count} Question" + }, + "correct-answers": { + "no-selected": "No correct answer selected", + "single-selected": "Single correct answer", + "multiple-selected": "Multiple correct answers" + } + }, + "paste-confirm-modal": { + "title": "Confirm Paste", + "description-plural": "You are about to paste {length} quiz questions. Review the content below and choose an action.", + "description-single": "You are about to paste 1 quiz question. Review the content below and choose an action.", + "discard-button": "Discard", + "confirm-button": "Confirm Paste" + }, + "add-another-button": "Add Another Question", + "paste-question-button": "Paste Question", + "create-button": "Create All Questions", + "edit-button": "Edit All Questions", + "creating": "Creating...", + "updating": "Updating...", + "error": { + "title": "Error", + "fail-update": "Failed to update quiz", + "fail-create": "Failed to create quiz", + "unexpected": "An unexpected error occurred", + "invalid-clipboard-title": "Error", + "invalid-clipboard": "Invalid quiz format", + "invalid-clipboard-message": "Failed to paste quiz data. Please ensure you have valid quiz data in your clipboard.", + "explanation-generation-failed": "Failed to generate explanation", + "quiz-copy-failed": "Failed to copy quiz to clipboard" + }, + "success": { + "title": "Success", + "edit-quizzes": "Successfully updated {length} quizzes", + "edit-quiz": "Successfully updated {length} quiz", + "create-quizzes": "Successfully created {length} quizzes", + "create-quiz": "Successfully created {length} quiz", + "quiz-copied": "Quiz copied to clipboard", + "paste-success-many": "Pasted {length} quizzes succesfully", + "paste-success-one": "Pasted 1 quiz succesfully", + "explanation-generated": "Explanation generated successfully" + } + }, "quiz-status": { "sidebar_aria": "Quiz progress sidebar", "question_status_title": "Question Progress", @@ -3860,7 +4003,11 @@ "question_navigation_label": "Jump to question", "answered_state": "Answered", "unanswered_state": "Unanswered", - "jump_to_question": "Jump to question" + "jump_to_question": "Jump to question", + "upcoming": "Upcoming", + "active": "Active", + "expired": "Expired", + "draft": "Draft" }, "time": { "remaining": "Time Remaining", diff --git a/apps/upskii/messages/vi.json b/apps/upskii/messages/vi.json index d0b947f047..6900055995 100644 --- a/apps/upskii/messages/vi.json +++ b/apps/upskii/messages/vi.json @@ -3836,28 +3836,168 @@ "create_description": "Tạo một hàng chờ mới" }, "ws-quiz-sets": { - "plural": "Bộ trắc nghiệm", - "singular": "Bộ trắc nghiệm", - "description": "Quản lý các bộ trắc nghiệm trong không gian làm việc của bạn bao gồm các câu hỏi nhiều lựa chọn dành cho mục đích học tập.", - "create": "Tạo bộ trắc nghiệm", - "create_description": "Tạo bộ trắc nghiệm mới", - "name": "Tên bộ trắc nghiệm", - "edit": "Chỉnh sửa bộ", - "link-quizzes": "Liên kết câu hỏi", - "link-modules": "Liên kết mô-đun" + "plural": "Bộ câu hỏi", + "singular": "Bộ câu hỏi", + "name": "Tên bộ câu hỏi", + "description": "Bộ câu hỏi được sử dụng để nhóm các câu quiz lại với nhau cho mục đích học tập.", + "create_description": "Tạo một bộ câu hỏi mới", + "link-quizzes": "Liên kết bài kiểm tra", + "link-modules": "Liên kết học phần", + "editing": "Đang cập nhật...", + "creating": "Đang tạo...", + "create": "Tạo bộ câu hỏi", + "edit": "Chỉnh sửa bộ câu hỏi", + "form-sections": { + "basic": { + "title": "Thông tin cơ bản", + "subtitle": "Thiết lập các chi tiết cơ bản cho bài kiểm tra của bạn" + }, + "timing-limit": { + "title": "Thông tin cơ bản", + "subtitle": "Thiết lập các chi tiết cơ bản cho bài kiểm tra của bạn" + }, + "schedule": { + "title": "Lịch trình", + "subtitle": "Đặt thời gian khả dụng và thời hạn cho bài kiểm tra" + }, + "settings": { + "title": "Cài đặt bài kiểm tra", + "subtitle": "Cấu hình hành vi bài kiểm tra và quyền của sinh viên" + } + }, + "form-description": "Cấu hình cài đặt, thời gian và quyền của bài kiểm tra để tạo trải nghiệm học tập hấp dẫn cho sinh viên của bạn.", + "required-badge": "Bắt buộc", + "form-fields": { + "name": { + "title": "Tên bộ câu hỏi", + "description": "Chọn một tên rõ ràng, mô tả cho bài kiểm tra của bạn", + "placeholder": "Ví dụ: Kiểm tra Chương 5" + }, + "attempt_limit": { + "title": "Giới hạn số lần làm bài", + "description": "Số lần tối đa người dùng có thể làm bài kiểm tra. Để 0 nếu không giới hạn.", + "placeholder": "Ví dụ: 3" + }, + "time_limit_minutes": { + "title": "Giới hạn thời gian (phút)", + "description": "Thời gian cho phép hoàn thành bài kiểm tra tính bằng phút. Để 0 nếu không giới hạn thời gian.", + "placeholder": "Ví dụ: 60" + }, + "available_date": { + "title": "Có hiệu lực từ", + "description": "Ngày và giờ bài kiểm tra có sẵn cho người tham gia." + }, + "due_date": { + "title": "Ngày hết hạn", + "description": "Thời hạn nộp bài kiểm tra." + }, + "explanation_mode": { + "title": "Chế độ giải thích", + "description": "Chọn thời điểm và cách học sinh có thể xem giải thích câu trả lời", + "placeholder": "Chọn thời điểm giải thích", + "select_never": "Không bao giờ hiển thị giải thích câu trả lời", + "select_correct_answer": "Chỉ hiển thị giải thích các câu trả lời đúng", + "select_all_answer": "Hiển thị giải thích của tất cả các câu trả lời" + }, + "instruction": { + "title": "Hướng dẫn", + "description": "Cung cấp hướng dẫn chi tiết cho học sinh bằng trình soạn thảo văn bản đa dạng thức, hiển thị trước khi bắt đầu bài kiểm tra." + }, + "allow_view_results": { + "title": "Cho phép xem kết quả ngay lập tức", + "description": "Cho phép người dùng xem điểm của họ ngay sau khi nộp bài." + }, + "results_released": { + "title": "Đã công bố kết quả", + "description": "Đánh dấu kết quả bài kiểm tra đã công bố, cho phép xem phản hồi chi tiết và giải thích." + }, + "allow_view_old_attempts": { + "title": "Cho phép xem lại các lần làm bài cũ", + "description": "Cho phép người dùng xem lại các lần làm bài kiểm tra trước đó của họ. Trước khi kết quả được công bố, người dùng chỉ có thể xem các câu trả lời đã nộp của mình. Khi kết quả được công bố, họ sẽ thấy các câu trả lời cùng với kết quả chi tiết và phản hồi." + } + } }, "ws-quizzes": { "plural": "Câu hỏi trắc nghiệm", "singular": "Bộ trắc nghiệm", "create": "Tạo câu hỏi", + "edit_description": "Chỉnh sửa bộ trắc nghiệm", "create_description": "Tạo một bộ trắc nghiệm mới", "description": "Quản lý bộ trắc nghiệm trong không gian làm việc của bạn có các câu hỏi nhiều lựa chọn dành cho mục đích học tập.", "edit": "Chỉnh sửa bộ trắc nghiệm", + "edit-all": "Chỉnh sửa toàn bộ", "question": "Câu hỏi", "answer": "Câu trả lời", - "points": "Points", - "submitting": "Submitting...", - "submit": "Submit", + "points": "Điểm", + "submitting": "Đang nộp...", + "submit": "Nộp", + "form": { + "edit-title": "Chỉnh sửa câu hỏi đố vui", + "create-title": "Tạo câu hỏi đố vui", + "create-description": "Tạo nhiều câu hỏi đố vui cùng lúc. Mỗi câu hỏi có thể có nhiều lựa chọn với giải thích. Người dùng cũng có thể sao chép và dán câu hỏi đố vui", + "edit-description": "Chỉnh sửa hoặc thêm nhiều câu hỏi đố vui cùng lúc. Mỗi câu hỏi có thể có nhiều lựa chọn với giải thích. Người dùng cũng có thể sao chép và dán câu hỏi đố vui", + "question-no": "Câu hỏi {no}", + "option-no": "Tùy chọn {no}", + "untitled-question": "Câu hỏi không có tiêu đề", + "content": { + "options": { + "label": "Tùy chọn trả lời", + "add-button": "Thêm tùy chọn", + "placeholder": "Tùy chọn {no}", + "total-options-text": "Tổng số tùy chọn", + "correct-label": "Đúng", + "explanation-label": "Giải thích", + "generate-explanation-button": "Tạo", + "explanation-placeholder": "Giải thích tại sao tùy chọn này đúng hoặc sai..." + }, + "questions": { + "label": "Câu hỏi", + "placeholder": "Nhập câu hỏi của bạn vào đây...", + "count-many": "{count} Câu hỏi", + "count-one": "{count} Câu hỏi" + }, + "correct-answers": { + "no-selected": "Chưa chọn câu trả lời đúng", + "single-selected": "Một câu trả lời đúng", + "multiple-selected": "Nhiều câu trả lời đúng" + } + }, + "paste-confirm-modal": { + "title": "Xác nhận dán", + "description-plural": "Bạn sắp dán {length} câu hỏi đố vui. Xem lại nội dung bên dưới và chọn hành động.", + "description-single": "Bạn sắp dán 1 câu hỏi đố vui. Xem lại nội dung bên dưới và chọn hành động.", + "discard-button": "Hủy bỏ", + "confirm-button": "Xác nhận dán" + }, + "add-another-button": "Thêm câu hỏi khác", + "paste-question-button": "Dán câu hỏi", + "create-button": "Tạo tất cả câu hỏi", + "edit-button": "Chỉnh sửa tất cả câu hỏi", + "creating": "Đang tạo...", + "updating": "Đang cập nhật...", + "error": { + "title": "Lỗi", + "fail-update": "Cập nhật câu đố thất bại", + "fail-create": "Tạo câu đố thất bại", + "unexpected": "Đã xảy ra lỗi không mong muốn", + "invalid-clipboard-title": "Lỗi", + "invalid-clipboard": "Định dạng câu đố không hợp lệ", + "invalid-clipboard-message": "Không thể dán dữ liệu câu đố. Vui lòng đảm bảo bạn có dữ liệu câu đố hợp lệ trong khay nhớ tạm.", + "explanation-generation-failed": "Không tạo được giải thích", + "quiz-copy-failed": "Sao chép câu đố vào khay nhớ tạm thất bại" + }, + "success": { + "title": "Thành công", + "edit-quizzes": "Đã cập nhật thành công {length} câu đố", + "edit-quiz": "Đã cập nhật thành công {length} câu đố", + "create-quizzes": "Đã tạo thành công {length} câu đố", + "create-quiz": "Đã tạo thành công {length} câu đố", + "quiz-copied": "Đã sao chép câu đố vào khay nhớ tạm", + "paste-success-many": "Đã dán thành công {length} câu đố", + "paste-success-one": "Đã dán thành công 1 câu đố", + "explanation-generated": "Giải thích đã được tạo thành công" + } + }, "quiz-status": { "sidebar_aria": "Thanh bên tiến độ bài kiểm tra", "question_status_title": "Tiến độ câu hỏi", @@ -3866,7 +4006,11 @@ "question_navigation_label": "Chuyển đến câu hỏi", "answered_state": "Đã trả lời", "unanswered_state": "Chưa trả lời", - "jump_to_question": "Chuyển đến câu hỏi" + "jump_to_question": "Chuyển đến câu hỏi", + "upcoming": "Sắp diễn ra", + "active": "Đang hoạt động", + "expired": "Đã hết hạn", + "draft": "Nháp" }, "time": { "remaining": "Thời gian còn lại", diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx index a83d931859..5667ab7c48 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx @@ -1,5 +1,5 @@ +import ClientQuizzes from '../../../../../../../../components/quiz/client-quizzes'; import ClientFlashcards from './flashcards/client-flashcards'; -import ClientQuizzes from './quizzes/client-quizzes'; import { extractYoutubeId } from '@/utils/url-helper'; import { createClient, diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/[setId]/take/before-taking-quiz-whole.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/[setId]/take/before-taking-quiz-whole.tsx index 9ca924fcc7..f82c8341fd 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/[setId]/take/before-taking-quiz-whole.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/[setId]/take/before-taking-quiz-whole.tsx @@ -118,8 +118,6 @@ export default function BeforeTakingQuizWhole({ const canViewTotalPointsOnly = quizData.resultsReleased; - console.log('Test', quizData.attempts[0]); - // const canViewOldAttemptsResults = quizData.resultsReleased; // can view attempts with points in detailed explanation const canViewOldAttemptsResults = diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/columns.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/columns.tsx index ab5264bfa4..becc572208 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/columns.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/columns.tsx @@ -86,6 +86,7 @@ export const getQuizSetColumns = ( ), diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/create/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/create/page.tsx new file mode 100644 index 0000000000..8a2d610c8b --- /dev/null +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/create/page.tsx @@ -0,0 +1,17 @@ +import QuizSetForm from '@/components/quiz/quiz-set-form'; + +interface Props { + params: Promise<{ + wsId: string; + moduleId: string; + }>; +} + +export default async function Page({ params }: Props) { + const { wsId, moduleId } = await params; + return ( +
+ +
+ ); +} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/form.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/form.tsx deleted file mode 100644 index bdc2554fc8..0000000000 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/form.tsx +++ /dev/null @@ -1,116 +0,0 @@ -'use client'; - -import { type WorkspaceQuizSet } from '@tuturuuu/types/db'; -import { Button } from '@tuturuuu/ui/button'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@tuturuuu/ui/form'; -import { useForm } from '@tuturuuu/ui/hooks/use-form'; -import { toast } from '@tuturuuu/ui/hooks/use-toast'; -import { Input } from '@tuturuuu/ui/input'; -import { zodResolver } from '@tuturuuu/ui/resolvers'; -import { useTranslations } from 'next-intl'; -import { useRouter } from 'next/navigation'; -import * as z from 'zod'; - -interface Props { - wsId: string; - moduleId: string; - data?: WorkspaceQuizSet; - // eslint-disable-next-line no-unused-vars - onFinish?: (data: z.infer) => void; -} - -const FormSchema = z.object({ - id: z.string().optional(), - name: z.string().min(1), - moduleId: z.string(), -}); - -export default function CourseModuleForm({ - wsId, - moduleId, - data, - onFinish, -}: Props) { - const t = useTranslations(); - const router = useRouter(); - - const form = useForm({ - resolver: zodResolver(FormSchema), - values: { - id: data?.id, - name: data?.name || '', - moduleId, - }, - }); - - const isDirty = form.formState.isDirty; - const isValid = form.formState.isValid; - const isSubmitting = form.formState.isSubmitting; - - const disabled = !isDirty || !isValid || isSubmitting; - - const onSubmit = async (data: z.infer) => { - try { - const res = await fetch( - data.id - ? `/api/v1/workspaces/${wsId}/quiz-sets/${data.id}` - : `/api/v1/workspaces/${wsId}/quiz-sets`, - { - method: data.id ? 'PUT' : 'POST', - body: JSON.stringify(data), - } - ); - - if (res.ok) { - onFinish?.(data); - router.refresh(); - } else { - const data = await res.json(); - toast({ - title: `Failed to ${data.id ? 'edit' : 'create'} course module`, - description: data.message, - }); - } - } catch (error) { - toast({ - title: `Failed to ${data.id ? 'edit' : 'create'} course module`, - description: error instanceof Error ? error.message : String(error), - }); - } - }; - - return ( -
- - ( - - {t('ws-quiz-sets.name')} - - - - - - )} - /> - - - - - ); -} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/instruction-editor.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/instruction-editor.tsx new file mode 100644 index 0000000000..acc0c9a1a0 --- /dev/null +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/instruction-editor.tsx @@ -0,0 +1,53 @@ +'use client'; + +import type { JSONContent } from '@tiptap/react'; +import { Button } from '@tuturuuu/ui/button'; +import { RichTextEditor } from '@tuturuuu/ui/text-editor/editor'; +import { useTranslations } from 'next-intl'; +import { useState } from 'react'; + +interface InstructionEditorProps { + quizSetId: string; + instruction: JSONContent | null; + setInstruction: (instruction: JSONContent) => void; +} + +export default function InstructionEditor({ + quizSetId, + instruction, + setInstruction, +}: InstructionEditorProps) { + const t = useTranslations(); + const [saving, setSaving] = useState(false); + + const INSTRUCTION_EDITOR_KEY = `instruction-quiz-set-${quizSetId}`; + + const handleSave = async () => { + setSaving(true); + if (instruction) { + localStorage.setItem(INSTRUCTION_EDITOR_KEY, JSON.stringify(instruction)); + } + setSaving(false); + }; + + return ( +
+
+ +
+ +
+ +
+
+ ); +} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/page.tsx index 4ea80f71fe..350f9ba1cb 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quiz-sets/page.tsx @@ -1,5 +1,4 @@ import { getQuizSetColumns } from './columns'; -import CourseModuleForm from './form'; import { CustomDataTable } from '@/components/custom-data-table'; import { createClient } from '@tuturuuu/supabase/next/server'; import { type WorkspaceQuizSet } from '@tuturuuu/types/db'; @@ -46,7 +45,8 @@ export default async function WorkspaceCoursesPage({ singularTitle={t('ws-quiz-sets.singular')} createTitle={t('ws-quiz-sets.create')} createDescription={t('ws-quiz-sets.create_description')} - form={} + // form={} + href={`/${wsId}/courses/${courseId}/modules/${moduleId}/quiz-sets/create`} /> ; } export function QuizSetRowActions({ wsId, + courseId, moduleId, row, }: QuizSetRowActionsProps) { @@ -76,7 +78,14 @@ export function QuizSetRowActions({ - setShowEditDialog(true)}> + {/* setShowEditDialog(true)}> */} + { + router.push( + `/${wsId}/courses/${courseId}/modules/${moduleId}/quiz-sets/${data.id}/edit` + ); + }} + > {t('common.edit')} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quizzes/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quizzes/page.tsx index bd1193d852..775642666d 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quizzes/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/quizzes/page.tsx @@ -1,6 +1,6 @@ +import AIQuizzes from '../../../../../../../../../components/quiz/client-ai'; +import ClientQuizzes from '../../../../../../../../../components/quiz/client-quizzes'; import QuizForm from '../../../../../quizzes/form'; -import AIQuizzes from './client-ai'; -import ClientQuizzes from './client-quizzes'; import { createClient } from '@tuturuuu/supabase/next/server'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; import { ListTodo } from '@tuturuuu/ui/icons'; diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/edit-set/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/edit-set/page.tsx new file mode 100644 index 0000000000..32372995e9 --- /dev/null +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/edit-set/page.tsx @@ -0,0 +1,64 @@ +import QuizSetForm from '@/components/quiz/quiz-set-form'; +import { createClient } from '@tuturuuu/supabase/next/server'; + +interface Props { + params: Promise<{ + wsId: string; + setId: string; + }>; +} + +export default async function Page({ params }: Props) { + const { wsId, setId } = await params; + const setDetails = await getQuizSetDetails(setId); + + console.log('Quiz Set Details:', setDetails); + + if (!setDetails) { + return
Error: Quiz set not found
; + } + + return ( +
+ +
+ ); +} + +async function getQuizSetDetails(setId: string) { + const supabase = await createClient(); + + const queryBuilder = supabase + .from('workspace_quiz_sets') + .select('*, course_module_quiz_sets(module_id)') + .eq('id', setId) + .single(); + const { data, error } = await queryBuilder; + + if (error) { + console.error('Error fetching quiz set details:', error); + return null; + } + + return { + ...data, + id: data?.id, + name: data?.name ?? '', + moduleId: data?.course_module_quiz_sets?.[0]?.module_id ?? undefined, + attemptLimit: data?.attempt_limit ?? null, + timeLimitMinutes: data?.time_limit_minutes ?? null, + allowViewResults: data?.allow_view_results ?? true, + availableDate: data?.available_date + ? data.available_date.toString().slice(0, 16) + : '', + dueDate: data?.due_date ? data.due_date.toString().slice(0, 16) : '', + explanationMode: data?.explanation_mode ?? 0, + instruction: data?.instruction ?? null, + resultsReleased: data?.results_released ?? false, + allowViewOldAttempts: data?.allow_view_old_attempts ?? true, + }; +} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/page.tsx index 7bed4f825a..f4704040e6 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/page.tsx @@ -1,11 +1,30 @@ import { getWorkspaceQuizColumns } from './columns'; -import QuizForm from './form'; import { CustomDataTable } from '@/components/custom-data-table'; import { createClient } from '@tuturuuu/supabase/next/server'; -import type { WorkspaceQuiz } from '@tuturuuu/types/db'; -import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; -import { Separator } from '@tuturuuu/ui/separator'; +import type { WorkspaceQuiz, WorkspaceQuizSet } from '@tuturuuu/types/db'; +import { Badge } from '@tuturuuu/ui/badge'; +import { Button } from '@tuturuuu/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@tuturuuu/ui/card'; +import { + BarChart3, + Calendar, + CheckCircle, + Edit, + FileText, + Plus, + Settings, + Target, + Timer, + XCircle, +} from 'lucide-react'; import { getTranslations } from 'next-intl/server'; +import Link from 'next/link'; interface SearchParams { q?: string; @@ -30,34 +49,377 @@ export default async function WorkspaceQuizzesPage({ const t = await getTranslations(); const { wsId, setId } = await params; - const { data, count } = await getData(setId, await searchParams); + const [quizData, quizSetData] = await Promise.all([ + getQuizData(setId, await searchParams), + getQuizSetData(setId), + ]); + + const { data: quizzes, count: quizCount } = quizData; + const quizSet = quizSetData; + console.log('Quiz Set Data:', quizSet); + + if (!quizSet) { + return ( +
+
+

+ Quiz Set Not Found +

+

+ The requested quiz set could not be found. +

+
+
+ ); + } + + const isActive = + new Date() >= new Date(quizSet.available_date) && + new Date() <= new Date(quizSet.due_date); + const isUpcoming = new Date() < new Date(quizSet.available_date); + const isExpired = new Date() > new Date(quizSet.due_date); + + const getStatusInfo = () => { + if (isUpcoming) + return { + status: 'ws-quizzes.quiz-status.upcoming', + color: 'bg-dynamic-blue', + textColor: 'text-dynamic-light-blue', + bgColor: 'bg-dynamic-blue/20', + }; + if (isActive) + return { + status: 'ws-quizzes.quiz-status.active', + color: 'bg-dynamic-green', + textColor: 'text-dynamic-light-green', + bgColor: 'bg-dynamic-green/20', + }; + if (isExpired) + return { + status: 'ws-quizzes.quiz-status.expired', + color: 'bg-dynamic-light-pink', + textColor: 'text-dynamic-light-pink', + bgColor: 'bg-dynamic-pink/20', + }; + return { + status: 'ws-quizzes.quiz-status.draft', + color: 'bg-muted', + textColor: 'text-muted-foreground', + bgColor: 'bg-muted/20', + }; + }; + + const statusInfo = getStatusInfo(); + + const getExplanationModeText = (mode: number) => { + switch (mode) { + case 0: + return 'ws-quiz-sets.form-fields.explanation_mode.select_never'; + case 1: + return 'ws-quiz-sets.form-fields.explanation_mode.select_correct_answer'; + case 2: + return 'ws-quiz-sets.form-fields.explanation_mode.select_all_answer'; + default: + return 'Unknown'; + } + }; return ( - <> - } - /> - - - +
+
+ {/* Header Section */} +
+
+
+

+ {quizSet.name} +

+ +
+ {t(statusInfo.status)} + +
+

+ Manage and monitor your quiz set with {quizCount} question + {quizCount !== 1 ? 's' : ''} +

+
+
+ + + + + + + + + +
+
+ + {/* Stats Overview */} +
+ + +
+
+

+ Total Questions +

+

+ {quizCount} +

+
+ +
+
+
+ + + +
+
+

+ Attempt Limit +

+

+ {quizSet.attempt_limit || '∞'} +

+
+ +
+
+
+ + + +
+
+

+ Time Limit +

+

+ {quizSet.time_limit_minutes + ? `${quizSet.time_limit_minutes}m` + : '∞'} +

+
+ +
+
+
+ + + +
+
+

+ Status +

+

+ {t(statusInfo.status)} +

+
+ +
+
+
+
+ + {/* Quiz Set Details */} +
+ {/* Schedule Information */} + + + + + Schedule + + Quiz availability and deadlines + + +
+ + Available From + + + {new Date(quizSet.available_date).toLocaleDateString()} at{' '} + {new Date(quizSet.available_date).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} + +
+
+ + Due Date + + + {new Date(quizSet.due_date).toLocaleDateString()} at{' '} + {new Date(quizSet.due_date).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} + +
+
+
+ + {/* Settings Overview */} + + + + + Settings + + Quiz configuration and rules + + +
+ + Explanation Mode + + + {t(getExplanationModeText(quizSet.explanation_mode))} + +
+
+ + View Results + + {quizSet.allow_view_results ? ( + + ) : ( + + )} +
+
+ + Results Released + + {quizSet.results_released ? ( + + ) : ( + + )} +
+
+ + View Old Attempts + + {quizSet.allow_view_old_attempts ? ( + + ) : ( + + )} +
+
+
+ + {/* Instructions */} + + + + + Instructions + + + Student guidelines and information + + + + {quizSet.instruction ? ( +
+
+ {typeof quizSet.instruction === 'string' + ? quizSet.instruction + : JSON.stringify(quizSet.instruction, null, 2)} +
+
+ ) : ( +
+ +

+ No instructions provided +

+ +
+ )} +
+
+
+ + {/* Questions List */} + + + + + Quiz Questions + + + Manage individual questions and their settings + + + + {quizCount > 0 ? ( + + ) : ( +
+ +

+ No Questions Yet +

+

+ Get started by adding your first quiz question. +

+ + + +
+ )} +
+
+
+
); } -async function getData( +async function getQuizData( setId: string, { q, @@ -89,8 +451,26 @@ async function getData( const { data, error, count } = await queryBuilder; if (error) { if (!retry) throw error; - return getData(setId, { q, pageSize, retry: false }); + return getQuizData(setId, { q, pageSize, retry: false }); } return { data, count } as { data: WorkspaceQuiz[]; count: number }; } + +async function getQuizSetData(setId: string) { + const supabase = await createClient(); + + // Try to get moduleId via join if possible + const { data, error } = await supabase + .from('workspace_quiz_sets') + .select('*') + .eq('id', setId) + .single(); + + if (error) { + console.error('Error fetching quiz set:', error); + return null; + } + + return data as WorkspaceQuizSet; +} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/paste-confirm-modal.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/paste-confirm-modal.tsx new file mode 100644 index 0000000000..33c15085a2 --- /dev/null +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/paste-confirm-modal.tsx @@ -0,0 +1,130 @@ +import { Button } from '@tuturuuu/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@tuturuuu/ui/card'; +import { toast } from '@tuturuuu/ui/hooks/use-toast'; +import { CheckCircle, X, XCircle } from '@tuturuuu/ui/icons'; +import { ScrollArea } from '@tuturuuu/ui/scroll-area'; +import { useTranslations } from 'next-intl'; +import React from 'react'; + +export default function PasteConfirmModal({ + pastedQuizzes, + appendQuiz, + setShowPasteConfirm, + setPastedQuizzes, +}: { + pastedQuizzes: any[]; + appendQuiz: (quiz: any) => void; + setShowPasteConfirm: (show: boolean) => void; + setPastedQuizzes: (quizzes: any[]) => void; +}) { + const t = useTranslations('ws-quizzes.form'); + const confirmPaste = () => { + pastedQuizzes.forEach((quiz) => { + const cleanQuiz = { + ...quiz, + id: undefined, // Remove ID to create new quiz + quiz_options: quiz.quiz_options.map((option: any) => ({ + ...option, + id: undefined, // Remove ID to create new options + })), + }; + appendQuiz(cleanQuiz); + }); + + setShowPasteConfirm(false); + setPastedQuizzes([]); + + toast({ + title: t('success.title'), + description: + pastedQuizzes.length > 1 + ? t('success.paste-success-many', { + length: pastedQuizzes.length, + }) + : t('success.paste-success-one'), + }); + }; + + const discardPaste = () => { + setShowPasteConfirm(false); + setPastedQuizzes([]); + }; + return ( +
+ + + + {t('paste-confirm-modal.title')} + + + {pastedQuizzes.length > 1 + ? t('paste-confirm-modal.description-plural', { + length: pastedQuizzes.length, + }) + : t('paste-confirm-modal.description-single')} + + + + +
+ {pastedQuizzes.map((quiz, index) => ( +
+

+ {t('question-no', { + no: index + 1, + })} + : {quiz.question || t('untitled-question')} +

+
+ {quiz.quiz_options?.map((option: any, optIndex: number) => ( +
+ {option.is_correct ? ( + + ) : ( + + )} + + {option.value || + t('option-no', { + no: optIndex + 1, + })} + +
+ ))} +
+
+ ))} +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/quiz-add-card.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/quiz-add-card.tsx new file mode 100644 index 0000000000..e8e4d743d3 --- /dev/null +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/quiz-sets/[setId]/quiz-add-card.tsx @@ -0,0 +1,428 @@ +import { Button } from '@tuturuuu/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@tuturuuu/ui/card'; +import { Checkbox } from '@tuturuuu/ui/checkbox'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@tuturuuu/ui/form'; +import { UseFormReturn } from '@tuturuuu/ui/hooks/use-form'; +import { toast } from '@tuturuuu/ui/hooks/use-toast'; +import { + Check, + CheckCheck, + Copy, + HelpCircle, + Loader2, + PlusCircle, + Trash2, + Wand2, + X, +} from '@tuturuuu/ui/icons'; +import { Input } from '@tuturuuu/ui/input'; +import { ScrollArea } from '@tuturuuu/ui/scroll-area'; +import { Textarea } from '@tuturuuu/ui/textarea'; +import { useTranslations } from 'next-intl'; +import React, { Fragment, useState } from 'react'; +import type { FieldArrayWithId } from 'react-hook-form'; + +type FormReturnType = UseFormReturn< + { + quizzes: { + quiz_options: { + value: string; + is_correct: boolean; + id?: string | undefined; + explanation?: string | undefined; + }[]; + question: string; + id?: string | undefined; + }[]; + moduleId?: string | undefined; + setId?: string | undefined; + }, + unknown, + { + quizzes: { + quiz_options: { + value: string; + is_correct: boolean; + id?: string | undefined; + explanation?: string | undefined; + }[]; + question: string; + id?: string | undefined; + }[]; + moduleId?: string | undefined; + setId?: string | undefined; + } +>; + +type FieldType = FieldArrayWithId< + { + quizzes: { + quiz_options: { + value: string; + is_correct: boolean; + id?: string | undefined; + explanation?: string | undefined; + }[]; + question: string; + id?: string | undefined; + }[]; + moduleId?: string | undefined; + setId?: string | undefined; + }, + 'quizzes', + 'id' +>; + +export default function QuizAddCard({ + wsId, + field, + quizIndex, + form, + removeQuiz, + quizFields, +}: { + wsId: string; + form: FormReturnType; + quizIndex: number; + field: FieldType; + removeQuiz: (index: number) => void; + quizFields: FieldType[]; +}) { + const t = useTranslations('ws-quizzes.form'); + const [loadingStates, setLoadingStates] = useState>( + {} + ); + const generateExplanation = async ( + quizIndex: number, + optionIndex: number + ) => { + const question = form.getValues(`quizzes.${quizIndex}.question`); + const option = form.getValues( + `quizzes.${quizIndex}.quiz_options.${optionIndex}` + ); + + const loadingKey = `${quizIndex}-${optionIndex}`; + setLoadingStates((prev) => ({ ...prev, [loadingKey]: true })); + + try { + const res = await fetch('/api/ai/objects/quizzes/explanation', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ wsId, question, option }), + }); + + if (res.ok) { + const { explanation } = await res.json(); + form.setValue( + `quizzes.${quizIndex}.quiz_options.${optionIndex}.explanation`, + explanation + ); + toast({ + title: t('success.title'), + description: t('success.explanation-generated'), + }); + } else { + throw new Error(t('error.explanation-generation-failed')); + } + } catch (error) { + toast({ + title: t('error.title'), + description: + error instanceof Error + ? error.message + : t('error.explanation-generation-failed'), + variant: 'destructive', + }); + } finally { + setLoadingStates((prev) => ({ ...prev, [loadingKey]: false })); + } + }; + + const addQuizOption = (quizIndex: number) => { + const currentOptions = form.getValues(`quizzes.${quizIndex}.quiz_options`); + form.setValue(`quizzes.${quizIndex}.quiz_options`, [ + ...currentOptions, + { value: '', is_correct: false, explanation: '' }, + ]); + }; + + const removeQuizOption = (quizIndex: number, optionIndex: number) => { + const currentOptions = form.getValues(`quizzes.${quizIndex}.quiz_options`); + if (currentOptions.length > 2) { + const newOptions = currentOptions.filter( + (_, index) => index !== optionIndex + ); + form.setValue(`quizzes.${quizIndex}.quiz_options`, newOptions); + } + }; + + const getCorrectAnswersCount = (quizIndex: number) => { + const options = form.watch(`quizzes.${quizIndex}.quiz_options`); + return options.filter((option) => option.is_correct).length; + }; + + const copyQuizToClipboard = async (quizIndex: number) => { + try { + const quizToCopy = form.getValues(`quizzes.${quizIndex}`); + const clipboardText = JSON.stringify(quizToCopy, null, 2); + await navigator.clipboard.writeText(clipboardText); + toast({ + title: t('success.title'), + description: t('success.quiz-copied'), + }); + } catch (error) { + console.error('Failed to copy quiz:', error); + toast({ + title: t('error.title'), + description: t('error.quiz-copy-failed'), + variant: 'destructive', + }); + } + }; + + const correctAnswers = getCorrectAnswersCount(quizIndex); + const options = form.watch(`quizzes.${quizIndex}.quiz_options`) || []; + + return ( + + +
+
+ +
+ + {t('question-no', { + no: quizIndex + 1, + })} + + + {correctAnswers === 0 && ( + + + {t('content.correct-answers.no-selected')} + + )} + {correctAnswers === 1 && ( + + + {t('content.correct-answers.single-selected')} + + )} + {correctAnswers > 1 && ( + + + {t('content.correct-answers.multiple-selected')} ( + {correctAnswers}) + + )} + +
+
+
+ + {quizFields.length > 1 && ( + + )} +
+
+
+ +
+ {/* Question Input */} + ( + + + {t('content.questions.label')} + + +