@tinyweb_dev/oe-exam-sdk 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/exam-questions.d.ts +1 -1
- package/dist/api/exam-questions.js +1 -1
- package/dist/api/exam-questions.types.d.ts +45 -1
- package/dist/components/exams/CompletionDialog.d.ts +8 -0
- package/dist/components/exams/CompletionDialog.js +1 -0
- package/dist/components/exams/CreateExamInfoForm.d.ts +4 -1
- package/dist/components/exams/CreateExamInfoForm.js +1 -1
- package/dist/components/exams/CreateExamPageContainer.d.ts +3 -0
- package/dist/components/exams/CreateExamPageContainer.js +1 -1
- package/dist/components/exams/ExamCreator.d.ts +9 -3
- package/dist/components/exams/ExamCreator.js +1 -1
- package/dist/components/exams/ExamPartTabs.d.ts +1 -1
- package/dist/components/exams/ExamPartTabs.js +1 -1
- package/dist/components/exams/ExamPreviewDialog.d.ts +20 -0
- package/dist/components/exams/ExamPreviewDialog.js +1 -0
- package/dist/components/exams/ExamQuestionGrid.d.ts +1 -1
- package/dist/components/exams/ExamQuestionGrid.js +1 -1
- package/dist/components/exams/ExamQuestionsPageContainer.d.ts +3 -0
- package/dist/components/exams/ExamQuestionsPageContainer.js +1 -1
- package/dist/components/exams/ExamViewDialog.d.ts +24 -0
- package/dist/components/exams/ExamViewDialog.js +1 -0
- package/dist/components/exams/TemplateSelector.js +1 -1
- package/dist/components/exams/UnsavedChangesDialog.d.ts +8 -0
- package/dist/components/exams/UnsavedChangesDialog.js +1 -0
- package/dist/components/exams/api.d.ts +1 -1
- package/dist/components/exams/create-exam.texts.d.ts +58 -0
- package/dist/components/exams/create-exam.texts.js +1 -0
- package/dist/components/exams/create-exam.validation.js +1 -1
- package/dist/components/exams/exam-question-status.d.ts +9 -0
- package/dist/components/exams/exam-question-status.js +1 -0
- package/dist/components/exams/index.d.ts +11 -1
- package/dist/components/exams/index.js +1 -1
- package/dist/components/themes/cambridge-yle/CambridgeYleTimer.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +1 -1
- package/dist/shared/lib/utils/question-reverse-transform.d.ts +6 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +1 -0
- package/dist/shared/lib/utils/question-transform-validation.d.ts +6 -0
- package/dist/shared/lib/utils/question-transform-validation.js +1 -0
- package/dist/shared/lib/utils/question-transform.d.ts +6 -2
- package/dist/shared/lib/utils/question-transform.js +1 -1
- package/package.json +10 -3
|
@@ -6,4 +6,4 @@ export declare function createMockExamQuestionsApi(seed: {
|
|
|
6
6
|
exam: ExamWithTemplate;
|
|
7
7
|
questions?: Partial<ExamQuestion>[];
|
|
8
8
|
}): ExamQuestionsSdkApi;
|
|
9
|
-
export type { ExamQuestionsSdkApi, ExamWithTemplate, FetchApiOptions, SaveExamInput, SaveQuestionInput, UpdatePartAudioInput } from './exam-questions.types';
|
|
9
|
+
export type { ExamQuestionsSdkApi, ExamWithTemplate, ExistingQuestionBank, FetchApiOptions, QuestionSearchItem, SaveExamInput, SaveQuestionInput, SaveQuestionResult, SearchQuestionBankInput, SearchQuestionBankResult, UpdatePartAudioInput, } from './exam-questions.types';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";import{transformQuestionToApiFormat as a}from"../shared/lib/utils/question-transform";export function createExamQuestionsApi(
|
|
1
|
+
import{API_BASE_URL as t,API_ENDPOINTS as e}from"../shared/constants/ApiConstant";import{transformQuestionToApiFormat as s}from"../shared/lib/utils/question-transform";import{transformApiQuestionsToFrontend as a}from"../shared/lib/utils/question-reverse-transform";export function createExamQuestionsApi(n={}){const i=function({baseUrl:e=t??"",fetcher:s=fetch,headers:a,apiKey:n}){return async function(t,i){const o=n?{"X-API-Key":n}:void 0,r=await s(`${e}${t}`,{...n?{}:{credentials:"include"},...i,headers:{"Content-Type":"application/json",...o,...a,...i?.headers}});if(!r.ok){const t=await r.json().catch(()=>({}));throw new Error(t.message||r.statusText)}return r.json()}}(n);return{getExam:t=>i(e.MANAGE.EXAM_DETAIL(t)),getQuestions:async t=>{const s=await i(e.MANAGE.EXAM_QUESTIONS(t)),n=Array.isArray(s.data)?s.data:s.data.questions??[];return a(n)},saveQuestion:async t=>{const{examId:a,question:n,template:o}=t;if(!a)throw new Error("examId is required");const r=s(n,function(t){const{question:e,template:s,addToQuestionBank:a,questionBankCode:n,questionBankTitle:i,sectionId:o,examLevel:r,subjectId:u}=t,d=s?.parts.findIndex(t=>t.id===e.partId)??-1,p=d>=0?d+1:void 0,c=e.sectionId;return{partId:e.partId,partNo:p,questionNumber:e.questionNumber,sectionId:o??c,addToQuestionBank:a,questionBankCode:n,questionBankTitle:i,examLevel:r,subjectId:u}}(t)),u=await i(e.MANAGE.EXAM_QUESTIONS(a),{method:"POST",body:JSON.stringify({examId:a,questions:[r]})});if((u.data.summary?.errorCount??0)>0)throw new Error("Unable to save question");const d=u.data.results?.[0]?.data,p={...n,id:d?.id??n.id,...d?.questionGroup?{questionGroup:d.questionGroup}:{},isCompleted:!0},c=(m=d?.questionBank,"object"!=typeof m||null===m||Array.isArray(m)?void 0:m);var m;return{question:p,questionBank:c?{id:String(c.id??""),code:String(c.code??""),title:String(c.title??"")}:void 0}},saveExam:async()=>{},updatePartAudio:t=>async function(t,{examId:s,partId:a,audioUrl:n,template:i}){await t(e.MANAGE.EXAM_TEMPLATE(s),{method:"PATCH",body:JSON.stringify({id:i.id,title:i.name,subTitle:"",theme:i.theme||"",totalQuestions:i.totalQuestions,totalParts:i.parts.length,description:i.description||"",examLevel:i.level||"",type:i.type,parts:i.parts.map(t=>({...t,audioUrl:t.id===a?n:t.audioUrl}))})})}(i,t),searchQuestions:async({questionType:t,keyword:s,page:a,size:n})=>{const o=new URLSearchParams;t&&o.set("questionType",t),s&&o.set("keyword",s),o.set("page",String(a)),o.set("size",String(n));const r=o.toString()?`?${o.toString()}`:"";return i(`${e.MANAGE.QUESTION_BANKS}${r}`)}}}export const createExamQuestionsFetchApi=createExamQuestionsApi;export function createMockExamQuestionsApi(t){const e=new Map((t.questions??[]).map(t=>[(t.questionNumber??1)-1,t]));let s=t.exam;return{getExam:async()=>({data:s}),getQuestions:async()=>Array.from(e.values()),saveQuestion:async({question:t,questionIndex:s})=>{const a={...t,questionNumber:t.questionNumber??s+1,isCompleted:!0};return e.set(s,a),{question:a}},saveExam:async()=>{},updatePartAudio:async({partId:t,audioUrl:e})=>{s.template&&"parts"in s.template&&(s={...s,template:{...s.template,parts:s.template.parts.map(s=>s.id===t?{...s,audioUrl:e}:s)}})},searchQuestions:async({questionType:t})=>({data:Array.from(e.values()).filter(e=>!t||e.type===t).map(t=>({id:t.id??String(t.questionNumber??""),questionType:t.type??"",content:t.content,correctAnswer:t.answer,points:t.points,difficulty:t.difficulty,skill:t.skill,explanation:t.explanation})),meta:{totalItems:e.size}})}}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { QuestionType } from '../shared/types/common.types';
|
|
1
2
|
import type { ExamData, ExamPart, ExamQuestion, ExamTemplate } from '../shared/types/entities/exam.types';
|
|
2
3
|
export interface ExamQuestionsSdkApi {
|
|
3
4
|
getExam: (examId: string) => Promise<ExamQuestionsApiResponse<ExamWithTemplate> | ExamWithTemplate>;
|
|
4
5
|
getQuestions?: (examId: string) => Promise<ExamQuestionsApiResponse<Partial<ExamQuestion>[]> | Partial<ExamQuestion>[]>;
|
|
5
|
-
saveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | void>;
|
|
6
|
+
saveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | SaveQuestionResult | void>;
|
|
6
7
|
saveExam?: (input: SaveExamInput) => Promise<void>;
|
|
7
8
|
updatePartAudio?: (input: UpdatePartAudioInput) => Promise<void>;
|
|
9
|
+
searchQuestions?: (input: SearchQuestionBankInput) => Promise<SearchQuestionBankResult>;
|
|
8
10
|
}
|
|
9
11
|
export interface ExamQuestionsApiResponse<T> {
|
|
10
12
|
data: T;
|
|
@@ -24,6 +26,7 @@ export interface EmbeddedExamTemplate extends Omit<ExamTemplate, 'name' | 'level
|
|
|
24
26
|
title: string;
|
|
25
27
|
subTitle?: string;
|
|
26
28
|
examLevel: string;
|
|
29
|
+
totalParts?: number;
|
|
27
30
|
parts: Array<ExamPart & {
|
|
28
31
|
title?: string;
|
|
29
32
|
}>;
|
|
@@ -36,6 +39,20 @@ export interface SaveQuestionInput {
|
|
|
36
39
|
examDifficulty?: string;
|
|
37
40
|
examLevel?: string;
|
|
38
41
|
subjectId?: string;
|
|
42
|
+
sectionId?: number;
|
|
43
|
+
addToQuestionBank?: boolean;
|
|
44
|
+
questionBankCode?: string;
|
|
45
|
+
questionBankTitle?: string;
|
|
46
|
+
existingQuestionBankId?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface SaveQuestionResult {
|
|
49
|
+
question: Partial<ExamQuestion>;
|
|
50
|
+
questionBank?: ExistingQuestionBank | null;
|
|
51
|
+
}
|
|
52
|
+
export interface ExistingQuestionBank {
|
|
53
|
+
id: string;
|
|
54
|
+
code: string;
|
|
55
|
+
title: string;
|
|
39
56
|
}
|
|
40
57
|
export interface SaveExamInput {
|
|
41
58
|
examId: string;
|
|
@@ -49,6 +66,33 @@ export interface UpdatePartAudioInput {
|
|
|
49
66
|
audioUrl: string;
|
|
50
67
|
template: ExamTemplate;
|
|
51
68
|
}
|
|
69
|
+
export interface SearchQuestionBankInput {
|
|
70
|
+
questionType?: QuestionType;
|
|
71
|
+
keyword?: string;
|
|
72
|
+
page: number;
|
|
73
|
+
size: number;
|
|
74
|
+
}
|
|
75
|
+
export interface QuestionSearchItem {
|
|
76
|
+
id: string;
|
|
77
|
+
questionType: QuestionType | string;
|
|
78
|
+
content: unknown;
|
|
79
|
+
correctAnswer: unknown;
|
|
80
|
+
points?: string | number;
|
|
81
|
+
difficulty?: string;
|
|
82
|
+
skill?: string;
|
|
83
|
+
explanation?: string | null;
|
|
84
|
+
createdAt?: string;
|
|
85
|
+
updatedAt?: string;
|
|
86
|
+
code?: string;
|
|
87
|
+
title?: string | null;
|
|
88
|
+
usageCount?: number;
|
|
89
|
+
}
|
|
90
|
+
export interface SearchQuestionBankResult {
|
|
91
|
+
data: QuestionSearchItem[];
|
|
92
|
+
meta?: {
|
|
93
|
+
totalItems?: number;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
52
96
|
export interface FetchApiOptions {
|
|
53
97
|
baseUrl?: string;
|
|
54
98
|
fetcher?: typeof fetch;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface CompletionDialogProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
completedCount: number;
|
|
5
|
+
totalQuestions: number;
|
|
6
|
+
onConfirm: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function CompletionDialog({ open, onOpenChange, completedCount, totalQuestions, onConfirm, }: CompletionDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as n}from"react/jsx-runtime";import{AlertDialog as t,AlertDialogAction as h,AlertDialogCancel as c,AlertDialogContent as o,AlertDialogDescription as i,AlertDialogFooter as r,AlertDialogHeader as l,AlertDialogTitle as s}from"../../components/ui/alert-dialog";import{AlertTriangle as a,CheckCircle as d}from"lucide-react";export function CompletionDialog({open:m,onOpenChange:p,completedCount:g,totalQuestions:u,onConfirm:x}){const C=g===u;return e(t,{open:m,onOpenChange:p,children:n(o,{children:[n(l,{children:[n(s,{className:"flex items-center gap-2",children:[C?e(d,{className:"h-5 w-5 text-green-500"}):e(a,{className:"h-5 w-5 text-amber-500"}),C?"Lưu đề thi thành công":"Đề thi chưa hoàn thành"]}),e(i,{asChild:!0,children:n("div",{className:"space-y-2 text-sm text-muted-foreground",children:[n("p",{children:["Bạn đã hoàn thành ",n("strong",{children:[g,"/",u]})," câu hỏi."]}),C?e("p",{children:"Đề thi đã sẵn sàng để gán vào phòng thi."}):n("p",{children:["Còn ",e("strong",{children:u-g})," câu chưa được tạo. Bạn có thể tiếp tục tạo sau hoặc thoát và quay lại sau."]})]})})]}),n(r,{children:[e(c,{children:C?"Tiếp tục chỉnh sửa":"Tiếp tục tạo"}),e(h,{onClick:x,className:C?"bg-green-600 hover:bg-green-700":"bg-blue-600 hover:bg-blue-700",children:C?"Xác nhận":"Lưu và thoát"})]})]})})}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type UseFormReturn } from 'react-hook-form';
|
|
2
2
|
import type { ExamFormData, ExamLevelOption } from '../../api/exam-create';
|
|
3
|
+
import type { CreateExamTexts } from './create-exam.texts';
|
|
3
4
|
export interface CreateExamInfoFormProps {
|
|
4
5
|
form: UseFormReturn<ExamFormData>;
|
|
5
6
|
examLevels: ExamLevelOption[];
|
|
6
7
|
isLoadingExamLevels?: boolean;
|
|
8
|
+
/** Optional label overrides (e.g. to reuse this form for homework). */
|
|
9
|
+
texts?: CreateExamTexts;
|
|
7
10
|
}
|
|
8
|
-
export declare function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels }: CreateExamInfoFormProps): import("react").JSX.Element;
|
|
11
|
+
export declare function CreateExamInfoForm({ form, examLevels, isLoadingExamLevels, texts, }: CreateExamInfoFormProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useCallback as a,useEffect as
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{useCallback as a,useEffect as n,useRef as o}from"react";import{Controller as l}from"react-hook-form";import{FileText as i,Loader2 as t}from"lucide-react";import{Card as d,CardContent as c,CardDescription as m,CardHeader as s,CardTitle as u}from"../../components/ui/card";import{Input as h}from"../../components/ui/input";import{Label as f}from"../../components/ui/label";import{Select as p,SelectContent as g,SelectItem as x,SelectTrigger as b,SelectValue as N}from"../../components/ui/select";import{Textarea as v}from"../../components/ui/textarea";import{useT as y}from"../../shared/lib/i18n";const C=["EASY","MEDIUM","HARD"];export function CreateExamInfoForm({form:f,examLevels:S,isLoadingExamLevels:w=!1,texts:M}){const q=y(),D=o(!1),I=f.watch("name"),V=o(f.setValue);V.current=f.setValue;const j=a(e=>e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/đ/g,"d").replace(/Đ/g,"D").toLowerCase().trim().replace(/[^a-z0-9\s-]/g,"").replace(/[\s]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").substring(0,100),[]);n(()=>{D.current||V.current("code",I?j(I):"",{shouldValidate:!1})},[I,j]);const A=e=>Boolean(f.formState.errors[e]),H=e=>f.formState.errors[e]?.message;return r(d,{className:"border border-gray-200",children:[r(s,{className:"border-b border-gray-100 bg-gray-50/50",children:[r(u,{className:"flex items-center gap-2 text-base font-semibold",children:[e(i,{className:"h-4 w-4 text-blue-600"}),M?.basicInfo??q("admin.exams.form.basicInfo")]}),e(m,{children:M?.requiredHint??q("admin.exams.form.requiredHint")})]}),e(c,{className:"space-y-4 pt-6",children:r("div",{className:"grid grid-cols-1 gap-4 md:grid-cols-2",children:[e(P,{label:M?.nameLabel??q("admin.exams.form.nameLabel"),error:H("name"),required:!0,hint:q("admin.exams.form.charCount",{count:String(I?.length||0),max:String(255)}),children:e(h,{id:"name",maxLength:255,className:A("name")?"border-red-500 focus:ring-red-500":"",placeholder:M?.namePlaceholder??q("admin.exams.form.namePlaceholder"),...f.register("name")})}),e(P,{label:M?.codeLabel??q("admin.exams.form.codeLabel"),error:H("code"),required:!0,hint:q("admin.exams.form.charCount",{count:String(f.watch("code")?.length||0),max:String(100)}),children:e(L,{form:f,hasError:A("code"),placeholder:M?.codePlaceholder??q("admin.exams.form.codePlaceholder"),onManualEdit:e=>{D.current=e.length>0}})}),e(P,{label:q("admin.exams.form.totalPoints"),error:H("totalPoints"),required:!0,children:e(l,{name:"totalPoints",control:f.control,render:({field:r})=>e(h,{id:"totalPoints",type:"text",inputMode:"numeric",placeholder:"100",value:r.value,onBlur:r.onBlur,onChange:e=>r.onChange(e.target.value.replace(/[^0-9]/g,"")),className:A("totalPoints")?"border-red-500 focus:ring-red-500":""})})}),e(P,{label:q("admin.exams.form.level"),error:H("level"),children:e(l,{name:"level",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,disabled:w,children:[e(b,{className:A("level")?"border-red-500 focus:ring-red-500":"",children:e(N,{placeholder:q(w?"admin.exams.form.loading":"admin.exams.form.levelPlaceholder")})}),e(g,{children:w?e("div",{className:"flex items-center justify-center p-4",children:e(t,{className:"h-4 w-4 animate-spin text-gray-400"})}):S.map(r=>e(x,{value:r.id,children:r.name},r.id))})]})})}),e(P,{label:q("admin.exams.form.difficulty"),error:H("difficulty"),children:e(l,{name:"difficulty",control:f.control,render:({field:a})=>r(p,{value:a.value,onValueChange:a.onChange,children:[e(b,{className:A("difficulty")?"border-red-500 focus:ring-red-500":"",children:e(N,{placeholder:q("admin.exams.form.difficultyPlaceholder")})}),e(g,{children:C.map(r=>e(x,{value:r,children:E(r,q)},r))})]})})}),e(P,{label:M?.durationLabel??q("admin.exams.form.duration"),error:H("duration"),children:e(h,{id:"duration",type:"number",inputMode:"numeric",min:5,max:300,placeholder:"45",className:A("duration")?"border-red-500 focus:ring-red-500":"",...f.register("duration")})}),e(P,{className:"md:col-span-2",label:M?.descriptionLabel??q("admin.exams.form.description"),error:H("description"),hint:q("admin.exams.form.charCount",{count:String(f.watch("description")?.length||0),max:String(2e3)}),children:e(v,{id:"description",rows:3,maxLength:2e3,placeholder:M?.descriptionPlaceholder??q("admin.exams.form.descriptionPlaceholder"),className:A("description")?"border-red-500 focus:ring-red-500":"",...f.register("description")})})]})})]})}function L({form:r,hasError:a,placeholder:n,onManualEdit:o}){const{onChange:l,...i}=r.register("code");return e(h,{id:"code",maxLength:100,className:a?"border-red-500 focus:ring-red-500":"",placeholder:n,...i,onChange:e=>{l(e),o(e.target.value)}})}function P({label:a,error:n,hint:o,required:l,className:i,children:t}){return r("div",{className:i,children:[r(f,{children:[a," ",l&&e("span",{className:"text-red-500",children:"*"})]}),e("div",{className:"mt-1",children:t}),n&&e("p",{className:"mt-1 text-sm text-red-600",children:n}),o&&e("p",{className:"mt-1 text-xs text-gray-500",children:o})]})}function E(e,r){switch(e){case"EASY":return r("admin.exams.form.easy");case"MEDIUM":return r("admin.exams.form.medium");case"HARD":return r("admin.exams.form.hard");default:return e}}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { UserRole } from '../../shared/types/common.types';
|
|
2
2
|
import type { CreateExamSdkApi } from '../../api/exam-create';
|
|
3
|
+
import type { CreateExamTexts } from './create-exam.texts';
|
|
3
4
|
export interface CreateExamPageContainerProps {
|
|
4
5
|
role?: UserRole.ADMIN | UserRole.TEACHER;
|
|
5
6
|
api: CreateExamSdkApi;
|
|
6
7
|
listPath?: string;
|
|
7
8
|
onNavigate?: (href: string) => void;
|
|
9
|
+
/** Optional label overrides (e.g. to reuse this flow for homework). */
|
|
10
|
+
texts?: CreateExamTexts;
|
|
8
11
|
}
|
|
9
12
|
export declare function CreateExamPageContainer(props: CreateExamPageContainerProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as
|
|
1
|
+
"use client";import{jsx as e,jsxs as t,Fragment as a}from"react/jsx-runtime";import{Suspense as n,useEffect as i,useState as r}from"react";import{zodResolver as s}from"@hookform/resolvers/zod";import{useForm as l}from"react-hook-form";import{ArrowLeft as o,ArrowRight as m,LayoutTemplate as c,Loader2 as d,X as p}from"lucide-react";import{Button as h}from"../../components/ui/button";import{Card as u,CardContent as x,CardHeader as f,CardTitle as g}from"../../components/ui/card";import{Dialog as b,DialogContent as v,DialogDescription as N,DialogHeader as y,DialogTitle as w}from"../../components/ui/dialog";import{useToast as C}from"../../shared/lib/hooks/useToast";import{useT as T}from"../../shared/lib/i18n";import{buildExamTemplatePayload as E}from"../../api/exam-create";import{CreateExamInfoForm as k}from"./CreateExamInfoForm";import{TemplateSelector as D}from"./TemplateSelector";import{examFormSchema as L}from"./create-exam.validation";function I({api:n,listPath:c="/manage/exams",onNavigate:p,texts:u}){const x=T(),{success:f,warning:g,error:I}=C(),[S,M]=r(!1),[z,P]=r(null),[q,O]=r([]),[F,H]=r(!1),[R,U]=r(!1),V=l({resolver:s(L),mode:"onBlur",defaultValues:{code:"",name:"",level:"",difficulty:"",duration:"",totalPoints:"100",description:""}});i(()=>{if(!n.getExamLevels)return;let e=!1;return H(!0),n.getExamLevels().then(t=>{e||O(t)}).catch(e=>I({message:x("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})).finally(()=>{e||H(!1)}),()=>{e=!0}},[n,I,x]);const Y=e=>{p?p(e):"undefined"!=typeof window&&(window.location.href=e)},$=V.handleSubmit(async e=>{if(z){U(!0);try{const a=function(e){return"object"==typeof e&&null!==e&&"data"in e}(t=await n.createExam({name:e.name,code:e.code||void 0,examLevel:e.level||void 0,difficulty:A(e.difficulty),totalScore:Number.parseInt(e.totalPoints,10),durationMinutes:e.duration?Number.parseInt(e.duration,10):void 0,description:e.description||void 0,isComplete:!1,isActive:!1,examTemplateId:z.id}))?t.data:t;await n.updateExamTemplate({examId:a.id,template:E(z)}),f({message:x("admin.exams.create.success"),description:u?.successDescription?.replace("{name}",a.name)??x("admin.exams.create.successDesc",{name:a.name})}),Y(`${c}/${a.id}/questions`)}catch(e){I({message:x("admin.exams.questions.error"),description:e instanceof Error?e.message:void 0})}finally{U(!1)}var t}else g({message:x("admin.exams.create.incompleteInfo"),description:u?.missingTemplateDescription??"Vui lòng chọn template đề thi trước khi tiếp tục."})},()=>g({message:x("admin.exams.create.incompleteInfo"),description:x("admin.exams.create.incompleteInfoDesc")}));return t("div",{className:"space-y-6",children:[t("div",{className:"flex items-center gap-4",children:[e(h,{variant:"ghost",size:"icon",onClick:()=>Y(c),className:"h-8 w-8",children:e(o,{className:"h-4 w-4"})}),t("div",{children:[e("h1",{className:"text-3xl font-bold tracking-tight text-gray-900",children:u?.title??x("admin.exams.create.title")}),e("p",{className:"mt-1 text-gray-600",children:u?.step1??x("admin.exams.create.step1")})]})]}),e("form",{id:"exam-form",onSubmit:$,children:e(k,{form:V,examLevels:q,isLoadingExamLevels:F,texts:u})}),e(j,{selectedTemplate:z,onOpen:()=>M(!0),onClear:()=>P(null),texts:u}),e(b,{open:S,onOpenChange:M,children:t(v,{className:"flex max-h-[92vh] min-h-[92vh] max-w-5xl flex-col overflow-hidden",children:[t(y,{children:[e(w,{children:u?.templateDialogTitle??"Chọn template đề thi"}),e(N,{children:u?.templateDialogDescription??"Chọn loại đề thi bạn muốn tạo."})]}),e(D,{api:n,onTemplateSelect:e=>{P(e),M(!1)},onCancel:()=>M(!1)})]})}),t("div",{className:"flex justify-end gap-3",children:[e(h,{variant:"outline",onClick:()=>Y(c),disabled:R,children:u?.cancelLabel??x("admin.exams.create.cancel")}),e(h,{type:"submit",form:"exam-form",className:"bg-blue-600 hover:bg-blue-700",disabled:R||!z,children:t(a,R?{children:[e(d,{className:"mr-2 h-4 w-4 animate-spin"}),u?.savingLabel??x("admin.exams.create.saving")]}:{children:[u?.saveAndContinueLabel??x("admin.exams.create.saveAndContinue"),e(m,{className:"ml-2 h-4 w-4"})]})})]})]})}export function CreateExamPageContainer(t){return e(n,{fallback:e(S,{}),children:e(I,{...t})})}function j({selectedTemplate:a,onOpen:n,onClear:i,texts:r}){return t(u,{className:"border border-gray-200",children:[e(f,{className:"border-b border-gray-100 bg-gray-50/50",children:t(g,{className:"flex items-center gap-2 text-base font-semibold",children:[e(c,{className:"h-4 w-4 text-blue-600"}),r?.templateSectionTitle??"Chọn Template"," ",e("span",{className:"text-red-500",children:"*"})]})}),e(x,{className:"pt-6",children:t("div",a?{className:"flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-green-100",children:e(c,{className:"h-4 w-4 text-green-600"})}),t("div",{children:[e("p",{className:"text-sm font-medium text-green-800",children:a.name}),t("p",{className:"text-xs text-green-600",children:[a.totalQuestions," câu hỏi • ",a.parts.length," phần"]})]})]}),t("div",{className:"flex items-center gap-2",children:[e(h,{type:"button",variant:"outline",size:"sm",onClick:n,className:"border-green-300 text-green-700 hover:bg-green-100",children:r?.changeTemplateLabel??"Đổi template"}),e(h,{type:"button",variant:"ghost",size:"icon",onClick:i,className:"h-7 w-7 text-green-600 hover:bg-green-100 hover:text-green-800",children:e(p,{className:"h-3.5 w-3.5"})})]})]}:{className:"flex flex-col items-center gap-3 rounded-lg border-2 border-dashed border-gray-300 py-8",children:[e(c,{className:"h-8 w-8 text-gray-400"}),e("p",{className:"text-sm text-gray-500",children:r?.noTemplateText??"Chưa chọn template đề thi"}),t(h,{type:"button",variant:"outline",onClick:n,children:[e(c,{className:"mr-2 h-4 w-4"}),r?.selectTemplateLabel??"Chọn template"]})]})})]})}function S(){return e("div",{className:"flex h-screen items-center justify-center",children:e(d,{className:"h-8 w-8 animate-spin text-blue-600"})})}function A(e){return"EASY"===e?"EASY":"MEDIUM"===e?"MEDIUM":"HARD"===e?"HARD":void 0}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type ExistingQuestionBank } from '../questions/components/QuestionBankOption';
|
|
1
2
|
import type { ExamData, ExamQuestion, ExamTemplate } from '../../shared/types/entities/exam.types';
|
|
2
|
-
import type { SaveQuestionInput } from '../../api/exam-questions.types';
|
|
3
|
+
import type { SaveQuestionInput, SearchQuestionBankInput, SearchQuestionBankResult } from '../../api/exam-questions.types';
|
|
3
4
|
export interface ExamCreatorProps {
|
|
4
5
|
template: ExamTemplate;
|
|
5
6
|
examId?: string;
|
|
@@ -7,10 +8,15 @@ export interface ExamCreatorProps {
|
|
|
7
8
|
examLevel?: string;
|
|
8
9
|
subjectId?: string;
|
|
9
10
|
initialQuestions?: Partial<ExamQuestion>[];
|
|
10
|
-
onSaveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> |
|
|
11
|
+
onSaveQuestion?: (input: SaveQuestionInput) => Promise<Partial<ExamQuestion> | {
|
|
12
|
+
question: Partial<ExamQuestion>;
|
|
13
|
+
questionBank?: ExistingQuestionBank | null;
|
|
14
|
+
} | void>;
|
|
11
15
|
onSave?: (examData: ExamData) => void | Promise<void>;
|
|
12
16
|
onCancel?: () => void;
|
|
13
17
|
onPartAudioChange?: (partId: string, audioUrl: string) => void | Promise<void>;
|
|
14
18
|
partAudioOverrides?: Record<string, string>;
|
|
19
|
+
searchQuestions?: (input: SearchQuestionBankInput) => Promise<SearchQuestionBankResult>;
|
|
20
|
+
saveExamRef?: React.MutableRefObject<(() => void) | null>;
|
|
15
21
|
}
|
|
16
|
-
export declare function ExamCreator(
|
|
22
|
+
export declare function ExamCreator(props: ExamCreatorProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useMemo as r,useRef as o,useState as i}from"react";import{ChevronLeft as a,ChevronRight as s,Save as n}from"lucide-react";import{Button as l}from"../../components/ui/button";import{QuestionCreator as d}from"../questions/creator/QuestionCreator";import{useToast as u}from"../../shared/lib/hooks/useToast";import{useT as m}from"../../shared/lib/i18n";import{ExamPartTabs as c}from"./ExamPartTabs";import{ExamQuestionGrid as p}from"./ExamQuestionGrid";import{PartAudioUploader as f}from"./PartAudioUploader";import{getEffectiveQuestionConfig as g,getPartByQuestionIndex as x,normalizeTemplateGroupConfig as h}from"./exam-template.utils";export function ExamCreator({template:b,examId:v,examDifficulty:w,examLevel:C,subjectId:y,initialQuestions:I=[],onSaveQuestion:N,onSave:S,onCancel:Q,onPartAudioChange:k,partAudioOverrides:q}){const A=m(),{success:E,warning:T,error:j}=u(),O=o(null),[D,M]=i(0),[P,L]=i(()=>Object.fromEntries(I.map(e=>[(e.questionNumber??1)-1,e]))),[_,U]=i(()=>new Set(I.map(e=>(e.questionNumber??1)-1))),[V,z]=i({}),[B,G]=i(!1),[R,$]=i(!1),H=o({}),F=x(b,D),J=F?h(F):void 0,K=F?g(F,D):void 0,W=!(!J?.groups||K?.groups),X=K&&"sectionName"in K?K.sectionName:void 0,Y=X&&!W?`${F?.id}_${X}`:F?.id,Z=P[D],ee=r(()=>({examId:v,templateId:b.id,questions:Object.values(P),currentQuestionIndex:D,lastSaved:(new Date).toISOString(),createdAt:(new Date).toISOString(),updatedAt:(new Date).toISOString(),isCompleted:_.size>=b.totalQuestions}),[_.size,D,v,P,b.id,b.totalQuestions]),te=e=>{R&&T({message:A("common.examCreator.saveBeforeExit")}),M(Math.max(0,Math.min(b.totalQuestions-1,e))),$(!1),H.current={}},re=async e=>{if(!F)return;if(!1===await(O.current?.triggerValidation?.()))return;const t={...Z,...H.current,...e,type:e?.type??H.current.type??Z?.type??K?.questionType,partId:F.id,questionNumber:D+1,difficulty:e?.difficulty??w??Z?.difficulty,skill:F.skill,isCompleted:!0};G(!0);try{const e=await(N?.({question:t,questionIndex:D,template:b,examId:v,examDifficulty:w,examLevel:C,subjectId:y}))||t;L(t=>({...t,[D]:e})),U(e=>new Set(e).add(D)),$(!1),E({message:A("common.examCreator.saveSuccess",{index:String(D+1)})})}catch(e){j({message:A("common.examCreator.saveError",{index:String(D+1)}),description:e instanceof Error?e.message:void 0})}finally{G(!1)}};return e("div",{className:"flex min-h-0 flex-1 flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50/30",children:t("div",{className:"flex min-h-0 flex-1 overflow-hidden",children:[e("div",{className:"w-80 overflow-y-auto border-r border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(c,{template:b,currentPartId:F?.id??"",completedQuestions:_,onPartChange:e=>te(b.parts.find(t=>t.id===e)?.startIndex??0)})}),t("div",{className:"flex flex-1 flex-col overflow-hidden",children:[e("div",{className:"min-h-0 flex-1 overflow-y-auto p-4 pb-4",children:t("div",{className:"mx-auto max-w-4xl",children:["LISTENING"===F?.skill&&e(f,{partId:F.id,partName:F.name,audioUrl:q?.[F.id]??F.audioUrl??"",examId:v,onAudioChange:e=>{k?.(F.id,e)},isReadOnly:!k}),e(d,{initialData:Z,prefilledType:K?.questionType,prefilledSkill:F?.skill,optionCount:K?.optionCount,optionMode:K?.optionMode,defaultValue:K?.defaultValue,defaultQuestion:K?.defaultQuestion,viewMode:K?.viewMode,groups:K?.groups||J?.groups,groupConfig:K?.groupConfig||J?.groupConfig,groupType:K?.groupType||J?.groupType,articleCount:K?.articleCount??F?.articleCount,questionConfig:K?.questionConfig||F?.questionConfig,questionIndexInPart:W?D-(F?.startIndex??0):D-(K?.startIndex??F?.startIndex??0),partId:Y,onChange:e=>{H.current=e,$(!0)},onSave:re,onCancel:Q,isEmbedded:!0,externalErrors:V[D],validationRef:O,suggestedAnswers:K?.suggestedAnswers||F?.suggestedAnswers,hasSuggestedAnswers:K?.hasSuggestedAnswers??F?.hasSuggestedAnswers},D)]})}),e("div",{className:"flex-shrink-0 border-t border-gray-200/80 bg-white/95 shadow-[0_-4px_20px_rgba(0,0,0,0.08)] backdrop-blur-sm",children:t("div",{className:"mx-auto flex max-w-4xl items-center justify-between px-6 py-4",children:[t(l,{variant:"outline",onClick:()=>te(D-1),disabled:0===D,children:[e(a,{className:"mr-2 h-4 w-4"}),"Câu trước"]}),t(l,{onClick:()=>{re()},disabled:B,className:"bg-green-600 hover:bg-green-700",children:[e(n,{className:"mr-2 h-4 w-4"}),B?"Đang lưu...":"Lưu câu hỏi"]}),t(l,{variant:"outline",onClick:()=>te(D+1),disabled:D>=b.totalQuestions-1,children:["Câu tiếp",e(s,{className:"ml-2 h-4 w-4"})]})]})})]}),t("div",{className:"w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:[e(p,{template:b,currentQuestionIndex:D,completedQuestions:_,onQuestionChange:te}),t(l,{onClick:()=>{(async()=>{R?T({message:A("common.examCreator.saveBeforeExit")}):await(S?.(ee))})()},className:"mt-4 w-full bg-blue-600 hover:bg-blue-700",children:[e(n,{className:"mr-2 h-4 w-4"}),"Hoàn tất đề thi"]})]})]})})}
|
|
1
|
+
"use client";import{jsx as e,jsxs as t,Fragment as n}from"react/jsx-runtime";import{useCallback as o,useEffect as i,useMemo as r,useRef as a,useState as s}from"react";import{CheckCircle as d,ChevronLeft as l,ChevronRight as u,Loader2 as c,Save as m}from"lucide-react";import{Button as p}from"../../components/ui/button";import{QuestionBankOption as g}from"../questions/components/QuestionBankOption";import{QuestionSearchDropdown as f}from"../questions/components/QuestionSearchDropdown";import{QuestionCreator as h}from"../questions/creator/QuestionCreator";import{useToast as x}from"../../shared/lib/hooks/useToast";import{useT as b}from"../../shared/lib/i18n";import{useExamQuestionStore as v}from"../../shared/lib/stores/examQuestionStore";import{transformApiQuestionToFrontend as C}from"../../shared/lib/utils/question-reverse-transform";import{ExamPartTabs as w}from"./ExamPartTabs";import{ExamQuestionGrid as y}from"./ExamQuestionGrid";import{PartAudioUploader as N}from"./PartAudioUploader";import{CompletionDialog as I}from"./CompletionDialog";import{UnsavedChangesDialog as k}from"./UnsavedChangesDialog";import{clearQuestionStatuses as q,getQuestionStatus as Q,QuestionStatus as S,setQuestionStatus as E}from"./exam-question-status";import{getEffectiveQuestionConfig as D,getPartByQuestionIndex as T,normalizeTemplateGroupConfig as A}from"./exam-template.utils";export function ExamCreator(L){const{template:B,examId:V=`exam-${Date.now()}`,initialQuestions:j=[]}=L,U=b(),{success:P,warning:$,error:M}=x(),R=a(null),z=a({}),F=a(!1),G=a(!1),H=a(!1),K=v(e=>e.initialized),W=v(e=>e.examId),J=v(e=>e.initializeExam),X=v(e=>e.resetStore),Y=v(e=>e.currentQuestionIndex),Z=v(e=>e.setCurrentQuestionIndex),ee=v(e=>e.questions),te=v(e=>e.upsertQuestionData),ne=v(e=>e.clearQuestionData),oe=v(e=>e.markQuestionCompleted),ie=v(e=>e.getQuestionData),re=v(e=>e.setSharedHintLetters),[ae,se]=s({}),[de,le]=s(!1),[ue,ce]=s(!1),[me,pe]=s(!1),[ge,fe]=s(null),[he,xe]=s(!1),[be,ve]=s(!1),[Ce,we]=s({}),[ye,Ne]=s({}),[Ie,ke]=s({}),[qe,Qe]=s(),[Se,Ee]=s(),De=T(B,Y),Te=De?.id??"",Ae=De?D(De,Y):void 0,Oe=De?A(De):void 0,_e=Boolean(Oe?.groups&&!Ae?.groups),Le=_(Ae),Be=Le&&!_e?`${Te}_${Le}`:Te,Ve=ee[Y]?.data,je=Ce[Y]??{checked:!1,code:"",title:""},Ue=r(()=>{const e=new Set;return Object.entries(ee).forEach(([t,n])=>{n.isCompleted&&e.add(Number(t))}),e},[ee]),Pe=r(()=>({id:V,templateId:B.id,questions:Object.values(ee).map(e=>e.data),currentQuestionIndex:Y,lastSaved:(new Date).toISOString(),createdAt:(new Date).toISOString(),updatedAt:(new Date).toISOString(),isCompleted:Ue.size>=B.totalQuestions}),[Ue.size,Y,V,ee,B.id,B.totalQuestions]);i(()=>{const e=W&&W!==V;e&&X(),K&&!e||J({examId:V,templateId:B.id,totalQuestions:B.totalQuestions})},[V,J,K,X,W,B.id,B.totalQuestions]),i(()=>{if(!K||0===j.length)return;F.current=!0,j.forEach(e=>{const t=(e.questionNumber??1)-1,n={...e,isCompleted:!0};te(t,n),oe(t,!0),z.current[t]=n,function(e,t,n,o){if("LOOK_PICTURE_FILL_WORD_HINT"!==t.type||!t.partId)return;const i=t.answer,r=t.questionGroup?.groupNumber;if(!i?.hintLetters?.length||!r)return;const a=T(n,e),s=a?D(a,e):void 0,d=_(s),l=d?`${t.partId}_${d}`:t.partId;o(l,r,i.hintLetters)}(t,n,B,re);const o=e.questionBank;o?.id&&Ne(e=>({...e,[t]:o}))});const e=window.setTimeout(()=>{F.current=!1},1e3);return()=>window.clearTimeout(e)},[j,K,oe,re,B,te]),i(()=>{pe(Q(V,Y+1)===S.UNSAVED)},[Y,V]),i(()=>{ce(Object.values(ee).some(e=>Object.keys(e.data).length>0&&!e.isCompleted))},[ee]),i(()=>{const e=e=>{ue&&(e.preventDefault(),e.returnValue="")};return window.addEventListener("beforeunload",e),()=>window.removeEventListener("beforeunload",e)},[ue]);const $e=o(e=>{H.current=!0,pe(!1),Z(Math.max(0,Math.min(B.totalQuestions-1,e))),window.setTimeout(()=>{H.current=!1},500)},[Z,B.totalQuestions]),Me=e=>{if(me)return fe(e),void xe(!0);$e(e)},Re=o(e=>{F.current||G.current||H.current||(te(Y,{questionNumber:Y+1,partId:Te,...e}),E(V,Y+1,S.UNSAVED),pe(!0))},[Te,Y,V,te]),ze=async e=>{if(!De)return;if(!function(e,t,n,o){if(!e.checked||t?.id)return!0;const i=e.code.trim()?void 0:"Vui lòng nhập mã câu hỏi cho ngân hàng",r=e.title.trim()?void 0:"Vui lòng nhập tiêu đề câu hỏi cho ngân hàng";return n(i),o(r),!i&&!r}(je,ye[Y],Qe,Ee))return;if(!1===await(R.current?.triggerValidation?.()))return;const t=R.current?.getFormData?.(),n=function(e,t,n,o,i,r,a){return{...n,...e,...t?{answer:t}:{},type:e?.type??n?.type??o,id:n?.id??e?.id,partId:i.id,questionNumber:r+1,difficulty:e?.difficulty??a??n?.difficulty,skill:i.skill,isCompleted:!0}}(e,t,ie(Y),Ae?.questionType,De,Y,L.examDifficulty);le(!0);try{const e=await(L.onSaveQuestion?.({question:n,questionIndex:Y,template:B,examId:V,examDifficulty:L.examDifficulty,examLevel:L.examLevel,subjectId:L.subjectId,sectionId:(o=Ae,o&&"sectionId"in o?o.sectionId:void 0),addToQuestionBank:!ye[Y]?.id&&je.checked,questionBankCode:je.checked?je.code:void 0,questionBankTitle:je.checked?je.title:void 0,existingQuestionBankId:ye[Y]?.id})),t=function(e,t){return e&&"question"in e?{...t,...e.question,isCompleted:!0}:{...t,...e??{},isCompleted:!0}}(e,n);te(Y,t),oe(Y,!0),z.current[Y]=t,function(e,t,n){const o=e&&"questionBank"in e?e.questionBank:e?.questionBank;o?.id&&n(e=>({...e,[t]:o}))}(e,Y,Ne),G.current=!0,window.setTimeout(()=>{G.current=!1},500),E(V,Y+1,S.SAVED),pe(!1),P({message:U("common.examCreator.saveSuccess",{index:String(Y+1)})})}catch(e){M({message:U("common.examCreator.saveError",{index:String(Y+1)}),description:e instanceof Error?e.message:void 0})}finally{le(!1)}var o};return L.saveExamRef&&(L.saveExamRef.current=()=>{me?$({message:U("common.examCreator.saveBeforeExit")}):ve(!0)}),t("div",{className:"flex min-h-0 flex-1 flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50/30",children:[t("div",{className:"flex min-h-0 flex-1 overflow-hidden",children:[e("div",{className:"w-80 overflow-y-auto border-r border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(w,{template:B,currentPartId:Te,completedQuestions:Ue,onPartChange:e=>Me(B.parts.find(t=>t.id===e)?.startIndex??0)})}),t("div",{className:"flex flex-1 flex-col overflow-hidden",children:[e("div",{className:"min-h-0 flex-1 overflow-y-auto p-4 pb-4",children:t("div",{className:"mx-auto max-w-4xl",children:["LISTENING"===De?.skill&&e(N,{partId:De.id,partName:De.name,audioUrl:L.partAudioOverrides?.[De.id]??De.audioUrl??"",examId:V,onAudioChange:e=>{L.onPartAudioChange?.(De.id,e)},isReadOnly:!L.onPartAudioChange}),e(h,{initialData:Ve,prefilledType:Ae?.questionType,prefilledSkill:De?.skill,optionCount:Ae?.optionCount,optionMode:Ae?.optionMode,defaultValue:Ae?.defaultValue,defaultQuestion:Ae?.defaultQuestion,viewMode:Ae?.viewMode,groups:Ae?.groups||Oe?.groups,groupConfig:Ae?.groupConfig||Oe?.groupConfig,groupType:Ae?.groupType||Oe?.groupType,articleCount:Ae?.articleCount??De?.articleCount,questionConfig:Ae?.questionConfig||De?.questionConfig,questionIndexInPart:_e?Y-(De?.startIndex??0):Y-(Ae?.startIndex??De?.startIndex??0),partId:Be,onChange:Re,onUnsavedChangesChange:pe,onSave:ze,onCancel:L.onCancel,isEmbedded:!0,externalErrors:ae[Y],validationRef:R,suggestedAnswers:Ae?.suggestedAnswers||De?.suggestedAnswers,hasSuggestedAnswers:Ae?.hasSuggestedAnswers??De?.hasSuggestedAnswers,headerExtra:e(f,{questionType:Ae?.questionType||De?.questionType,onSelect:e=>{const t={...C({question_type:e.type,content:e.content,correct_answer:e.answer,points:e.points,difficult_level:e.difficulty,question_skill_type:e.skill,explanation:e.explanation}),id:void 0,partId:Te,questionNumber:Y+1,isCompleted:!1};te(Y,t);const n=e.id,o=e.code,i=e.title;(o||i)&&ke(e=>({...e,[Y]:{code:o,title:i}})),n&&(o||i)&&Ne(e=>({...e,[Y]:{id:n,code:o??"",title:i??""}})),E(V,Y+1,S.UNSAVED),pe(!0),oe(Y,!1),P({message:U("common.examCreator.bankSelected")})},selectedQuestionLabel:Ie[Y]?`${Ie[Y].code?`#${Ie[Y].code} - `:""}${Ie[Y].title||""}`:void 0,disabled:!(Ae?.questionType||De?.questionType),compact:!0,searchQuestions:L.searchQuestions})},Y),e("div",{className:"mt-8",children:e(g,{checked:je.checked,onCheckedChange:e=>O(Y,{checked:e},we,pe,V),code:je.code,onCodeChange:e=>O(Y,{code:e},we,pe,V),title:je.title,onTitleChange:e=>O(Y,{title:e},we,pe,V),codeError:qe,error:Se,existingBank:ye[Y]||null})})]})}),e("div",{className:"flex-shrink-0 border-t border-gray-200/80 bg-white/95 shadow-[0_-4px_20px_rgba(0,0,0,0.08)] backdrop-blur-sm",children:t("div",{className:"mx-auto flex max-w-4xl items-center justify-between px-6 py-4",children:[t(p,{variant:"outline",onClick:()=>Me(Y-1),disabled:0===Y,className:"gap-2 border-gray-300 hover:bg-gray-50",children:[e(l,{className:"h-4 w-4"}),"Câu trước"]}),t("div",{className:"flex items-center gap-4",children:[me?t("div",{className:"flex items-center gap-2 rounded-full bg-amber-50 px-3 py-1.5 ring-1 ring-amber-200",children:[e("span",{className:"h-2 w-2 animate-pulse rounded-full bg-amber-500"}),e("span",{className:"text-xs font-medium text-amber-700",children:"Chưa lưu"})]}):ee[Y]?.isCompleted?t("div",{className:"flex items-center gap-2 rounded-full bg-green-50 px-3 py-1.5 ring-1 ring-green-200",children:[e(d,{className:"h-3.5 w-3.5 text-green-500"}),e("span",{className:"text-xs font-medium text-green-700",children:"Đã lưu"})]}):null,t("div",{className:"flex items-center gap-2 rounded-full bg-gray-100 px-4 py-2",children:[t("span",{className:"text-sm font-bold text-gray-900",children:["Câu ",Y+1]}),e("span",{className:"text-sm text-gray-400",children:"/"}),e("span",{className:"text-sm text-gray-500",children:B.totalQuestions})]}),e(p,{onClick:()=>{ze(ie(Y))},disabled:de||!me,className:"gap-2 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 shadow-md hover:shadow-lg transition-all",children:t(n,de?{children:[e(c,{className:"h-4 w-4 animate-spin"}),"Đang lưu..."]}:{children:[e(m,{className:"h-4 w-4"}),"Lưu câu hỏi"]})})]}),t(p,{variant:"outline",onClick:()=>Me(Y+1),disabled:Y>=B.totalQuestions-1||!ee[Y]?.isCompleted,className:"gap-2 border-gray-300 hover:bg-gray-50",children:["Câu tiếp theo",e(u,{className:"h-4 w-4"})]})]})})]}),e("div",{className:"w-80 overflow-y-auto border-l border-gray-200/60 bg-white/80 p-4 backdrop-blur-sm",children:e(y,{template:B,currentQuestionIndex:Y,completedQuestions:Ue,onQuestionChange:Me})})]}),e(I,{open:be,onOpenChange:ve,completedCount:Ue.size,totalQuestions:B.totalQuestions,onConfirm:()=>{ve(!1),q(V),X(),L.onSave?.(Pe)}}),e(k,{open:he,onOpenChange:xe,onConfirm:()=>{const e=z.current[Y];e?(te(Y,e),E(V,Y+1,S.SAVED)):ne(Y),pe(!1),null!==ge&&$e(ge),fe(null),xe(!1)},onCancel:()=>xe(!1),questionNumber:Y+1})]})}function O(e,t,n,o,i){n(n=>{const o=n[e]??{checked:!1,code:"",title:""};return{...n,[e]:{...o,...t}}}),o(!0),E(i,e+1,S.UNSAVED)}function _(e){return e&&"sectionName"in e?e.sectionName:void 0}
|
|
@@ -5,5 +5,5 @@ interface ExamPartTabsProps {
|
|
|
5
5
|
completedQuestions: Set<number>;
|
|
6
6
|
onPartChange: (partId: string) => void;
|
|
7
7
|
}
|
|
8
|
-
export declare function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange }: ExamPartTabsProps): import("react").JSX.Element;
|
|
8
|
+
export declare function ExamPartTabs({ template, currentPartId, completedQuestions, onPartChange, }: ExamPartTabsProps): import("react").JSX.Element;
|
|
9
9
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{CheckCircle2 as
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{CheckCircle2 as r,ChevronRight as a,Circle as s,Layers as n,Lock as l,Play as i}from"lucide-react";import{cn as o}from"../../shared/lib/utils";export function ExamPartTabs({template:d,currentPartId:c,completedQuestions:m,onPartChange:g}){const h=e=>{let t=0;for(let r=e.startIndex;r<=e.endIndex;r++)m.has(r)&&t++;const r=e.endIndex-e.startIndex+1;return{completed:t,total:r,percentage:r>0?Math.round(t/r*100):0}},b=e=>{const t=h(e);return 0===t.completed?"not-started":t.completed===t.total?"completed":"in-progress"},x=e=>{if(0===e)return!0;for(let t=0;t<e;t++){const e=d.parts[t];if("completed"!==b(e))return!1}return!0},p=(t,a,n)=>n?"completed"===t?e(r,{className:o("h-4 w-4",a?"text-white":"text-green-500")}):"in-progress"===t?e(i,{className:o("h-4 w-4",a?"text-white":"text-amber-500")}):e(s,{className:o("h-4 w-4",a?"text-white/70":"text-gray-400")}):e(l,{className:"h-4 w-4 text-gray-400"}),u=(r,s,n,i,d,c)=>t("button",{onClick:()=>c&&g(r.id),disabled:!c,className:o("group relative w-full rounded-xl border-2 p-4 text-left transition-all duration-200","focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none",{"border-transparent bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow-lg shadow-blue-200/50":d&&c,"border-green-200 bg-gradient-to-r from-green-50 to-emerald-50 hover:border-green-300 hover:shadow-md":!d&&"completed"===i&&c,"border-amber-200 bg-gradient-to-r from-amber-50 to-orange-50 hover:border-amber-300 hover:shadow-md":!d&&"in-progress"===i&&c,"border-gray-200 bg-white hover:border-gray-300 hover:shadow-md":!d&&"not-started"===i&&c,"cursor-not-allowed border-gray-100 bg-gray-50/50 opacity-60":!c}),children:[d&&e("div",{className:"absolute -left-[2px] top-1/2 h-8 w-1 -translate-y-1/2 rounded-full bg-white shadow-sm"}),t("div",{className:"flex items-start gap-3",children:[e("div",{className:o("flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg transition-colors",d?"bg-white/20":"completed"===i?"bg-green-100":"in-progress"===i?"bg-amber-100":"bg-gray-100"),children:p(i,d,c)}),t("div",{className:"flex-1 min-w-0",children:[t("div",{className:"flex items-center justify-between gap-2",children:[e("span",{className:o("text-sm font-semibold truncate",d?"text-white":"completed"===i?"text-green-800":"in-progress"===i?"text-amber-800":"text-gray-800"),children:r.name}),t("span",{className:o("flex-shrink-0 rounded-full px-2 py-0.5 text-xs font-bold",d?"bg-white/20 text-white":"completed"===i?"bg-green-100 text-green-700":"in-progress"===i?"bg-amber-100 text-amber-700":"bg-gray-100 text-gray-600"),children:[n.completed,"/",n.total]})]}),r.description&&e("p",{className:o("mt-1 text-xs leading-relaxed",d?"text-blue-100":"text-gray-500"),children:r.description}),t("div",{className:"mt-2",children:[e("div",{className:o("h-1.5 overflow-hidden rounded-full",d?"bg-white/20":"bg-gray-200"),children:e("div",{className:o("h-full rounded-full transition-all duration-500 ease-out",d?"bg-white":"completed"===i?"bg-gradient-to-r from-green-400 to-emerald-500":"in-progress"===i?"bg-gradient-to-r from-amber-400 to-orange-500":"bg-gray-300"),style:{width:`${n.percentage}%`}})}),t("div",{className:o("mt-1 flex items-center justify-between text-xs",d?"text-blue-100":"text-gray-500"),children:[t("span",{children:[r.questionCount," câu hỏi"]}),t("span",{className:"font-medium",children:[n.percentage,"%"]})]})]}),r.isGroup&&r.groups&&r.groups.length>0&&t("div",{className:o("mt-2 rounded-lg p-2 text-xs",d?"bg-white/10":"bg-gray-100/80"),children:[t("div",{className:o("mb-1 font-medium",d?"text-blue-100":"text-gray-600"),children:[r.groups.length," nhóm câu hỏi:"]}),e("div",{className:"space-y-0.5",children:r.groups.map((r,s)=>t("div",{className:o("flex items-center justify-between",d?"text-blue-100":"text-gray-500"),children:[t("span",{className:"flex items-center gap-1",children:[e(a,{className:"h-3 w-3"}),"Nhóm ",r.number]}),t("span",{className:"font-medium",children:[r.questionCount," câu"]})]},s))})]}),r.sections&&r.sections.length>0&&t("div",{className:o("mt-2 rounded-lg p-2 text-xs",d?"bg-white/10":"bg-gray-100/80"),children:[t("div",{className:o("mb-1 font-medium",d?"text-blue-100":"text-gray-600"),children:[r.sections.length," phần nhỏ:"]}),e("div",{className:"space-y-1",children:r.sections.map((r,s)=>{let n=0;for(let e=r.startIndex;e<=r.endIndex;e++)m.has(e)&&n++;const l=r.endIndex-r.startIndex+1;return t("div",{className:o("flex items-center justify-between",d?"text-blue-100":"text-gray-500"),children:[t("span",{className:"flex items-center gap-1 truncate flex-1 mr-2",children:[e(a,{className:"h-3 w-3 flex-shrink-0"}),e("span",{className:"truncate",children:r.name})]}),t("span",{className:"font-medium flex-shrink-0",children:[n,"/",l]})]},s)})})]}),!c&&t("div",{className:"mt-2 flex items-center gap-1 text-xs text-gray-400",children:[e(l,{className:"h-3 w-3"}),e("span",{children:"Hoàn thành phần trước để mở khóa"})]})]})]})]},r.id);return t("div",{className:"space-y-4",children:[t("div",{className:"flex items-center gap-2 px-1",children:[e("div",{className:"flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600 shadow-sm",children:e(n,{className:"h-4 w-4 text-white"})}),t("div",{children:[e("h3",{className:"text-sm font-bold text-gray-900",children:"Các phần thi"}),t("p",{className:"text-xs text-gray-500",children:[d.parts.length," phần"]})]})]}),e("div",{className:"space-y-4",children:(()=>{const r=[];d.parts.forEach((e,t)=>{const a=e.category||"",s=r[r.length-1];s&&s.groupKey===a?s.parts.push({part:e,originalIndex:t}):r.push({groupKey:a,parts:[{part:e,originalIndex:t}]})});const a={LISTENING:"🎧 LISTENING",READING_AND_WRITING:"📝 READING AND WRITING",READING:"📖 READING",WRITING:"✍️ WRITING",SPEAKING:"🗣️ SPEAKING"},s={LISTENING:{bg:"bg-gradient-to-r from-pink-100 via-rose-100 to-pink-50",border:"border-pink-300",text:"text-pink-900",connector:"bg-pink-300"},READING_AND_WRITING:{bg:"bg-gradient-to-r from-teal-100 via-cyan-100 to-teal-50",border:"border-teal-300",text:"text-teal-900",connector:"bg-teal-300"},READING:{bg:"bg-gradient-to-r from-amber-100 via-yellow-100 to-amber-50",border:"border-amber-300",text:"text-amber-900",connector:"bg-amber-300"},WRITING:{bg:"bg-gradient-to-r from-indigo-100 via-blue-100 to-indigo-50",border:"border-indigo-300",text:"text-indigo-900",connector:"bg-indigo-300"},SPEAKING:{bg:"bg-gradient-to-r from-violet-100 via-purple-100 to-violet-50",border:"border-violet-300",text:"text-violet-900",connector:"bg-violet-300"}};return r.length<=1&&!d.parts.some(e=>e.category)?d.parts.map((e,t)=>{const r=h(e),a=b(e),s=c===e.id,n=x(t);return u(e,0,r,a,s,n)}):r.map((r,n)=>{const l=s[r.groupKey]||{bg:"bg-gray-50",border:"border-gray-200",text:"text-gray-800",connector:"bg-gray-300"},i=a[r.groupKey]||r.groupKey;return t("div",{className:"space-y-2",children:[e("div",{className:o("rounded-lg border-2 px-3 py-2 text-center",l.bg,l.border),children:e("span",{className:o("text-xs font-bold tracking-wide uppercase",l.text),children:i})}),e("div",{className:"relative ml-3 pl-5",children:e("div",{className:"space-y-3",children:r.parts.map(({part:a,originalIndex:s},n)=>{const i=h(a),d=b(a),m=c===a.id,g=x(s),p=0===n,N=n===r.parts.length-1;return t("div",{className:"relative",children:[!p&&e("div",{className:o("absolute -left-5 top-0 h-1/2 w-[2px]",l.connector)}),!N&&e("div",{className:o("absolute -left-5 top-1/2 w-[2px]",l.connector),style:{height:"calc(50% + 12px)"}}),p&&e("div",{className:o("absolute -left-5 -top-2 h-[calc(50%+8px)] w-[2px]",l.connector)}),e("div",{className:o("absolute -left-5 top-1/2 -translate-y-1/2 h-[2px] w-5",l.connector)}),u(a,0,i,d,m,g)]},a.id)})})})]},`group-${n}`)})})()}),t("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-br from-gray-50 to-slate-100 p-4 shadow-sm",children:[e("h4",{className:"mb-3 text-xs font-bold uppercase tracking-wide text-gray-500",children:"Tổng quan"}),t("div",{className:"space-y-2",children:[t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-2 w-2 rounded-full bg-blue-500"}),e("span",{className:"text-sm text-gray-600",children:"Tổng câu hỏi"})]}),e("span",{className:"text-sm font-bold text-gray-900",children:d.totalQuestions})]}),t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-2 w-2 rounded-full bg-green-500"}),e("span",{className:"text-sm text-gray-600",children:"Đã hoàn thành"})]}),e("span",{className:"text-sm font-bold text-green-600",children:m.size})]}),t("div",{className:"flex items-center justify-between",children:[t("div",{className:"flex items-center gap-2",children:[e("div",{className:"h-2 w-2 rounded-full bg-amber-500"}),e("span",{className:"text-sm text-gray-600",children:"Còn lại"})]}),e("span",{className:"text-sm font-bold text-amber-600",children:d.totalQuestions-m.size})]})]}),t("div",{className:"mt-3 pt-3 border-t border-gray-200",children:[t("div",{className:"flex items-center justify-between mb-1",children:[e("span",{className:"text-xs text-gray-500",children:"Tiến độ tổng"}),t("span",{className:"text-xs font-bold text-blue-600",children:[Math.round(m.size/d.totalQuestions*100),"%"]})]}),e("div",{className:"h-2 overflow-hidden rounded-full bg-gray-200",children:e("div",{className:"h-full rounded-full bg-gradient-to-r from-blue-500 to-indigo-600 transition-all duration-500",style:{width:m.size/d.totalQuestions*100+"%"}})})]})]})]})}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ExamQuestionsSdkApi } from '../../api/exam-questions.types';
|
|
2
|
+
export interface ExamPreviewDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
/** Exam (đề) to preview. `null` while nothing is selected. */
|
|
6
|
+
examId: string | null;
|
|
7
|
+
/** Questions API adapter — provides `getExam` + `getQuestions`. */
|
|
8
|
+
api: ExamQuestionsSdkApi;
|
|
9
|
+
/** Fallback heading shown until the exam name loads. */
|
|
10
|
+
title?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read-only popup that previews an exam (đề) by id: the layout mirrors the
|
|
14
|
+
* exam taking experience (left sidebar with exam info + progress + question
|
|
15
|
+
* navigation, right pane showing one question at a time) but creates NO attempt
|
|
16
|
+
* — exam + questions are fetched through {@link ExamQuestionsSdkApi}, so it
|
|
17
|
+
* matches exactly how the đề looks. Each question renders via {@link QuestionViewer}
|
|
18
|
+
* in review mode (correct answers highlighted).
|
|
19
|
+
*/
|
|
20
|
+
export declare function ExamPreviewDialog({ open, onOpenChange, examId, api, title }: ExamPreviewDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useEffect as a,useMemo as n,useRef as i,useState as s}from"react";import{ArrowLeft as r,ArrowRight as l,Eye as d,Loader2 as m}from"lucide-react";import{Badge as c}from"../../components/ui/badge";import{Button as o}from"../../components/ui/button";import{Card as p,CardContent as x,CardHeader as u,CardTitle as h}from"../../components/ui/card";import{Dialog as g,DialogContent as f,DialogHeader as N,DialogTitle as v}from"../../components/ui/dialog";import{Progress as b}from"../../components/ui/progress";import{useT as y}from"../../shared/lib/i18n";import{QuestionViewer as w}from"../questions/viewer/QuestionViewer";function j(e){return e&&"object"==typeof e&&"data"in e?e.data:e}function I(e){switch(e?.toUpperCase()){case"EASY":return"bg-green-100 text-green-800";case"MEDIUM":return"bg-orange-100 text-orange-800";case"HARD":return"bg-red-100 text-red-800";default:return"bg-gray-100 text-gray-800"}}export function ExamPreviewDialog({open:q,onOpenChange:S,examId:C,api:Q,title:k}){const E=y(),[P,z]=s(null),[A,M]=s([]),[$,D]=s(0),[T,O]=s("all"),[R,U]=s(!1),[H,V]=s(!1),[Y,B]=s(null),[F,G]=s(0),J=i(null);a(()=>{if(!q||!C)return;let e=!1;return V(!0),B(null),D(0),O("all"),(async()=>{try{const t=j(await Q.getExam(C)),a=Q.getQuestions?j(await Q.getQuestions(C)):[];if(e)return;const n=[...a].sort((e,t)=>(e.questionNumber??0)-(t.questionNumber??0));z(t),M(n)}catch{e||B(E("admin.exams.preview.loadError"))}finally{e||V(!1)}})(),()=>{e=!0}},[q,C,F,Q,E]),a(()=>{q||(z(null),M([]),D(0),O("all"),U(!1),B(null))},[q]);const K=n(()=>{const e=(P?.template?.parts??[]).map(e=>({id:e.id,name:e.name||e.title})),t=new Set(e.map(e=>e.id));for(const a of A){const n=a.partId??"";n&&!t.has(n)&&(t.add(n),e.push({id:n}))}return e},[P,A]),L=e=>{const t=K.findIndex(t=>t.id===(e??""));return t>=0?t+1:1},W=n(()=>{const e=new Map;for(const t of A){const a=t.partId??"";if(!e.has(a)){const t=K.findIndex(e=>e.id===a),n=t>=0?t+1:1;e.set(a,{partId:a,partNo:n,partName:K[t]?.name||`Part ${n}`,questions:[]})}e.get(a).questions.push(t)}return Array.from(e.values()).sort((e,t)=>e.partNo-t.partNo)},[A,K]),X=P?.name??k??"",Z=P?.template?.totalTime??P?.duration??0,_=function(e){return e.reduce((e,t)=>e+(Number(t.points)||0),0)}(A),ee=A[$],te=A.length>0?($+1)/A.length*100:0,ae=e=>{D(e),J.current?.scrollTo({top:0,behavior:"smooth"})},ne="all"===T?A:A.filter(e=>L(e.partId)===T);return e(g,{open:q,onOpenChange:S,children:t(f,{className:"max-h-[95vh] max-w-7xl overflow-y-auto",children:[e(N,{children:t(v,{className:"flex items-center gap-2",children:[e(d,{className:"h-5 w-5 text-blue-600"}),E("admin.exams.preview.title",{name:X})]})}),t("div",{className:"flex h-[80vh] flex-col gap-4 sm:flex-row",children:[e("div",{className:"w-full overflow-y-auto border-b border-gray-200 sm:w-80 sm:border-b-0 sm:border-r",children:t("div",{className:"space-y-4 p-4",children:[t(p,{children:[e(u,{className:"pb-3",children:e(h,{className:"text-base",children:E("admin.exams.preview.examInfo")})}),t(x,{className:"space-y-3",children:[t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm text-gray-600",children:E("admin.exams.preview.duration")}),e("span",{className:"text-sm font-medium",children:E("admin.exams.view.minutes",{count:String(Z)})})]}),t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm text-gray-600",children:E("admin.exams.preview.questionCount")}),e("span",{className:"text-sm font-medium",children:H?"...":E("admin.exams.preview.questions",{count:String(A.length)})})]}),t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm text-gray-600",children:E("admin.exams.preview.totalPoints")}),e("span",{className:"text-sm font-medium",children:E("admin.exams.preview.points",{count:String(_)})})]})]})]}),!H&&A.length>0&&e(p,{children:e(x,{className:"p-4",children:t("div",{className:"space-y-2",children:[t("div",{className:"flex justify-between text-sm",children:[e("span",{children:E("admin.exams.preview.progress")}),t("span",{className:"font-medium",children:[$+1,"/",A.length]})]}),e(b,{value:te,className:"h-2"})]})})}),H?e("div",{className:"flex items-center justify-center py-4",children:t("div",{className:"flex items-center gap-2 text-gray-500",children:[e(m,{className:"h-4 w-4 animate-spin"}),e("span",{className:"text-sm",children:E("admin.exams.preview.loading")})]})}):A.length>0&&t("div",{className:"space-y-3",children:[e("h4",{className:"text-sm font-medium text-gray-700",children:E("admin.exams.preview.questionNavigation")}),t("div",{className:"flex flex-wrap gap-2",children:[e(o,{variant:"all"===T?"default":"outline",size:"sm",onClick:()=>O("all"),className:"h-8 px-3 text-xs font-medium",children:E("admin.exams.preview.all")}),W.map(e=>t(o,{variant:T===e.partNo?"default":"outline",size:"sm",onClick:()=>{O(e.partNo);const t=e.questions[0];if(t){const e=A.findIndex(e=>e.id===t.id);-1!==e&&ae(e)}},className:"h-8 px-3 text-xs font-medium",children:["P",e.partNo]},e.partId||`part-${e.partNo}`))]}),e("div",{className:"rounded bg-gray-50 p-2 text-xs",children:e("span",{className:"font-medium",children:"all"===T?E("admin.exams.preview.allQuestions",{count:String(A.length)}):E("admin.exams.preview.partQuestions",{part:String(T),count:String(W.find(e=>e.partNo===T)?.questions.length??0)})})}),e("div",{className:"rounded-lg bg-gray-50 p-3",children:e("div",{className:"grid max-h-64 grid-cols-6 gap-2 overflow-y-auto pr-2",children:ne.map(t=>{const a=A.findIndex(e=>e.id===t.id);return e(o,{variant:"ghost",size:"sm",onClick:()=>ae(a),className:"h-8 w-full p-0 text-xs font-medium transition-all duration-200 "+(a===$?"border-2 border-blue-500 bg-blue-500 text-white hover:bg-blue-600":"border border-gray-300 bg-white hover:bg-gray-100"),children:t.questionNumber},t.id)})})})]}),!H&&A.length>0&&t("div",{children:[t("div",{className:"mb-2 flex items-center justify-between",children:[e("h4",{className:"text-sm font-medium text-gray-700",children:X}),e(o,{variant:"ghost",size:"sm",onClick:()=>U(e=>!e),className:"h-6 px-2 text-xs text-gray-600 hover:text-gray-900",children:E(R?"admin.exams.preview.collapse":"admin.exams.preview.detail")})]}),e("div",{className:"rounded-lg bg-gray-50 p-3",children:t("div",{className:"flex items-center justify-between",children:[t("div",{children:[t("span",{className:"text-sm font-medium text-gray-900",children:["Total: ",E("admin.exams.preview.questions",{count:String(A.length)})]}),e("div",{className:"mt-1 flex gap-3",children:t("span",{className:"text-xs text-gray-600",children:[W.length," part(s)"]})})]}),e("div",{className:"text-xs text-gray-500",children:E("admin.exams.view.minutes",{count:String(Z)})})]})}),R&&e("div",{className:"mt-3 space-y-2",children:W.map(a=>{const n=L(ee?.partId)===a.partNo;return e("div",{className:"cursor-pointer rounded-lg border-2 p-3 transition-all duration-200 "+(n?"border-blue-400 bg-blue-50 shadow-md":"border-gray-200 bg-white hover:border-gray-300"),onClick:()=>{const e=a.questions[0];if(e){const t=A.findIndex(t=>t.id===e.id);-1!==t&&(ae(t),O(a.partNo))}},children:t("div",{className:"flex items-center justify-between",children:[e("span",{className:"text-sm font-medium "+(n?"text-blue-900":"text-gray-900"),children:a.partName}),e(c,{variant:"secondary",className:"text-xs "+(n?"bg-blue-200 text-blue-800":"bg-gray-100 text-gray-700"),children:E("admin.exams.preview.questions",{count:String(a.questions.length)})})]})},a.partId||`part-${a.partNo}`)})})]})]})}),e("div",{className:"flex-1 overflow-y-auto",children:t("div",{className:"flex h-full flex-col",children:[t("div",{className:"flex items-center justify-between border-b pb-4",children:[t("div",{children:[e("h3",{className:"text-lg font-semibold",children:H?e("span",{className:"text-gray-400",children:E("admin.exams.preview.loading")}):A.length>0?E("admin.exams.preview.questionIndex",{index:String($+1),total:String(A.length)}):E("admin.exams.preview.noQuestions")}),ee&&e("div",{className:"mt-1 inline-flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5",children:e("span",{className:"text-sm font-semibold text-blue-700",children:(e=>{const t=K.findIndex(t=>t.id===(e??""));return K[t]?.name||`Part ${t>=0?t+1:1}`})(ee.partId)})})]}),ee&&t("div",{className:"flex items-center gap-2",children:[e(c,{variant:"outline",children:E("admin.exams.preview.points",{count:String(Number(ee.points)||1)})}),e(c,{className:I(ee.difficulty),children:ee.difficulty??"N/A"})]})]}),e("div",{ref:J,className:"flex-1 overflow-y-auto py-4",children:H?t("div",{className:"flex flex-col items-center justify-center space-y-4 py-12",children:[e(m,{className:"h-8 w-8 animate-spin text-blue-500"}),e("p",{className:"text-gray-600",children:E("admin.exams.preview.loadingQuestions")})]}):Y?e("div",{className:"flex flex-col items-center justify-center space-y-4 py-12",children:t("div",{className:"rounded-lg border border-red-200 bg-red-50 p-6 text-center",children:[e("p",{className:"font-medium text-red-800",children:Y}),e(o,{variant:"outline",onClick:()=>G(e=>e+1),className:"mt-4",children:E("admin.exams.preview.retry")})]})}):0===A.length?e("div",{className:"flex flex-col items-center justify-center space-y-4 py-12",children:e("div",{className:"rounded-lg border border-yellow-200 bg-yellow-50 p-6 text-center",children:e("p",{className:"font-medium text-yellow-800",children:E("admin.exams.preview.examNoQuestions")})})}):ee?e(w,{question:ee,isReviewMode:!0}):null}),!H&&A.length>0&&t("div",{className:"mt-auto flex items-center justify-between border-t border-gray-200 bg-gray-50 px-4 py-3",children:[t(o,{variant:"outline",onClick:()=>{$>0&&ae($-1)},disabled:0===$,className:"flex h-9 items-center gap-2 px-3 text-sm",children:[e(r,{className:"h-4 w-4"}),e("span",{className:"hidden sm:inline",children:E("admin.exams.preview.prevQuestion")}),e("span",{className:"sm:hidden",children:E("admin.exams.preview.prev")})]}),t("div",{className:"text-sm text-gray-600",children:[$+1," / ",A.length]}),t(o,{variant:"outline",onClick:()=>{$<A.length-1&&ae($+1)},disabled:$===A.length-1,className:"flex h-9 items-center gap-2 px-3 text-sm",children:[e("span",{className:"hidden sm:inline",children:E("admin.exams.preview.nextQuestion")}),e("span",{className:"sm:hidden",children:E("admin.exams.preview.next")}),e(l,{className:"h-4 w-4"})]})]})]})})]}),e("div",{className:"border-t pt-4",children:t("div",{className:"flex items-center justify-between",children:[e("div",{className:"text-sm text-gray-600",children:E("admin.exams.preview.footerNote")}),e(o,{variant:"outline",onClick:()=>S(!1),className:"border-gray-300",children:E("admin.exams.preview.close")})]})})]})})}
|
|
@@ -5,5 +5,5 @@ interface ExamQuestionGridProps {
|
|
|
5
5
|
completedQuestions: Set<number>;
|
|
6
6
|
onQuestionChange: (questionIndex: number) => void;
|
|
7
7
|
}
|
|
8
|
-
export declare function ExamQuestionGrid({ template, currentQuestionIndex, completedQuestions, onQuestionChange }: ExamQuestionGridProps): import("react").JSX.Element;
|
|
8
|
+
export declare function ExamQuestionGrid({ template, currentQuestionIndex, completedQuestions, onQuestionChange, }: ExamQuestionGridProps): import("react").JSX.Element;
|
|
9
9
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{CheckCircle2 as r,Circle as a,ListOrdered as s,Zap as n}from"lucide-react";import{cn as i}from"../../shared/lib/utils";import{getPartByQuestionIndex as
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{CheckCircle2 as r,Circle as a,ListOrdered as s,Zap as n}from"lucide-react";import{cn as i}from"../../shared/lib/utils";import{getPartByQuestionIndex as o,getSectionForQuestionIndex as d}from"./exam-template.utils";export function ExamQuestionGrid({template:l,currentQuestionIndex:c,completedQuestions:m,onQuestionChange:h}){const g=e=>{const t=(e=>e===c?"current":m.has(e)?"completed":"not-started")(e);return i("h-8 w-8 rounded-lg text-xs font-semibold transition-all duration-200","hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-1","border-2 cursor-pointer shadow-sm",{"bg-gradient-to-br from-blue-500 to-indigo-600 text-white border-transparent shadow-lg shadow-blue-300/50 ring-2 ring-blue-400 ring-offset-1":"current"===t,"bg-gradient-to-br from-green-400 to-emerald-500 text-white border-transparent hover:shadow-md hover:shadow-green-200":"completed"===t,"bg-white text-gray-500 border-gray-200 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700":"not-started"===t})},u=Array.from({length:l.totalQuestions},(e,t)=>t);return t("div",{className:"space-y-5",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-9 w-9 items-center justify-center rounded-lg bg-blue-600 shadow-sm",children:e(s,{className:"h-4 w-4 text-white"})}),t("div",{children:[e("h3",{className:"text-sm font-bold text-gray-900",children:"Danh sách câu hỏi"}),e("p",{className:"text-xs text-gray-500",children:"Click để chuyển câu"})]})]}),e("div",{className:"rounded-xl border border-gray-200 bg-gradient-to-b from-gray-50 to-white p-4 shadow-inner",children:e("div",{className:"grid max-h-[320px] grid-cols-6 gap-3 overflow-y-auto px-3 py-2 justify-center",children:u.map(t=>{const r=t+1,a=o(l,t),s=a?d(a,t):void 0,n=[`Câu ${r}`,a?.name||"Unknown Part"];return s&&n.push(s.name),e("button",{onClick:()=>h(t),className:g(t),title:n.join(" - "),children:r},t)})})}),t("div",{className:"rounded-xl border border-gray-100 bg-white p-3 shadow-sm",children:[e("div",{className:"mb-2 text-xs font-bold uppercase tracking-wide text-gray-400",children:"Chú thích"}),t("div",{className:"grid grid-cols-1 gap-2",children:[t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 shadow-md",children:e(n,{className:"h-3.5 w-3.5 text-white"})}),e("span",{className:"text-xs font-medium text-gray-700",children:"Câu đang làm"})]}),t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-green-400 to-emerald-500 shadow-sm",children:e(r,{className:"h-3.5 w-3.5 text-white"})}),e("span",{className:"text-xs font-medium text-gray-700",children:"Đã hoàn thành"})]}),t("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-7 w-7 items-center justify-center rounded-lg border-2 border-gray-200 bg-white",children:e(a,{className:"h-3.5 w-3.5 text-gray-400"})}),e("span",{className:"text-xs font-medium text-gray-700",children:"Chưa tạo"})]})]})]})]})}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { UserRole } from '../../shared/types/common.types';
|
|
2
2
|
import type { ExamQuestionsSdkApi } from '../../api/exam-questions.types';
|
|
3
|
+
import type { CreateExamTexts } from './create-exam.texts';
|
|
3
4
|
export interface ExamQuestionsPageContainerProps {
|
|
4
5
|
examId: string;
|
|
5
6
|
role?: UserRole.ADMIN | UserRole.TEACHER;
|
|
6
7
|
api: ExamQuestionsSdkApi;
|
|
7
8
|
listPath?: string;
|
|
8
9
|
onNavigate?: (href: string) => void;
|
|
10
|
+
/** Optional label overrides for embedded contexts. */
|
|
11
|
+
texts?: CreateExamTexts;
|
|
9
12
|
}
|
|
10
13
|
export declare function ExamQuestionsPageContainer(props: ExamQuestionsPageContainerProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{Suspense as a,useEffect as i,useMemo as n,useRef as s,useState as r}from"react";import{ArrowLeft as o,Hash as l,Loader2 as m,Save as c}from"lucide-react";import{Badge as u}from"../../components/ui/badge";import{Button as d}from"../../components/ui/button";import{useToast as f}from"../../shared/lib/hooks/useToast";import{useT as h}from"../../shared/lib/i18n";import{transformApiQuestionsToFrontend as p}from"../../shared/lib/utils/question-reverse-transform";import{ExamCreator as x}from"./ExamCreator";import{initializeQuestionStatusesFromAPI as b}from"./exam-question-status";function g({api:a,examId:m,listPath:g="/manage/exams",onNavigate:w,texts:q}){const Q=h(),{error:k,success:E}=f(),L=s(null),[j,C]=r(null),[I,P]=r([]),[T,A]=r({}),[z,B]=r(!0),D=e=>{w?w(e):"undefined"!=typeof window&&(window.location.href=e)};i(()=>{let e=!1;return B(!0),Promise.all([a.getExam(m),a.getQuestions?.(m)]).then(([t,a])=>{if(e)return;const i=y(t),n=function(e){return 0===e.length?[]:e.some(e=>"question_type"in e||"questionType"in e)?p(e):e}((void 0===(s=a)?void 0:y(s))??[]);var s;C(i),P(n);const r=function(e){return e?.totalQuestions??0}(i.template);r>0&&b(m,n.map(e=>e.questionNumber??0),r)}).catch(e=>k({message:Q("admin.exams.questions.error"),description:e instanceof Error?e.message:Q("admin.exams.questions.loadError")})).finally(()=>!e&&B(!1)),()=>{e=!0}},[a,m,k,Q]);const S=n(()=>j?.template?function(e){return"name"in e?e:{...e,name:e.title,description:e.description||e.subTitle||"",level:e.examLevel,parts:e.parts.map(e=>({...e,name:e.name||e.title||""}))}}(j.template):null,[j?.template]);return z?e(v,{label:q?.questionsLoadingLabel??Q("admin.exams.questions.loading")}):j&&S?t("div",{className:"-m-4 flex h-[calc(100vh-4rem)] flex-col overflow-hidden md:-m-6 md:-mb-10",children:[t("div",{className:"flex flex-shrink-0 items-center gap-4 border-b border-gray-200/60 bg-white px-4 py-3",children:[e(d,{variant:"ghost",size:"icon",onClick:()=>D(g),className:"h-8 w-8",children:e(o,{className:"h-4 w-4"})}),e("div",{className:"min-w-0 flex-1",children:t("h1",{className:"truncate text-lg font-semibold tracking-tight text-gray-900",children:[j.name,j.code&&t(u,{variant:"outline",className:"ml-2 align-middle border-blue-200 bg-blue-50 text-blue-700 text-xs font-normal",children:[e(l,{className:"mr-0.5 h-3 w-3"}),j.code]})]})}),t(d,{onClick:()=>L.current?.(),className:"gap-2 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 shadow-sm",size:"sm",children:[e(c,{className:"h-4 w-4"}),q?.saveQuestionsLabel??"Lưu đề thi"]})]}),e(x,{template:S,examId:m,examDifficulty:j.difficulty,examLevel:j.examLevel??void 0,subjectId:j.subject?.id,initialQuestions:I,onSaveQuestion:a.saveQuestion,onSave:async e=>{S&&(await(a.saveExam?.({examId:m,questions:e.questions,template:S,examData:e})),D(g))},onCancel:()=>D(g),onPartAudioChange:a.updatePartAudio?async(e,t)=>{if(!S)return;const i=T[e];A(a=>({...a,[e]:t}));try{await(a.updatePartAudio?.({examId:m,partId:e,audioUrl:t,template:S})),E({message:Q("admin.exams.partEditor.audioFile"),description:"Đã lưu file âm thanh cho part."})}catch(t){A(t=>{const a={...t};return void 0===i?delete a[e]:a[e]=i,a}),k({message:Q("admin.exams.questions.error"),description:t instanceof Error?t.message:void 0})}}:void 0,partAudioOverrides:T,searchQuestions:a.searchQuestions,saveExamRef:L})]}):e(N,{onBack:()=>D(g),label:q?.questionsNoTemplateLabel??Q("admin.exams.questions.noTemplate")})}export function ExamQuestionsPageContainer(t){return e(a,{fallback:e(v,{label:"Loading..."}),children:e(g,{...t})})}function v({label:a}){return e("div",{className:"flex h-screen items-center justify-center",children:t("div",{className:"flex flex-col items-center gap-3",children:[e(m,{className:"h-8 w-8 animate-spin text-blue-600"}),e("p",{className:"text-sm text-gray-600",children:a})]})})}function N({label:a,onBack:i}){return e("div",{className:"flex h-screen items-center justify-center",children:t("div",{className:"text-center",children:[e("p",{className:"text-lg text-gray-900",children:a}),e(d,{onClick:i,className:"mt-4",children:"Quay lại"})]})})}function y(e){return"object"==typeof e&&null!==e&&"data"in e?e.data:e}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const EXAM_DIFFICULTY: {
|
|
2
|
+
readonly EASY: "EASY";
|
|
3
|
+
readonly MEDIUM: "MEDIUM";
|
|
4
|
+
readonly HARD: "HARD";
|
|
5
|
+
};
|
|
6
|
+
export type ExamDifficulty = (typeof EXAM_DIFFICULTY)[keyof typeof EXAM_DIFFICULTY];
|
|
7
|
+
export interface ExamViewDialogExam {
|
|
8
|
+
id: string;
|
|
9
|
+
code?: string;
|
|
10
|
+
name: string;
|
|
11
|
+
subject: string;
|
|
12
|
+
level: string;
|
|
13
|
+
duration: number;
|
|
14
|
+
questionCount: number;
|
|
15
|
+
totalPoints: number;
|
|
16
|
+
difficulty: ExamDifficulty;
|
|
17
|
+
description?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ExamViewDialogProps {
|
|
20
|
+
open: boolean;
|
|
21
|
+
onOpenChange: (open: boolean) => void;
|
|
22
|
+
exam: ExamViewDialogExam | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function ExamViewDialog({ open, onOpenChange, exam }: ExamViewDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import{Dialog as n,DialogContent as i,DialogDescription as r,DialogFooter as s,DialogHeader as l,DialogTitle as t}from"../../components/ui/dialog";import{Button as o}from"../../components/ui/button";import{Badge as c}from"../../components/ui/badge";import{BookOpen as m,Clock as d,FileText as g,GraduationCap as b,Hash as u,Star as x,Target as h,X as N}from"lucide-react";import{useT as p}from"../../shared/lib/i18n";export const EXAM_DIFFICULTY={EASY:"EASY",MEDIUM:"MEDIUM",HARD:"HARD"};function v({exam:n}){return e("div",{className:"bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-5 text-white",children:e(l,{className:"space-y-2",children:e("div",{className:"flex items-start justify-between",children:a("div",{className:"flex items-center gap-3",children:[e("div",{className:"flex h-12 w-12 items-center justify-center rounded-xl bg-white/20 backdrop-blur-sm",children:e(g,{className:"h-6 w-6 text-white"})}),a("div",{children:[e(t,{className:"text-xl font-bold text-white",children:n.name}),e(r,{className:"text-blue-100 mt-1",children:a("span",{className:"inline-flex items-center gap-1.5",children:[e(u,{className:"h-3.5 w-3.5"}),n.code||n.id]})})]})]})})})})}function C({label:n,value:i,icon:r,iconContainerClassName:s,iconClassName:l,hoverClassName:t}){return e("div",{className:`rounded-xl border border-gray-200 bg-gray-50/50 p-4 transition-all ${t}`,children:a("div",{className:"flex items-center gap-3",children:[e("div",{className:`flex h-10 w-10 items-center justify-center rounded-lg ${s}`,children:e(r,{className:`h-5 w-5 ${l}`})}),a("div",{children:[e("p",{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:n}),e("p",{className:"text-sm font-semibold text-gray-900 mt-0.5",children:i})]})]})})}function f({label:n,config:i}){const r=i.icon;return e("div",{className:"rounded-xl border border-gray-200 bg-gray-50/50 p-4 transition-all hover:border-gray-300",children:a("div",{className:"flex items-center gap-3",children:[e("div",{className:`flex h-10 w-10 items-center justify-center rounded-lg ${i.iconContainerClassName}`,children:e(r,{className:`h-5 w-5 ${i.iconClassName}`})}),a("div",{children:[e("p",{className:"text-xs font-medium text-gray-500 uppercase tracking-wide",children:n}),e(c,{className:`mt-1 ${i.badgeClassName} border`,children:i.label})]})]})})}function y({description:n,title:i}){return a("div",{className:"rounded-xl border border-gray-200 bg-gray-50/50 p-4",children:[a("h4",{className:"text-sm font-semibold text-gray-900 mb-2 flex items-center gap-2",children:[e(g,{className:"h-4 w-4 text-gray-500"}),i]}),e("p",{className:"text-sm text-gray-600 leading-relaxed",children:n})]})}function w({closeLabel:n,onClose:i}){return e(s,{className:"border-t border-gray-200 bg-gray-50/50 px-6 py-4",children:a(o,{variant:"outline",onClick:i,className:"gap-2",children:[e(N,{className:"h-4 w-4"}),n]})})}export function ExamViewDialog({open:r,onOpenChange:s,exam:l}){const t=p();if(!l)return null;const o=function(e,a){return{[EXAM_DIFFICULTY.EASY]:{label:a("admin.exams.difficulty.easy"),badgeClassName:"bg-green-50 text-green-700 border-green-200",iconContainerClassName:"bg-green-100",iconClassName:"text-green-600",icon:x},[EXAM_DIFFICULTY.MEDIUM]:{label:a("admin.exams.difficulty.medium"),badgeClassName:"bg-yellow-50 text-yellow-700 border-yellow-200",iconContainerClassName:"bg-yellow-100",iconClassName:"text-yellow-600",icon:x},[EXAM_DIFFICULTY.HARD]:{label:a("admin.exams.difficulty.hard"),badgeClassName:"bg-red-50 text-red-700 border-red-200",iconContainerClassName:"bg-red-100",iconClassName:"text-red-600",icon:x}}[e]}(l.difficulty,t);return e(n,{open:r,onOpenChange:s,children:a(i,{className:"max-w-2xl p-0 overflow-hidden",children:[e(v,{exam:l}),a("div",{className:"px-6 py-5 space-y-6",children:[a("div",{className:"grid grid-cols-2 md:grid-cols-3 gap-4",children:[e(C,{label:t("admin.exams.view.subject"),value:l.subject,icon:m,iconContainerClassName:"bg-blue-100",iconClassName:"text-blue-600",hoverClassName:"hover:border-blue-200 hover:bg-blue-50/30"}),e(C,{label:t("admin.exams.view.level"),value:l.level,icon:b,iconContainerClassName:"bg-purple-100",iconClassName:"text-purple-600",hoverClassName:"hover:border-purple-200 hover:bg-purple-50/30"}),e(C,{label:t("admin.exams.view.duration"),value:t("admin.exams.view.minutes",{count:String(l.duration)}),icon:d,iconContainerClassName:"bg-orange-100",iconClassName:"text-orange-600",hoverClassName:"hover:border-orange-200 hover:bg-orange-50/30"}),e(C,{label:t("admin.exams.view.questionCount"),value:t("admin.exams.view.questions",{count:String(l.questionCount)}),icon:g,iconContainerClassName:"bg-cyan-100",iconClassName:"text-cyan-600",hoverClassName:"hover:border-cyan-200 hover:bg-cyan-50/30"}),e(C,{label:t("admin.exams.view.totalPoints"),value:t("admin.exams.view.points",{count:String(l.totalPoints)}),icon:h,iconContainerClassName:"bg-green-100",iconClassName:"text-green-600",hoverClassName:"hover:border-green-200 hover:bg-green-50/30"}),e(f,{label:t("admin.exams.view.difficulty"),config:o})]}),l.description&&e(y,{title:t("admin.exams.view.description"),description:l.description})]}),e(w,{closeLabel:t("admin.exams.view.close"),onClose:()=>s(!1)})]})})}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as
|
|
1
|
+
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import{useEffect as t,useState as l}from"react";import{BookOpen as s,ChevronLeft as n,ChevronRight as i,Clock as r,Loader2 as c,Search as o,Users as m}from"lucide-react";import{Button as d}from"../../components/ui/button";import{Card as p,CardContent as g,CardDescription as h,CardHeader as x,CardTitle as u}from"../../components/ui/card";import{Input as N}from"../../components/ui/input";import{useT as f}from"../../shared/lib/i18n";import{mapTemplateDetailToExamTemplate as v}from"../../api/exam-create";export function TemplateSelector({api:s,onTemplateSelect:r,onCancel:m}){const p=f(),[g,h]=l(""),[x,u]=l(1),[b,y]=l([]),[k,j]=l(1),[C,E]=l(0),[T,L]=l(!0),[S,D]=l(),[P,q]=l();t(()=>{u(1)},[g]),t(()=>{let e=!1;return L(!0),s.getExamTemplates({page:x,search:g||void 0}).then(a=>{e||(y(a.items),j(a.totalPages),E(a.totalItems))}).catch(a=>{e||D(a instanceof Error?a.message:p("admin.exams.questions.loadError"))}).finally(()=>{e||L(!1)}),()=>{e=!0}},[s,x,g,p]);const z=async e=>{D(void 0),q(e.id);try{const t="data"in(a=await s.getExamTemplateDetail(e.id))?a.data:a;r(v(t))}catch(e){D(e instanceof Error?e.message:p("admin.exams.questions.loadError"))}finally{q(void 0)}var a},A=x>1&&!T,I=x<k&&!T;return a("div",{className:"flex min-h-0 flex-1 flex-col gap-4",children:[a("div",{className:"relative",children:[e(o,{className:"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"}),e(N,{value:g,onChange:e=>h(e.target.value),placeholder:p("admin.templates.searchPlaceholder"),className:"pl-9"})]}),T?e("div",{className:"flex flex-1 items-center justify-center",children:e(c,{className:"h-6 w-6 animate-spin text-blue-600"})}):null,!T&&S?e("p",{className:"text-sm text-red-600",children:S}):null,T||S?null:e("div",{className:"grid flex-1 content-start gap-3 overflow-y-auto md:grid-cols-2 lg:grid-cols-3",children:b.map((a,t)=>e(w,{template:a,index:t,onSelect:z,isLoading:P===a.id,disabled:!!P},a.id))}),T||S||0!==b.length?null:e("p",{className:"flex-1 text-center text-sm text-gray-500",children:p("admin.templates.table.empty")}),!T&&!S&&C>0?a("div",{className:"flex shrink-0 items-center justify-between border-t border-gray-100 pt-3",children:[e("span",{className:"text-sm text-gray-500",children:p("admin.templates.table.showing",{count:b.length,total:C})}),a("div",{className:"flex items-center gap-2",children:[a(d,{variant:"outline",size:"sm",disabled:!A,onClick:()=>u(e=>Math.max(1,e-1)),children:[e(n,{className:"h-4 w-4"}),p("common.pagination.previous")]}),e("span",{className:"text-sm text-gray-600",children:p("admin.schedules.list.page",{current:x,total:k})}),a(d,{variant:"outline",size:"sm",disabled:!I,onClick:()=>u(e=>e+1),children:[p("common.pagination.next"),e(i,{className:"h-4 w-4"})]})]})]}):null,m&&e("div",{className:"flex shrink-0 justify-end",children:e(d,{variant:"outline",onClick:m,children:p("common.cancel")})})]})}const b=["blue","green","purple","orange","pink","indigo"],y={blue:"bg-blue-100 text-blue-600",green:"bg-green-100 text-green-600",purple:"bg-purple-100 text-purple-600",orange:"bg-orange-100 text-orange-600",pink:"bg-pink-100 text-pink-600",indigo:"bg-indigo-100 text-indigo-600"};function w({template:t,index:l,onSelect:n,isLoading:i,disabled:o}){const d=b[l%b.length];return a(p,{className:"relative cursor-pointer border-2 border-gray-200 bg-white transition-all duration-200 hover:border-gray-300 hover:bg-gray-50 hover:shadow-md "+(o?"pointer-events-none opacity-60":""),onClick:()=>n(t),children:[e(x,{className:"px-4 py-3",children:a("div",{className:"flex items-center gap-2",children:[e("div",{className:`rounded-md p-1.5 ${y[d]}`,children:e(s,{className:"h-4 w-4"})}),a("div",{className:"min-w-0 flex-1",children:[e(u,{className:"truncate text-sm font-semibold",children:t.title}),t.subTitle&&e(h,{className:"truncate text-[11px]",children:t.subTitle})]})]})}),e(g,{className:"space-y-2 px-4 pb-3 pt-0",children:a("div",{className:"grid grid-cols-2 gap-x-3 gap-y-1 text-xs",children:[a("div",{className:"flex items-center gap-1.5",children:[e(s,{className:"h-3 w-3 text-gray-500"}),a("span",{className:"text-gray-600",children:[t.totalQuestions," câu hỏi"]})]}),a("div",{className:"flex items-center gap-1.5",children:[e("div",{className:"h-3 w-3 rounded-full bg-gray-300"}),a("span",{className:"text-gray-600",children:[t.totalParts," phần"]})]}),t.examLevel&&a("div",{className:"flex items-center gap-1.5",children:[e(m,{className:"h-3 w-3 text-gray-500"}),a("span",{className:"text-gray-600",children:["Cấp độ ",t.examLevel]})]}),t.createdAt&&a("div",{className:"flex items-center gap-1.5",children:[e(r,{className:"h-3 w-3 text-gray-500"}),e("span",{className:"text-gray-600",children:new Date(t.createdAt).toLocaleDateString("vi-VN")})]})]})}),i?e("div",{className:"absolute inset-0 flex items-center justify-center rounded-xl bg-white/60",children:e(c,{className:"h-5 w-5 animate-spin text-blue-600"})}):null]})}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface UnsavedChangesDialogProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
onConfirm: () => void;
|
|
5
|
+
onCancel: () => void;
|
|
6
|
+
questionNumber?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function UnsavedChangesDialog({ open, onOpenChange, onConfirm, onCancel, questionNumber, }: UnsavedChangesDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import{AlertDialog as n,AlertDialogAction as c,AlertDialogCancel as r,AlertDialogContent as l,AlertDialogDescription as s,AlertDialogFooter as t,AlertDialogHeader as i,AlertDialogTitle as h}from"../../components/ui/alert-dialog";import{AlertTriangle as m,Save as o,X as d}from"lucide-react";export function UnsavedChangesDialog({open:g,onOpenChange:x,onConfirm:p,onCancel:u,questionNumber:b}){return e(n,{open:g,onOpenChange:x,children:a(l,{className:"max-w-md",children:[a("div",{className:"flex flex-col items-center pt-2 pb-4",children:[e("div",{className:"flex h-16 w-16 items-center justify-center rounded-full bg-amber-100 mb-4",children:e(m,{className:"h-8 w-8 text-amber-600"})}),a(i,{className:"text-center space-y-2",children:[e(h,{className:"text-xl font-semibold text-gray-900",children:"Xác nhận chuyển câu hỏi"}),e(s,{className:"text-center text-gray-600",children:b?a("span",{children:["Câu hỏi ",e("strong",{className:"text-gray-800",children:b})," có những thay đổi chưa được lưu."]}):e("span",{children:"Câu hỏi hiện tại có những thay đổi chưa được lưu."})})]})]}),e("div",{className:"rounded-lg border border-amber-200 bg-amber-50 p-4 mb-4",children:e("p",{className:"text-sm text-amber-800",children:"Nếu bạn tiếp tục mà không lưu, các thay đổi sẽ bị mất và không thể khôi phục."})}),a(t,{className:"flex flex-col-reverse sm:flex-row gap-2 sm:gap-3",children:[a(r,{onClick:u,className:"flex-1 gap-2 border-gray-300 hover:bg-gray-50",children:[e(o,{className:"h-4 w-4"}),"Quay lại chỉnh sửa"]}),a(c,{onClick:p,className:"flex-1 gap-2 bg-red-500 hover:bg-red-600 text-white",children:[e(d,{className:"h-4 w-4"}),"Hủy thay đổi"]})]})]})})}
|
|
@@ -2,4 +2,4 @@ export { buildExamTemplatePayload, createExamCreateApi, createMockCreateExamApi
|
|
|
2
2
|
export type { CreateExamFetchApiOptions, CreateExamPayload, CreateExamSdkApi, ExamFormData, ExamLevelOption, ExamResponse, ExamTemplatePayload, UpdateExamTemplateInput } from '../../api/exam-create';
|
|
3
3
|
export { createExamQuestionsApi, createExamQuestionsFetchApi, createMockExamQuestionsApi } from '../../api/exam-questions';
|
|
4
4
|
export { createExamTakingApi, createMockExamTakingApi } from '../../api/exam-taking';
|
|
5
|
-
export type { ExamQuestionsSdkApi, ExamWithTemplate, FetchApiOptions, SaveExamInput, SaveQuestionInput, UpdatePartAudioInput } from '../../api/exam-questions';
|
|
5
|
+
export type { ExamQuestionsSdkApi, ExamWithTemplate, ExistingQuestionBank, FetchApiOptions, QuestionSearchItem, SaveExamInput, SaveQuestionInput, SaveQuestionResult, SearchQuestionBankInput, SearchQuestionBankResult, UpdatePartAudioInput } from '../../api/exam-questions';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional label overrides for the exam-creation flow. Lets a consumer reuse
|
|
3
|
+
* these containers in a non-exam context (e.g. authoring homework) by passing
|
|
4
|
+
* its own wording. Every field is optional; when omitted the component falls
|
|
5
|
+
* back to its default i18n string, so existing usages are unaffected.
|
|
6
|
+
*/
|
|
7
|
+
export interface CreateExamTexts {
|
|
8
|
+
/** Page title — default 'Tạo đề thi mới'. */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** Step subtitle — default 'Bước 1: Nhập thông tin cơ bản của đề thi'. */
|
|
11
|
+
step1?: string;
|
|
12
|
+
/** Basic-info card title — default 'Thông tin cơ bản'. */
|
|
13
|
+
basicInfo?: string;
|
|
14
|
+
/** Required-fields hint shown under the basic-info card title. */
|
|
15
|
+
requiredHint?: string;
|
|
16
|
+
/** Name field label — default 'Tên đề thi'. */
|
|
17
|
+
nameLabel?: string;
|
|
18
|
+
/** Name field placeholder — default 'Nhập tên đề thi...'. */
|
|
19
|
+
namePlaceholder?: string;
|
|
20
|
+
/** Code field label — default 'Mã đề thi'. */
|
|
21
|
+
codeLabel?: string;
|
|
22
|
+
/** Code field placeholder — default 'Nhập mã đề thi...'. */
|
|
23
|
+
codePlaceholder?: string;
|
|
24
|
+
/** Duration field label — default 'Thời gian thi (phút)'. */
|
|
25
|
+
durationLabel?: string;
|
|
26
|
+
/** Description field label — default 'Mô tả'. */
|
|
27
|
+
descriptionLabel?: string;
|
|
28
|
+
/** Description field placeholder — default exam description placeholder. */
|
|
29
|
+
descriptionPlaceholder?: string;
|
|
30
|
+
/** Template section title — default 'Chọn Template'. */
|
|
31
|
+
templateSectionTitle?: string;
|
|
32
|
+
/** Empty template hint — default 'Chưa chọn template đề thi'. */
|
|
33
|
+
noTemplateText?: string;
|
|
34
|
+
/** Template picker dialog title — default 'Chọn template đề thi'. */
|
|
35
|
+
templateDialogTitle?: string;
|
|
36
|
+
/** Template picker dialog description — default 'Chọn loại đề thi bạn muốn tạo.'. */
|
|
37
|
+
templateDialogDescription?: string;
|
|
38
|
+
/** Missing-template warning description. */
|
|
39
|
+
missingTemplateDescription?: string;
|
|
40
|
+
/** Button text for picking a template. */
|
|
41
|
+
selectTemplateLabel?: string;
|
|
42
|
+
/** Button text for changing a selected template. */
|
|
43
|
+
changeTemplateLabel?: string;
|
|
44
|
+
/** Cancel button label. */
|
|
45
|
+
cancelLabel?: string;
|
|
46
|
+
/** Saving button label. */
|
|
47
|
+
savingLabel?: string;
|
|
48
|
+
/** Submit button label. */
|
|
49
|
+
saveAndContinueLabel?: string;
|
|
50
|
+
/** Success toast description. Use {name} as placeholder for the created name. */
|
|
51
|
+
successDescription?: string;
|
|
52
|
+
/** Loading label for question-authoring screen. */
|
|
53
|
+
questionsLoadingLabel?: string;
|
|
54
|
+
/** Empty-template label for question-authoring screen. */
|
|
55
|
+
questionsNoTemplateLabel?: string;
|
|
56
|
+
/** Save button label for question-authoring screen. */
|
|
57
|
+
saveQuestionsLabel?: string;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{z as t}from"zod";export const examFormSchema=t.object({code:t.string().min(1,"Mã đề
|
|
1
|
+
import{z as t}from"zod";export const examFormSchema=t.object({code:t.string().min(1,"Mã đề kiểm tra không được để trống").max(100,"Mã đề kiểm tra không được vượt quá 100 ký tự").regex(/^[a-zA-Z0-9_-]+$/,"Mã đề kiểm tra chỉ được chứa chữ cái, số, dấu gạch ngang (-) và gạch dưới (_)").trim(),name:t.string().min(3,"Tên đề kiểm tra phải có ít nhất 3 ký tự").max(255,"Tên đề kiểm tra không được vượt quá 255 ký tự").trim(),level:t.string(),difficulty:t.string(),duration:t.string().refine(t=>{if(!t)return!0;const n=Number.parseInt(t,10);return!Number.isNaN(n)&&n>=5&&n<=300},{message:"Thời gian kiểm tra phải từ 5 đến 300 phút"}),totalPoints:t.string().min(1,"Vui lòng nhập tổng điểm").refine(t=>/^[1-9]\d*$/.test(t.trim()),{message:"Tổng điểm phải là số nguyên dương"}).refine(t=>{const n=Number.parseInt(t,10);return n>=1&&n<=1e3},{message:"Tổng điểm phải từ 1 đến 1000"}),description:t.string().max(2e3,"Mô tả không được vượt quá 2000 ký tự").trim()});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare enum QuestionStatus {
|
|
2
|
+
NEW = "new",
|
|
3
|
+
SAVED = "saved",
|
|
4
|
+
UNSAVED = "unsaved"
|
|
5
|
+
}
|
|
6
|
+
export declare function getQuestionStatus(examId: string, questionNumber: number): QuestionStatus;
|
|
7
|
+
export declare function setQuestionStatus(examId: string, questionNumber: number, status: QuestionStatus): void;
|
|
8
|
+
export declare function clearQuestionStatuses(examId: string): void;
|
|
9
|
+
export declare function initializeQuestionStatusesFromAPI(examId: string, savedQuestionNumbers: number[], totalQuestions: number): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export var QuestionStatus;!function(t){t.NEW="new",t.SAVED="saved",t.UNSAVED="unsaved"}(QuestionStatus||(QuestionStatus={}));const t=t=>`oe-exam-question-status:${t}`;function e(e){if("undefined"==typeof window)return{};const n=window.localStorage.getItem(t(e));if(!n)return{};try{return JSON.parse(n)}catch{return{}}}function n(e,n){"undefined"!=typeof window&&window.localStorage.setItem(t(e),JSON.stringify(n))}export function getQuestionStatus(t,n){return e(t)[n]??QuestionStatus.NEW}export function setQuestionStatus(t,o,i){n(t,{...e(t),[o]:i})}export function clearQuestionStatuses(e){"undefined"!=typeof window&&window.localStorage.removeItem(t(e))}export function initializeQuestionStatusesFromAPI(t,e,o){const i=new Set(e.filter(t=>Number.isFinite(t))),u={};for(let t=1;t<=o;t+=1)u[t]=i.has(t)?QuestionStatus.SAVED:QuestionStatus.NEW;n(t,u)}
|
|
@@ -3,6 +3,7 @@ export { CreateExamPageContainer } from './CreateExamPageContainer';
|
|
|
3
3
|
export type { CreateExamPageContainerProps } from './CreateExamPageContainer';
|
|
4
4
|
export { CreateExamInfoForm } from './CreateExamInfoForm';
|
|
5
5
|
export type { CreateExamInfoFormProps } from './CreateExamInfoForm';
|
|
6
|
+
export type { CreateExamTexts } from './create-exam.texts';
|
|
6
7
|
export { TemplateSelector } from './TemplateSelector';
|
|
7
8
|
export type { TemplateSelectorProps } from './TemplateSelector';
|
|
8
9
|
export { ExamCreator } from './ExamCreator';
|
|
@@ -12,6 +13,15 @@ export { ExamQuestionGrid } from './ExamQuestionGrid';
|
|
|
12
13
|
export { PartAudioUploader } from './PartAudioUploader';
|
|
13
14
|
export type { PartAudioUploaderProps } from './PartAudioUploader';
|
|
14
15
|
export { createExamQuestionsApi, createExamQuestionsFetchApi, createMockExamQuestionsApi } from './api';
|
|
15
|
-
export type { ExamQuestionsSdkApi, ExamWithTemplate, FetchApiOptions, SaveExamInput, SaveQuestionInput, UpdatePartAudioInput } from './api';
|
|
16
|
+
export type { ExamQuestionsSdkApi, ExamWithTemplate, ExistingQuestionBank, FetchApiOptions, QuestionSearchItem, SaveExamInput, SaveQuestionInput, SaveQuestionResult, SearchQuestionBankInput, SearchQuestionBankResult, UpdatePartAudioInput } from './api';
|
|
16
17
|
export { ExamQuestionsPageContainer } from './ExamQuestionsPageContainer';
|
|
17
18
|
export type { ExamQuestionsPageContainerProps } from './ExamQuestionsPageContainer';
|
|
19
|
+
export { UnsavedChangesDialog } from './UnsavedChangesDialog';
|
|
20
|
+
export type { UnsavedChangesDialogProps } from './UnsavedChangesDialog';
|
|
21
|
+
export { CompletionDialog } from './CompletionDialog';
|
|
22
|
+
export type { CompletionDialogProps } from './CompletionDialog';
|
|
23
|
+
export { EXAM_DIFFICULTY, ExamViewDialog } from './ExamViewDialog';
|
|
24
|
+
export type { ExamDifficulty, ExamViewDialogExam, ExamViewDialogProps } from './ExamViewDialog';
|
|
25
|
+
export { ExamPreviewDialog } from './ExamPreviewDialog';
|
|
26
|
+
export type { ExamPreviewDialogProps } from './ExamPreviewDialog';
|
|
27
|
+
export { QuestionStatus, clearQuestionStatuses, getQuestionStatus, initializeQuestionStatusesFromAPI, setQuestionStatus } from './exam-question-status';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./take";export{CreateExamPageContainer}from"./CreateExamPageContainer";export{CreateExamInfoForm}from"./CreateExamInfoForm";export{TemplateSelector}from"./TemplateSelector";export{ExamCreator}from"./ExamCreator";export{ExamPartTabs}from"./ExamPartTabs";export{ExamQuestionGrid}from"./ExamQuestionGrid";export{PartAudioUploader}from"./PartAudioUploader";export{createExamQuestionsApi,createExamQuestionsFetchApi,createMockExamQuestionsApi}from"./api";export{ExamQuestionsPageContainer}from"./ExamQuestionsPageContainer";
|
|
1
|
+
export*from"./take";export{CreateExamPageContainer}from"./CreateExamPageContainer";export{CreateExamInfoForm}from"./CreateExamInfoForm";export{TemplateSelector}from"./TemplateSelector";export{ExamCreator}from"./ExamCreator";export{ExamPartTabs}from"./ExamPartTabs";export{ExamQuestionGrid}from"./ExamQuestionGrid";export{PartAudioUploader}from"./PartAudioUploader";export{createExamQuestionsApi,createExamQuestionsFetchApi,createMockExamQuestionsApi}from"./api";export{ExamQuestionsPageContainer}from"./ExamQuestionsPageContainer";export{UnsavedChangesDialog}from"./UnsavedChangesDialog";export{CompletionDialog}from"./CompletionDialog";export{EXAM_DIFFICULTY,ExamViewDialog}from"./ExamViewDialog";export{ExamPreviewDialog}from"./ExamPreviewDialog";export{QuestionStatus,clearQuestionStatuses,getQuestionStatus,initializeQuestionStatusesFromAPI,setQuestionStatus}from"./exam-question-status";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as e,jsxs as t}from"react/jsx-runtime";import s from"next/image";import r from"
|
|
1
|
+
import{jsx as e,jsxs as t}from"react/jsx-runtime";import s from"next/image";import r from"../../../components/themes/summer-sky/assets/images/clock.png";export function CambridgeYleTimer({remainingSeconds:a,totalQuestions:n,answeredCount:p}){const o=a%60,x=`${Math.floor(a/60).toString().padStart(2,"0")}:${o.toString().padStart(2,"0")}`;let m="bg-[#001590]",i="shadow-[0_4px_20px_rgba(0,0,0,0.25)]";return a<=120?(m="bg-[rgba(220,38,38,0.85)]",i="shadow-[0_0_24px_rgba(220,38,38,0.5)] animate-pulse"):a<=300&&(m="bg-[rgba(180,120,0,0.85)]"),t("div",{className:`flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] backdrop-blur-[8px] transition-all duration-300 ease-in-out border-none ${m} ${i}`,children:[e(s,{src:r,alt:"clock",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10"}),e("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:"'SVN-Volte Rounded', 'Quicksand', sans-serif",textShadow:"0 2px 4px rgba(0, 0, 0, 0.2)"},children:x})]})}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,9 +7,9 @@ export * as Results from './components/results';
|
|
|
7
7
|
export * as Themes from './components/themes';
|
|
8
8
|
export * as Shared from './shared';
|
|
9
9
|
export { buildExamTemplatePayload, createExamCreateApi, createExamQuestionsApi, createExamQuestionsFetchApi, createExamTakingApi, createMockCreateExamApi, createMockExamQuestionsApi, createMockExamTakingApi, createMockResultReviewApi, createResultReviewApi } from './api';
|
|
10
|
-
export type { AnswerGradingStatusItem, AnswersGradingStatusResult, CreateExamFetchApiOptions, CreateExamPayload, CreateExamSdkApi, ExamFormData, ExamLevelOption, ExamQuestionsSdkApi, ExamResponse, ExamTakingAnswerValue, ExamTakingAttempt, ExamTakingFetchApiOptions, ExamTakingQuestion, ExamTakingQuestionsResult, ExamTakingRoom, ExamTakingSdkApi, ExamTakingTemplatePart, ExamTemplatePayload, ExamWithTemplate, FetchApiOptions, GradingSummary, SaveExamInput, SaveExamTakingAnswersInput, SaveQuestionInput, SubmitExamTakingAttemptResult, UpdateExamTemplateInput, UpdatePartAudioInput, UploadAudioAnswerResult } from './api';
|
|
11
|
-
export { CreateExamPageContainer, ExamQuestionsPageContainer, ExamTakingPageContainer } from './components/exams';
|
|
12
|
-
export type { CreateExamPageContainerProps, ExamQuestionsPageContainerProps, ExamTakingPageContainerProps } from './components/exams';
|
|
10
|
+
export type { AnswerGradingStatusItem, AnswersGradingStatusResult, CreateExamFetchApiOptions, CreateExamPayload, CreateExamSdkApi, ExamFormData, ExamLevelOption, ExamQuestionsSdkApi, ExamResponse, ExamTakingAnswerValue, ExamTakingAttempt, ExamTakingFetchApiOptions, ExamTakingQuestion, ExamTakingQuestionsResult, ExamTakingRoom, ExamTakingSdkApi, ExamTakingTemplatePart, ExamTemplatePayload, ExamWithTemplate, ExistingQuestionBank, FetchApiOptions, GradingSummary, QuestionSearchItem, SaveExamInput, SaveExamTakingAnswersInput, SaveQuestionInput, SaveQuestionResult, SearchQuestionBankInput, SearchQuestionBankResult, SubmitExamTakingAttemptResult, UpdateExamTemplateInput, UpdatePartAudioInput, UploadAudioAnswerResult } from './api';
|
|
11
|
+
export { CreateExamPageContainer, ExamPreviewDialog, ExamQuestionsPageContainer, ExamTakingPageContainer, ExamViewDialog } from './components/exams';
|
|
12
|
+
export type { CreateExamPageContainerProps, CreateExamTexts, ExamPreviewDialogProps, ExamQuestionsPageContainerProps, ExamTakingPageContainerProps, ExamViewDialogExam, ExamViewDialogProps } from './components/exams';
|
|
13
13
|
export { ResultReviewPageContainer } from './components/results';
|
|
14
14
|
export type { ResultReviewPageContainerProps } from './components/results';
|
|
15
15
|
export { QuestionCreator } from './components/questions/creator/QuestionCreator';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*as Api from"./api";export*as Components from"./components";export*as Mcp from"./mcp";export*as Exams from"./components/exams";export*as Questions from"./components/questions";export*as Results from"./components/results";export*as Themes from"./components/themes";export*as Shared from"./shared";export{buildExamTemplatePayload,createExamCreateApi,createExamQuestionsApi,createExamQuestionsFetchApi,createExamTakingApi,createMockCreateExamApi,createMockExamQuestionsApi,createMockExamTakingApi,createMockResultReviewApi,createResultReviewApi}from"./api";export{CreateExamPageContainer,ExamQuestionsPageContainer,ExamTakingPageContainer}from"./components/exams";export{ResultReviewPageContainer}from"./components/results";export{QuestionCreator}from"./components/questions/creator/QuestionCreator";export{QuestionGroupCreator}from"./components/questions/creator/QuestionGroupCreator";export{renderDedicatedComponent}from"./components/questions/creator/dedicated-component-router";export{questionTypeRegistry}from"./components/questions/creator/question-type-registry";export{QUESTION_TYPE_CONFIG,QUESTION_TYPES_WITH_DEDICATED_COMPONENTS}from"./components/questions/_shared/config/question-types.config";export{validateQuestion}from"./components/questions/_shared/utils/question-validation";export{buildAnswerPayload}from"./components/questions/_shared/utils/answer-builder";export*from"./components/questions/_shared/types";
|
|
1
|
+
export*as Api from"./api";export*as Components from"./components";export*as Mcp from"./mcp";export*as Exams from"./components/exams";export*as Questions from"./components/questions";export*as Results from"./components/results";export*as Themes from"./components/themes";export*as Shared from"./shared";export{buildExamTemplatePayload,createExamCreateApi,createExamQuestionsApi,createExamQuestionsFetchApi,createExamTakingApi,createMockCreateExamApi,createMockExamQuestionsApi,createMockExamTakingApi,createMockResultReviewApi,createResultReviewApi}from"./api";export{CreateExamPageContainer,ExamPreviewDialog,ExamQuestionsPageContainer,ExamTakingPageContainer,ExamViewDialog}from"./components/exams";export{ResultReviewPageContainer}from"./components/results";export{QuestionCreator}from"./components/questions/creator/QuestionCreator";export{QuestionGroupCreator}from"./components/questions/creator/QuestionGroupCreator";export{renderDedicatedComponent}from"./components/questions/creator/dedicated-component-router";export{questionTypeRegistry}from"./components/questions/creator/question-type-registry";export{QUESTION_TYPE_CONFIG,QUESTION_TYPES_WITH_DEDICATED_COMPONENTS}from"./components/questions/_shared/config/question-types.config";export{validateQuestion}from"./components/questions/_shared/utils/question-validation";export{buildAnswerPayload}from"./components/questions/_shared/utils/answer-builder";export*from"./components/questions/_shared/types";
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -5,3 +5,6 @@ export * from './constants/routes';
|
|
|
5
5
|
export * from './lib/utils';
|
|
6
6
|
export * from '../components/questions/_shared';
|
|
7
7
|
export * from './types';
|
|
8
|
+
export * from './lib/utils/question-transform';
|
|
9
|
+
export * from './lib/utils/question-reverse-transform';
|
|
10
|
+
export * from './lib/utils/question-transform-validation';
|
package/dist/shared/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./constants/ApiConstant";export*from"./constants/QueryKeyConstant";export*from"./constants/exam-level.enum";export*from"./constants/routes";export*from"./lib/utils";export*from"../components/questions/_shared";export*from"./types";
|
|
1
|
+
export*from"./constants/ApiConstant";export*from"./constants/QueryKeyConstant";export*from"./constants/exam-level.enum";export*from"./constants/routes";export*from"./lib/utils";export*from"../components/questions/_shared";export*from"./types";export*from"./lib/utils/question-transform";export*from"./lib/utils/question-reverse-transform";export*from"./lib/utils/question-transform-validation";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ExamQuestion } from '../../types/entities/exam.types';
|
|
2
|
+
type JsonRecord = Record<string, unknown>;
|
|
3
|
+
type ApiQuestion = JsonRecord;
|
|
4
|
+
export declare function transformApiQuestionToFrontend(apiQuestion: ApiQuestion): Partial<ExamQuestion>;
|
|
5
|
+
export declare function transformApiQuestionsToFrontend(apiQuestions: ApiQuestion[]): Partial<ExamQuestion>[];
|
|
6
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n=n=>"object"==typeof n&&null!==n&&!Array.isArray(n),e=e=>n(e)?e:{},t=n=>Array.isArray(n)?n:[],o=(n,e="")=>"string"==typeof n?n:e,r=(n,e)=>"number"==typeof n&&Number.isFinite(n)?n:e,s=n=>e(n.correctAnswer??n.correct_answer),i=n=>e(n.questionGroup??n.question_group),_=n=>Number.parseFloat(String(n.total_points??n.points??1))||1,a=n=>{const e=n?.toUpperCase();return"EASY"===e||"MEDIUM"===e||"HARD"===e?e:void 0},E=n=>{const e=n?.toUpperCase();return"LISTENING"===e||"READING"===e||"WRITING"===e||"SPEAKING"===e?e:void 0},A=n=>({content:n.content,answer:s(n)}),N=n=>{const r=e(n.content),i=s(n);return{content:"",answer:{imageUrl:o(r.imageUrl),question:o(r.question),options:t(r.options).map(n=>"string"==typeof n?n:o(e(n).text)),correctAnswer:i.answer??i.correctAnswer??-1,explanation:o(n.explanation),points:_(n)}}},p=n=>{const t=e(n.content),a=s(n),E=i(n);return{content:"",answer:{...e(E.payload),...t,...a,groupNumber:r(E.groupNumber,1),explanation:o(n.explanation),points:_(n)}}},u=n=>{const t=s(n),r=e(n.content);return{content:n.content??"",answer:{...t,...r,explanation:o(n.explanation),points:_(n)}}},S={CHOOSE_THE_CORRECT_ANSWER:n=>{const r=e(n.content),i=s(n),a=i.answer??-1,E=Boolean(r.multipleAnswers??i.multipleAnswers??Array.isArray(a));return{content:"",answer:{question:o(r.question),optionType:o(r.optionType,"text"),options:t(r.options),imageUrl:o(r.imageUrl),askerName:o(r.askerName),answererName:o(r.answererName),correctAnswer:E?Array.isArray(a)?a:-1===a?[]:[a]:Array.isArray(a)?a[0]??-1:a,multipleAnswers:E,explanation:o(n.explanation),points:_(n)}}},LOOK_PICTURE_CHOOSE_CORRECT_ANSWER:N,LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER:N,FILL_IN_BLANK:n=>{const r=e(n.content),i=s(n),a=e(t(r.blanks)[0]);return{content:"",answer:{question:o(r.question??a.text),answers:Array.isArray(i.answers)?i.answers:[],caseSensitive:Boolean(i.caseSensitive??!1),imageUrl:o(r.imageUrl),showHints:Boolean(r.showHints??!1),hints:t(r.hints),explanation:o(n.explanation),points:_(n),blanks:t(r.blanks)}}},READ_PASSAGE_AND_ANSWER_QUESTIONS:n=>{const a=e(n.content),E=s(n),A=i(n),N=e(A.payload);return{content:"",answer:{passage:o(N.passage??a.passage),passageTitle:o(N.passageTitle??a.passageTitle),instructions:o(N.instructions??a.instructions),singleQuestion:{id:o(n.id,`question-${String(n.question_number??n.questionNumber??1)}`),question:o(a.question),options:t(a.options),correctAnswer:E.correctAnswer??E.answer??-1,points:_(n),groupNumber:r(A.groupNumber,1)}}}},LABEL_THE_PICTURE:n=>{const r=e(n.content),i=s(n),_=e(i.labels??i.answers);return{content:"",answer:{imageUrl:o(r.imageUrl),wordBank:t(r.wordBank),labels:t(r.labels).map(n=>{const t=e(n),r=o(t.id);return{...t,correctAnswer:t.correctAnswer??_[r]??""}})}}},READ_AND_COLOR_OBJECTS:p,IMAGE_OBJECT_MATCHING:p,FILL_MISSING_WORDS_IN_GRID:n=>({content:n.content??{},answer:s(n)}),LOOK_PICTURE_FILL_WORD_HINT:p,ANSWER_THE_QUESTION:p,TRUE_FALSE:u,WORD_ORDERING:u,WORD_FILL_PARAGRAPH:u,MATCH_BY_WRITING_ANSWER:p,WRITE_SENTENCES:p,WRITE_A_SHORT_LETTER:u,WRITE_CORRECT_VERB_FORM:u,CHOOSE_CORRECT_ADJECTIVE:u,WORD_FILL_STRUCTURED_FORM:p,LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP:p,LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE:p,LISTEN_AND_DRAG_OBJECTS_INTO_SCENE:p,LISTEN_AND_SPEAK_ANSWER:p,LISTEN_AND_SPEAK_IMAGE_GROUP:p,LISTEN_AND_SPEAK_QUESTION_LIST:p,LISTEN_AND_SPEAK_COMPARE_IMAGES:p,LISTEN_AND_SPEAK_WITH_STORY_IMAGES:p,LISTEN_AND_SPEAK_ODD_ONE_OUT:p,LISTEN_AND_SPEAK_INFO_EXCHANGE:p,SPEAKING_DESCRIBE_IMAGE:u,READ_DISPLAYED_CONTENT:u,SPONTANEOUS_QA:u,SPEAKING_CONVERSATION:p,SPEAKING_CUE_CARD:u,GN_SPEAKING_INTERVIEW:p};export function transformApiQuestionToFrontend(e){const t=String(e.questionType??e.question_type??e.type??""),s=(S[t]??A)(e),i=(e=>{const t=e.questionBank??e.question_bank;return n(t)?t:void 0})(e);return{id:o(e.id),type:t,content:s.content,answer:s.answer,points:_(e),level:(N=e.level,"Pre-A1"===N||"A1"===N||"A2"===N||"B1"===N||"B2"===N||"C1"===N||"C2"===N?N:void 0),difficulty:a(o(e.difficulty??e.difficult_level)),skill:E(o(e.questionSkillType??e.question_skill_type??e.skill)),explanation:o(e.explanation),partId:o(e.partId??e.part_id),questionNumber:r(e.questionNumber??e.question_number,1),isCompleted:!0,questionGroup:e.questionGroup,questionBank:i?{id:o(i.id),code:o(i.code),title:o(i.title)}:void 0};var N}export function transformApiQuestionsToFrontend(n){return n.map(transformApiQuestionToFrontend)}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ExamQuestion } from '../../types/entities/exam.types';
|
|
2
|
+
export interface QuestionTransformValidationResult {
|
|
3
|
+
isValid: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function validateQuestionForTransform(question: Partial<ExamQuestion>): QuestionTransformValidationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n=n=>"string"==typeof n&&n.trim().length>0,t=n=>Array.isArray(n)&&n.length>0,e=n=>"object"==typeof n&&null!==n&&!Array.isArray(n),s=n=>e(n)?n:void 0,u=n=>"number"==typeof n&&Number.isFinite(n)&&n>0;export function validateQuestionForTransform(r){const h=[];r.type||h.push("Question type is required");const o=s(r.answer),p=s(r.content);switch(r.type){case"FILL_MISSING_WORDS_IN_GRID":!function(n,t,s){Array.isArray(n?.grid)||s.push("Grid is required for FILL_MISSING_WORDS_IN_GRID");u(n?.rows)||s.push("Rows must be at least 1");u(n?.columns)||s.push("Columns must be at least 1");i=t?.answers,e(i)&&Object.keys(i).length>0||s.push("At least one missing letter is required");var i}(p,o,h);break;case"LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER":!function(t,e){n(t?.imageUrl)||e.push("Vui lòng upload hình ảnh");n(t?.question)||e.push("Vui lòng nhập nội dung câu hỏi");i(t?.options,e),(void 0===t?.correctAnswer||-1===t.correctAnswer)&&e.push("Vui lòng chọn đáp án đúng")}(o,h);break;case"LOOK_PICTURE_CHOOSE_CORRECT_ANSWER":!function(t,e){n(t?.imageUrl)||e.push("Vui lòng upload hình ảnh");i(t?.options,e),("number"!=typeof t?.correctAnswer||t.correctAnswer<0)&&e.push("Vui lòng chọn đáp án đúng")}(o,h);break;case"LOOK_PICTURE_FILL_WORD_HINT":!function(e,u){if(!t(e?.subQuestions))return void u.push("Vui lòng nhập dữ liệu câu hỏi");e.subQuestions.forEach(t=>{const e=s(t);n(e?.imageUrl)||u.push("Vui lòng upload hình ảnh"),n(e?.word)||u.push("Vui lòng nhập từ có chỗ trống"),n(e?.correctAnswer)||u.push("Vui lòng nhập đáp án đúng")})}(o,h);break;case"LABEL_THE_PICTURE":!function(e,s){n(e?.imageUrl)||s.push("Vui lòng upload hình ảnh");t(e?.labels)||s.push("Vui lòng thêm ít nhất một vị trí cần điền")}(o,h);break;case"READ_AND_COLOR_OBJECTS":case"IMAGE_OBJECT_MATCHING":!function(e,s){n(e?.backgroundImageUrl)||s.push("Vui lòng upload ảnh nền");t(e?.colorNodes)||s.push("Vui lòng thêm ít nhất một màu");t(e?.regionNodes)||s.push("Vui lòng thêm ít nhất một vùng tô màu")}(o,h);break;case"READ_PASSAGE_AND_ANSWER_QUESTIONS":!function(t,e){n(t?.passage)||e.push("Vui lòng nhập nội dung đoạn văn");const u=s(t?.singleQuestion);if(!u)return void e.push("Vui lòng nhập dữ liệu câu hỏi");n(u.question)||e.push("Vui lòng nhập câu hỏi");i(u.options,e),("number"!=typeof u.correctAnswer||u.correctAnswer<0)&&e.push("Vui lòng chọn đáp án đúng")}(o,h);break;case"CHOOSE_CORRECT_ADJECTIVE":!function(t,e,s){n(t?.sentence)||s.push("Vui lòng nhập câu hỏi");i(t?.options,s),("number"!=typeof e?.answer||e.answer<0)&&s.push("Vui lòng chọn đáp án đúng")}(p,o,h);break;case"ANSWER_THE_QUESTION":!function(t,e){n(t?.question)||e.push("Vui lòng nhập câu hỏi");n(t?.correctAnswer)||e.push("Vui lòng nhập đáp án")}(o,h);break;case"WORD_ORDERING":!function(n,e){(!t(n?.words)||n.words.length<2)&&e.push("Cần ít nhất 2 từ/chữ cái");(!t(n?.answer)||n.answer.length<2)&&e.push("Cần ít nhất 2 từ/chữ cái trong đáp án")}(o,h);break;case"WORD_FILL_PARAGRAPH":!function(e,s){n(e?.paragraph)||s.push("Vui lòng nhập đoạn văn");"DEFAULT"===e?.viewMode||"EMAIL"===e?.viewMode||t(e?.wordBank)||s.push("Cần ít nhất 1 chỗ trống");t(e?.answers)||s.push("Cần ít nhất 1 đáp án")}(o,h);break;case"LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP":!function(e,s){(!t(e?.options)||e.options.length<2)&&s.push("Cần tối thiểu 2 đáp án trong nhóm");n(e?.audioUrl)||s.push("Phải có file audio");n(e?.correctAnswer)||s.push("Phải chọn đáp án đúng")}(o,h);break;case"LISTEN_AND_SPEAK_QUESTION_LIST":!function(e,s){n(e?.audioUrl)||s.push("Vui lòng tải lên file âm thanh");n(e?.activeQuestionId)||s.push("Vui lòng chọn câu hỏi");t(e?.expectedAnswers)||s.push("Cần ít nhất 1 đáp án mong muốn")}(o,h);break;case"WRITE_A_SHORT_LETTER":!function(n,t){u(n?.minWords)||t.push("Số từ tối thiểu phải >= 1");"number"==typeof n?.maxWords&&"number"==typeof n.minWords&&n.maxWords<n.minWords&&t.push("Số từ tối đa phải >= số từ tối thiểu")}(o,h)}return{isValid:0===h.length,errors:h}}function i(e,u){if(!t(e)||e.length<2)return void u.push("Phải có ít nhất 2 lựa chọn");e.filter(t=>{const e="string"==typeof t?t:s(t)?.text;return n(e)}).length<2&&u.push("Phải có ít nhất 2 đáp án được điền")}
|
|
@@ -8,6 +8,8 @@ export interface TransformQuestionOptions {
|
|
|
8
8
|
questionBankCode?: string;
|
|
9
9
|
questionBankTitle?: string;
|
|
10
10
|
sectionId?: number;
|
|
11
|
+
examLevel?: string;
|
|
12
|
+
subjectId?: string;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* Transform frontend question format to API format.
|
|
@@ -22,9 +24,11 @@ export interface TransformQuestionItem {
|
|
|
22
24
|
addToQuestionBank?: boolean;
|
|
23
25
|
questionBankCode?: string;
|
|
24
26
|
questionBankTitle?: string;
|
|
27
|
+
examLevel?: string;
|
|
28
|
+
subjectId?: string;
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
|
-
* Transform multiple questions to API format
|
|
28
|
-
*
|
|
31
|
+
* Transform multiple questions to API format with the same validation gate used
|
|
32
|
+
* by the host frontend screen.
|
|
29
33
|
*/
|
|
30
34
|
export declare function transformQuestionsToApiFormat(questions: TransformQuestionItem[]): TransformQuestionsResult;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{validateQuestionForTransform as o}from"./question-transform-validation";import{transformLookPictureChooseCorrectAnswer as t}from"../../../components/questions/types/look-picture-choose-correct-answer/transform";import{transformLookPictureFillBlankChooseAnswer as s}from"../../../components/questions/types/look-picture-fill-blank-choose-answer/transform";import{transformLookPictureFillWordHint as e}from"../../../components/questions/types/look-picture-fill-word-hint/transform";import{transformReadAndColorObjects as n}from"../../../components/questions/types/read-and-color-objects/transform";import{transformReadPassageAndAnswerQuestions as r}from"../../../components/questions/types/read-passage-and-answer-questions/transform";import{transformArrangeLettersIntoWords as a}from"../../../components/questions/types/arrange-letters-into-words/transform";import{transformWriteShortParagraph as i}from"../../../components/questions/types/write-short-paragraph/transform";import{transformWriteAShortLetter as p}from"../../../components/questions/types/write-a-short-letter/transform";import{transformFillMissingWordsInGrid as m}from"../../../components/questions/types/fill-missing-words-in-grid/transform";import{transformLabelThePicture as u}from"../../../components/questions/types/label-the-picture/transform";import{transformChooseTheCorrectAnswer as c}from"../../../components/questions/types/choose-the-correct-answer/transform";import{transformFillInBlank as _}from"../../../components/questions/types/fill-in-blank/transform";import{transformWriteCorrectVerbForm as E}from"../../../components/questions/types/write-correct-verb-form/transform";import{transformChooseCorrectAdjective as f}from"../../../components/questions/types/choose-correct-adjective/transform";import{transformAnswerTheQuestion as N}from"../../../components/questions/types/answer-the-question/transform";import{transformTrueFalse as A}from"../../../components/questions/types/true-false/transform";import{transformWordOrdering as I}from"../../../components/questions/types/word-ordering/transform";import{transformWordFillParagraph as d}from"../../../components/questions/types/word-fill-paragraph/transform";import{transformMatchByWritingAnswer as T}from"../../../components/questions/types/match-by-writing-answer/transform";import{transformWriteSentences as l}from"../../../components/questions/types/write-sentences/transform";import{transformListenAndChooseFromAnswerGroup as S}from"../../../components/questions/types/listen-and-choose-from-answer-group/transform";import{transformListenAndChooseObjectsInScene as R}from"../../../components/questions/types/listen-and-choose-objects-in-scene/transform";import{transformListenAndDragObjectsIntoScene as q}from"../../../components/questions/types/listen-and-drag-objects-into-scene/transform";import{transformListenAndSpeakAnswer as D}from"../../../components/questions/types/listen-and-speak-answer/transform";import{transformListenAndSpeakImageGroup as O}from"../../../components/questions/types/listen-and-speak-image-group/transform";import{transformListenAndSpeakQuestionList as L}from"../../../components/questions/types/listen-and-speak-question-list/transform";import{transformListenAndSpeakCompareImages as y}from"../../../components/questions/types/listen-and-speak-compare-images/transform";import{transformListenAndSpeakOddOneOut as C}from"../../../components/questions/types/listen-and-speak-odd-one-out/transform";import{transformListenAndSpeakWithStoryImages as G}from"../../../components/questions/types/listen-and-speak-with-story-images/transform";import{transformListenAndSpeakInfoExchange as g}from"../../../components/questions/types/listen-and-speak-info-exchange/transform";import{transformSpeakingDescribeImage as w}from"../../../components/questions/types/speaking-describe-image/transform";import{transformReadDisplayedContent as h}from"../../../components/questions/types/read-displayed-content/transform";import{transformSpontaneousQA as k}from"../../../components/questions/types/spontaneous-qa/transform";import{transformSpeakingConversation as P}from"../../../components/questions/types/speaking-conversation/transform";import{transformSpeakingCueCard as b}from"../../../components/questions/types/speaking-cue-card/transform";import{transformGNSpeakingInterview as B}from"../../../components/questions/types/gn-speaking-interview/transform";import{transformWordFillStructuredForm as W}from"../../../components/questions/types/word-fill-structured-form/transform";const v={LOOK_PICTURE_CHOOSE_CORRECT_ANSWER:t,LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER:s,LOOK_PICTURE_FILL_WORD_HINT:e,READ_AND_COLOR_OBJECTS:n,IMAGE_OBJECT_MATCHING:n,READ_PASSAGE_AND_ANSWER_QUESTIONS:r,ARRANGE_LETTERS_INTO_WORDS:a,WRITE_SHORT_PARAGRAPH:i,WRITE_A_SHORT_LETTER:p,FILL_MISSING_WORDS_IN_GRID:m,LABEL_THE_PICTURE:u,CHOOSE_THE_CORRECT_ANSWER:c,FILL_IN_BLANK:_,WRITE_CORRECT_VERB_FORM:E,CHOOSE_CORRECT_ADJECTIVE:f,ANSWER_THE_QUESTION:N,TRUE_FALSE:A,WORD_ORDERING:I,WORD_FILL_PARAGRAPH:d,MATCH_BY_WRITING_ANSWER:T,WRITE_SENTENCES:l,LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP:S,LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE:R,LISTEN_AND_DRAG_OBJECTS_INTO_SCENE:q,LISTEN_AND_SPEAK_ANSWER:D,LISTEN_AND_SPEAK_IMAGE_GROUP:O,LISTEN_AND_SPEAK_QUESTION_LIST:L,LISTEN_AND_SPEAK_COMPARE_IMAGES:y,LISTEN_AND_SPEAK_ODD_ONE_OUT:C,LISTEN_AND_SPEAK_WITH_STORY_IMAGES:G,LISTEN_AND_SPEAK_INFO_EXCHANGE:g,SPEAKING_DESCRIBE_IMAGE:w,READ_DISPLAYED_CONTENT:h,SPONTANEOUS_QA:k,SPEAKING_CONVERSATION:P,SPEAKING_CUE_CARD:b,GN_SPEAKING_INTERVIEW:B,WORD_FILL_STRUCTURED_FORM:W};export function transformQuestionToApiFormat(o,t={}){const{partId:s,partNo:e,questionNumber:n,addToQuestionBank:r,questionBankCode:a,questionBankTitle:i,sectionId:p,examLevel:m,subjectId:u}=t,{id:c,type:_,points:E,explanation:f}=o,N=_?v[_]:void 0,A=N?N(o):{apiContent:{},apiCorrectAnswer:{}},{apiContent:I,apiCorrectAnswer:d,totalPoints:T,hintLetters:l,groupNumber:S,passageData:R,imageData:q,basicGroupData:D,questionListGroupData:O,sceneData:L,answerGroupData:y,compareImagesGroupData:C,infoExchangeGroupData:G,conversationGroupData:g,isExample:w,suggestedAnswers:h}=A,k=o.answer,P=o.content,b={id:c,questionType:_||"",content:I,correctAnswer:d||{},points:T??E??1,explanation:A.explanation||f||k?.explanation||P?.explanation||"",questionNumber:n,partId:s,partNo:e,...null!=p?{sectionId:p}:{},questionSkillType:o.skill,examLevel:m,subjectId:u,addToQuestionBank:r??!1,questionBank:r&&a&&i?{code:a,title:i}:void 0,isExample:w||!1};h&&h.length>0&&(b.suggestedAnswers=h);const B=function(o){const{hintLetters:t,passageData:s,imageData:e,basicGroupData:n,sceneData:r,answerGroupData:a,questionListGroupData:i,compareImagesGroupData:p,infoExchangeGroupData:m,conversationGroupData:u}=o;if(t&&t.length>0)return{hintLetters:t};if(s)return s;if(e)return e;if(n&&(n.title||n.subtitle||n.content||n.imageUrl||n.articles))return n;return r||(a||(i||(p||(m||(u||void 0)))))}({hintLetters:l,passageData:R,imageData:q,basicGroupData:D,sceneData:L,answerGroupData:y,questionListGroupData:O,compareImagesGroupData:C,infoExchangeGroupData:G,conversationGroupData:g});return B&&(b.isGroup=!0,b.questionGroup={payload:B,groupNumber:S||1}),b}export function transformQuestionsToApiFormat(t){const s=[],e=[];return t.forEach((t,n)=>{const{question:r,partId:a,partNo:i,sectionId:p,addToQuestionBank:m,questionBankCode:u,questionBankTitle:c,examLevel:_,subjectId:E}=t,f=o(r);if(!f.isValid)return void e.push({index:n,errors:f.errors});const N=r.questionNumber??n+1;s.push(transformQuestionToApiFormat(r,{partId:a,partNo:i,questionNumber:N,addToQuestionBank:m,questionBankCode:u,questionBankTitle:c,sectionId:p,examLevel:_,subjectId:E}))}),{validQuestions:s,invalidQuestions:e}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinyweb_dev/oe-exam-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Reusable OceanEdu question components.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,11 +18,18 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"clean": "rm -rf dist",
|
|
21
|
-
"build:
|
|
21
|
+
"build:compile": "tsc -p tsconfig.build.json",
|
|
22
|
+
"build:alias": "tsc-alias -p tsconfig.build.json",
|
|
23
|
+
"build:ts": "npm run build:compile && npm run build:assets && npm run build:alias",
|
|
22
24
|
"build:assets": "for d in cambridge-yle english-certification summer-sky; do [ -d src/components/themes/$d/assets ] && cp -R src/components/themes/$d/assets dist/components/themes/$d/ || true; done && cp src/global.d.ts dist/global.d.ts",
|
|
23
25
|
"minify:js": "find dist -name '*.js' -type f -exec sh -c 'for f do terser \"$f\" --compress directives=false --mangle --module --comments false --output \"$f\"; done' sh {} +",
|
|
24
|
-
"build": "npm run clean && npm run build:ts && npm run
|
|
26
|
+
"build": "npm run clean && npm run build:ts && npm run minify:js",
|
|
25
27
|
"pack:local": "npm pack",
|
|
28
|
+
"release": "npm run release:patch",
|
|
29
|
+
"release:patch": "npm version patch --no-git-tag-version && npm run release:push",
|
|
30
|
+
"release:minor": "npm version minor --no-git-tag-version && npm run release:push",
|
|
31
|
+
"release:major": "npm version major --no-git-tag-version && npm run release:push",
|
|
32
|
+
"release:push": "npm run build && npm pack --dry-run && VERSION=$(node -p \"require('./package.json').version\") && git add package.json package-lock.json && git commit -m \"chore(sdk): release v$VERSION\" && git tag \"sdk-v$VERSION\" && git push origin HEAD && git push origin \"sdk-v$VERSION\"",
|
|
26
33
|
"prepack": "npm run build"
|
|
27
34
|
},
|
|
28
35
|
"exports": {
|