@burdenoff/microfe-buildmyiq 2026.709.1 → 2026.710.1
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/buildmyiq/BuildmyiqRoot.d.ts +5 -0
- package/dist/buildmyiq/BuildmyiqRoot.d.ts.map +1 -0
- package/dist/buildmyiq/BuildmyiqRoutes.d.ts +4 -0
- package/dist/buildmyiq/BuildmyiqRoutes.d.ts.map +1 -0
- package/dist/buildmyiq/components/Avatar.d.ts +15 -0
- package/dist/buildmyiq/components/Avatar.d.ts.map +1 -0
- package/dist/buildmyiq/components/Avatar.js.map +1 -1
- package/dist/buildmyiq/components/BuiDiscussion.d.ts +18 -0
- package/dist/buildmyiq/components/BuiDiscussion.d.ts.map +1 -0
- package/dist/buildmyiq/components/BuiTags.d.ts +17 -0
- package/dist/buildmyiq/components/BuiTags.d.ts.map +1 -0
- package/dist/buildmyiq/components/DataTable.d.ts +28 -0
- package/dist/buildmyiq/components/DataTable.d.ts.map +1 -0
- package/dist/buildmyiq/components/EmptyState.d.ts +18 -0
- package/dist/buildmyiq/components/EmptyState.d.ts.map +1 -0
- package/dist/buildmyiq/components/EvidenceUpload.d.ts +14 -0
- package/dist/buildmyiq/components/EvidenceUpload.d.ts.map +1 -0
- package/dist/buildmyiq/components/FileLink.d.ts +15 -0
- package/dist/buildmyiq/components/FileLink.d.ts.map +1 -0
- package/dist/buildmyiq/components/FilterTabs.d.ts +18 -0
- package/dist/buildmyiq/components/FilterTabs.d.ts.map +1 -0
- package/dist/buildmyiq/components/MetricsCard.d.ts +23 -0
- package/dist/buildmyiq/components/MetricsCard.d.ts.map +1 -0
- package/dist/buildmyiq/components/MiniBar.d.ts +16 -0
- package/dist/buildmyiq/components/MiniBar.d.ts.map +1 -0
- package/dist/buildmyiq/components/PageLayout.d.ts +16 -0
- package/dist/buildmyiq/components/PageLayout.d.ts.map +1 -0
- package/dist/buildmyiq/components/Pagination.d.ts +22 -0
- package/dist/buildmyiq/components/Pagination.d.ts.map +1 -0
- package/dist/buildmyiq/components/StateViews.d.ts +24 -0
- package/dist/buildmyiq/components/StateViews.d.ts.map +1 -0
- package/dist/buildmyiq/components/StatusBadge.d.ts +9 -0
- package/dist/buildmyiq/components/StatusBadge.d.ts.map +1 -0
- package/dist/buildmyiq/components/Tabs.d.ts +17 -0
- package/dist/buildmyiq/components/Tabs.d.ts.map +1 -0
- package/dist/buildmyiq/components/Viz.d.ts +25 -0
- package/dist/buildmyiq/components/Viz.d.ts.map +1 -0
- package/dist/buildmyiq/components/Viz.js.map +1 -1
- package/dist/buildmyiq/components/index.d.ts +21 -0
- package/dist/buildmyiq/components/index.d.ts.map +1 -0
- package/dist/buildmyiq/context/BuildmyiqContext.d.ts +16 -0
- package/dist/buildmyiq/context/BuildmyiqContext.d.ts.map +1 -0
- package/dist/buildmyiq/context/index.d.ts +6 -0
- package/dist/buildmyiq/context/index.d.ts.map +1 -0
- package/dist/buildmyiq/hooks/index.d.ts +10 -0
- package/dist/buildmyiq/hooks/index.d.ts.map +1 -0
- package/dist/buildmyiq/hooks/useBuiComments.d.ts +36 -0
- package/dist/buildmyiq/hooks/useBuiComments.d.ts.map +1 -0
- package/dist/buildmyiq/hooks/useBuiFiles.d.ts +47 -0
- package/dist/buildmyiq/hooks/useBuiFiles.d.ts.map +1 -0
- package/dist/buildmyiq/hooks/useBuiTags.d.ts +44 -0
- package/dist/buildmyiq/hooks/useBuiTags.d.ts.map +1 -0
- package/dist/buildmyiq/hooks/useBuildmyiqApi.d.ts +65 -0
- package/dist/buildmyiq/hooks/useBuildmyiqApi.d.ts.map +1 -0
- package/dist/buildmyiq/hooks/useCursorPager.d.ts +33 -0
- package/dist/buildmyiq/hooks/useCursorPager.d.ts.map +1 -0
- package/dist/buildmyiq/index.d.ts +9 -0
- package/dist/buildmyiq/index.d.ts.map +1 -0
- package/dist/buildmyiq/pages/DashboardPage.d.ts +4 -0
- package/dist/buildmyiq/pages/DashboardPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/_shared/CourseCard.d.ts +13 -0
- package/dist/buildmyiq/pages/_shared/CourseCard.d.ts.map +1 -0
- package/dist/buildmyiq/pages/_shared/index.d.ts +6 -0
- package/dist/buildmyiq/pages/_shared/index.d.ts.map +1 -0
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.d.ts +4 -0
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.d.ts +4 -0
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.d.ts +4 -0
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts +4 -0
- package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts +4 -0
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/_forms.d.ts +44 -0
- package/dist/buildmyiq/pages/authoring/_forms.d.ts.map +1 -0
- package/dist/buildmyiq/pages/catalog/CatalogPage.d.ts +4 -0
- package/dist/buildmyiq/pages/catalog/CatalogPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts +4 -0
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts +4 -0
- package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
- package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts +4 -0
- package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/insights/InsightsPage.d.ts +4 -0
- package/dist/buildmyiq/pages/insights/InsightsPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -1
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.d.ts +4 -0
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/learning/MyLearningPage.d.ts +4 -0
- package/dist/buildmyiq/pages/learning/MyLearningPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
- package/dist/buildmyiq/pages/paths/PathsPage.d.ts +4 -0
- package/dist/buildmyiq/pages/paths/PathsPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/paths/PathsPage.js.map +1 -1
- package/dist/buildmyiq/pages/skills/SkillsPage.d.ts +4 -0
- package/dist/buildmyiq/pages/skills/SkillsPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/skills/SkillsPage.js.map +1 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts +4 -0
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +4 -8
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
- package/dist/buildmyiq/shared/hooks/useTr.d.ts +9 -0
- package/dist/buildmyiq/shared/hooks/useTr.d.ts.map +1 -0
- package/dist/buildmyiq/types/index.d.ts +382 -0
- package/dist/buildmyiq/types/index.d.ts.map +1 -0
- package/dist/buildmyiq/utils/cn.d.ts +4 -0
- package/dist/buildmyiq/utils/cn.d.ts.map +1 -0
- package/dist/buildmyiq/utils/cn.js +2 -2
- package/dist/buildmyiq/utils/formatters.d.ts +33 -0
- package/dist/buildmyiq/utils/formatters.d.ts.map +1 -0
- package/dist/buildmyiq/utils/i18n.d.ts +6 -0
- package/dist/buildmyiq/utils/i18n.d.ts.map +1 -0
- package/dist/buildmyiq/utils/index.d.ts +9 -0
- package/dist/buildmyiq/utils/index.d.ts.map +1 -0
- package/dist/buildmyiq/utils/navigation.d.ts +14 -0
- package/dist/buildmyiq/utils/navigation.d.ts.map +1 -0
- package/dist/dev/main.d.ts +2 -0
- package/dist/dev/main.d.ts.map +1 -0
- package/dist/generated/global-operations.d.ts +2 -0
- package/dist/generated/global-operations.d.ts.map +1 -0
- package/dist/generated/global-types.d.ts +18199 -0
- package/dist/generated/global-types.d.ts.map +1 -0
- package/dist/generated/wspace-operations.d.ts +2 -0
- package/dist/generated/wspace-operations.d.ts.map +1 -0
- package/dist/generated/wspace-types.d.ts +43046 -0
- package/dist/generated/wspace-types.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -1
- package/dist/node_modules/{clsx → .bun/clsx@2.1.1/node_modules/clsx}/dist/clsx.js +1 -1
- package/dist/node_modules/.bun/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
- package/dist/node_modules/{tailwind-merge → .bun/tailwind-merge@3.5.0/node_modules/tailwind-merge}/dist/bundle-mjs.js +170 -212
- package/dist/node_modules/.bun/tailwind-merge@3.5.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
- package/dist/services/assistantApi.d.ts +76 -0
- package/dist/services/assistantApi.d.ts.map +1 -0
- package/dist/services/assistantApi.js +272 -0
- package/dist/services/assistantApi.js.map +1 -0
- package/dist/services/createSandboxAssistantTransport.d.ts +40 -0
- package/dist/services/createSandboxAssistantTransport.d.ts.map +1 -0
- package/dist/services/createSandboxAssistantTransport.js +120 -0
- package/dist/services/createSandboxAssistantTransport.js.map +1 -0
- package/dist/types/assistant.d.ts +15 -0
- package/dist/types/assistant.d.ts.map +1 -0
- package/dist/utils/pageContext.d.ts +17 -0
- package/dist/utils/pageContext.d.ts.map +1 -0
- package/dist/utils/pageContext.js +49 -0
- package/dist/utils/pageContext.js.map +1 -0
- package/package.json +1 -1
- package/dist/node_modules/clsx/dist/clsx.js.map +0 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBuiFiles.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiFiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA8BH,8EAA8E;AAC9E,eAAO,MAAM,WAAW,GAAU,KAAK,EACrC,OAAO,MAAM,EACb,WAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,UAAU,MAAM,KACf,OAAO,CAAC,KAAK,CAmBf,CAAC;AAMF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AA+BD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACjD,WAAW,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAO,sBAoFnC,CAAC;AAMF,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,UAAU,GAAI,IAAI,MAAM,GAAG,IAAI,GAAG,SAAS,8EAsBvD,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buildmyiq — wspace-tags-svc UniversalTag REUSE hooks (Integration C).
|
|
3
|
+
* @module buildmyiq/hooks/useBuiTags
|
|
4
|
+
*
|
|
5
|
+
* The buildmyiq svc does NOT expose a tags field, so we drive the platform tag
|
|
6
|
+
* assignment store directly (approach (b) in the reuse audit) — no buildmyiq-svc
|
|
7
|
+
* change. All operations are composed into the WORKSPACE supergraph:
|
|
8
|
+
* - `getEntityTags(entityType, entityId)` → current assignments (display)
|
|
9
|
+
* - `searchTags(query)` → pick an existing tag
|
|
10
|
+
* - `createTag(input)` → create a new tag (needs a vocabulary)
|
|
11
|
+
* - `assignTag(input)` → link tag ↔ entity
|
|
12
|
+
* - `removeTagAssignment(id)` → unlink
|
|
13
|
+
*
|
|
14
|
+
* `createTag` requires a `vocabularyId`; we resolve a stable, workspace-scoped
|
|
15
|
+
* "buildmyiq" vocabulary (`getVocabularyByKey`, creating it on first use).
|
|
16
|
+
*
|
|
17
|
+
* Transport reuses `wspaceFetch` from `useBuiFiles`. Lives in a separate file;
|
|
18
|
+
* `useBuildmyiqApi.ts` is untouched.
|
|
19
|
+
*/
|
|
20
|
+
export interface BuiTag {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
color: string | null;
|
|
24
|
+
key: string;
|
|
25
|
+
}
|
|
26
|
+
export interface BuiTagAssignment {
|
|
27
|
+
id: string;
|
|
28
|
+
tag: BuiTag;
|
|
29
|
+
}
|
|
30
|
+
/** Current tag assignments for an Buildmyiq entity. */
|
|
31
|
+
export declare const useEcoEntityTags: (entityType: string, entityId: string | undefined) => import('@tanstack/react-query').UseQueryResult<BuiTagAssignment[], Error>;
|
|
32
|
+
/** Search existing tags (enabled only for a non-empty query). */
|
|
33
|
+
export declare const useEcoTagSearch: (query: string) => import('@tanstack/react-query').UseQueryResult<BuiTag[], Error>;
|
|
34
|
+
/** Assign an existing tag to the entity. */
|
|
35
|
+
export declare const useEcoAssignTag: (entityType: string, entityId: string) => import('@tanstack/react-query').UseMutationResult<{
|
|
36
|
+
id: string;
|
|
37
|
+
}, Error, string, unknown>;
|
|
38
|
+
/** Remove a tag assignment by its assignment id. */
|
|
39
|
+
export declare const useEcoRemoveTagAssignment: (entityType: string, entityId: string) => import('@tanstack/react-query').UseMutationResult<boolean, Error, string, unknown>;
|
|
40
|
+
/** Create a new tag from a free-text label and assign it to the entity. */
|
|
41
|
+
export declare const useEcoCreateAndAssignTag: (entityType: string, entityId: string) => import('@tanstack/react-query').UseMutationResult<{
|
|
42
|
+
id: string;
|
|
43
|
+
}, Error, string, unknown>;
|
|
44
|
+
//# sourceMappingURL=useBuiTags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBuiTags.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiTags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAOH,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;CACb;AAiBD,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,GAAG,SAAS,8EAmBhF,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,oEAiB5C,CAAC;AAEF,4CAA4C;AAC5C,eAAO,MAAM,eAAe,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM;QAKZ,MAAM;0BAW7D,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,yBAAyB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,uFAgB7E,CAAC;AA+BF,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM;QAalB,MAAM;0BAkBhE,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BuiConnection, BuiCourse, BuiCourseLevel, BuiCourseStatus, BuiModule, BuiLesson, BuiAssessment, BuiAssessmentStatus, BuiQuestion, BuiSubmission, BuiSubmissionStatus, BuiEnrollment, BuiEnrollmentStatus, BuiCohort, BuiCohortStatus, BuiSkill, BuiSkillCategory, BuiCertificate, BuiLessonProgress, BuiCreateCourseInput, BuiUpdateCourseInput, BuiCreateModuleInput, BuiCreateLessonInput, BuiCreateAssessmentInput, BuiCreateQuestionInput, BuiCreateSkillInput, BuiCreateCohortInput, BuiEnrollInput, BuiRecordLessonProgressInput, BuiSubmitAssessmentInput, BuiGradeSubmissionInput, BuiIssueCertificateInput } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface UseBuiCoursesArgs {
|
|
4
|
+
level?: BuiCourseLevel;
|
|
5
|
+
status?: BuiCourseStatus;
|
|
6
|
+
cursor?: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const useBuiCourses: (args?: UseBuiCoursesArgs) => import('@tanstack/react-query').UseQueryResult<BuiConnection<BuiCourse>, Error>;
|
|
11
|
+
export declare const useBuiCourse: (id: string | undefined) => import('@tanstack/react-query').UseQueryResult<BuiCourse | null, Error>;
|
|
12
|
+
export declare const useBuiCreateCourse: () => import('@tanstack/react-query').UseMutationResult<BuiCourse, Error, BuiCreateCourseInput, unknown>;
|
|
13
|
+
export declare const useBuiUpdateCourse: () => import('@tanstack/react-query').UseMutationResult<BuiCourse, Error, {
|
|
14
|
+
id: string;
|
|
15
|
+
input: BuiUpdateCourseInput;
|
|
16
|
+
}, unknown>;
|
|
17
|
+
export declare const useBuiPublishCourse: () => import('@tanstack/react-query').UseMutationResult<BuiCourse, Error, string, unknown>;
|
|
18
|
+
export declare const useBuiArchiveCourse: () => import('@tanstack/react-query').UseMutationResult<boolean, Error, string, unknown>;
|
|
19
|
+
export declare const useBuiModules: (courseId: string | undefined) => import('@tanstack/react-query').UseQueryResult<BuiModule[], Error>;
|
|
20
|
+
export declare const useBuiLessons: (courseId: string | undefined, moduleId?: string) => import('@tanstack/react-query').UseQueryResult<BuiLesson[], Error>;
|
|
21
|
+
export declare const useBuiCreateModule: () => import('@tanstack/react-query').UseMutationResult<BuiModule, Error, BuiCreateModuleInput, unknown>;
|
|
22
|
+
export declare const useBuiCreateLesson: () => import('@tanstack/react-query').UseMutationResult<BuiLesson, Error, BuiCreateLessonInput, unknown>;
|
|
23
|
+
export declare const useBuiAssessments: (courseId: string | undefined, status?: BuiAssessmentStatus) => import('@tanstack/react-query').UseQueryResult<BuiAssessment[], Error>;
|
|
24
|
+
export declare const useBuiAssessment: (id: string | undefined) => import('@tanstack/react-query').UseQueryResult<BuiAssessment | null, Error>;
|
|
25
|
+
export declare const useBuiCreateAssessment: () => import('@tanstack/react-query').UseMutationResult<BuiAssessment, Error, BuiCreateAssessmentInput, unknown>;
|
|
26
|
+
export declare const useBuiPublishAssessment: () => import('@tanstack/react-query').UseMutationResult<BuiAssessment, Error, string, unknown>;
|
|
27
|
+
export declare const useBuiCreateQuestion: () => import('@tanstack/react-query').UseMutationResult<BuiQuestion, Error, BuiCreateQuestionInput, unknown>;
|
|
28
|
+
export interface UseBuiSubmissionsArgs {
|
|
29
|
+
assessmentId?: string;
|
|
30
|
+
learnerId?: string;
|
|
31
|
+
status?: BuiSubmissionStatus;
|
|
32
|
+
cursor?: string;
|
|
33
|
+
limit?: number;
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare const useBuiSubmissions: (args?: UseBuiSubmissionsArgs) => import('@tanstack/react-query').UseQueryResult<BuiConnection<BuiSubmission>, Error>;
|
|
37
|
+
export declare const useBuiSubmitAssessment: () => import('@tanstack/react-query').UseMutationResult<BuiSubmission, Error, BuiSubmitAssessmentInput, unknown>;
|
|
38
|
+
export declare const useBuiGradeSubmission: () => import('@tanstack/react-query').UseMutationResult<BuiSubmission, Error, {
|
|
39
|
+
id: string;
|
|
40
|
+
input: BuiGradeSubmissionInput;
|
|
41
|
+
}, unknown>;
|
|
42
|
+
export interface UseBuiEnrollmentsArgs {
|
|
43
|
+
courseId?: string;
|
|
44
|
+
learnerId?: string;
|
|
45
|
+
status?: BuiEnrollmentStatus;
|
|
46
|
+
cursor?: string;
|
|
47
|
+
limit?: number;
|
|
48
|
+
enabled?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare const useBuiEnrollments: (args?: UseBuiEnrollmentsArgs) => import('@tanstack/react-query').UseQueryResult<BuiConnection<BuiEnrollment>, Error>;
|
|
51
|
+
export declare const useBuiEnrollment: (id: string | undefined) => import('@tanstack/react-query').UseQueryResult<BuiEnrollment | null, Error>;
|
|
52
|
+
export declare const useBuiEnroll: () => import('@tanstack/react-query').UseMutationResult<BuiEnrollment, Error, BuiEnrollInput, unknown>;
|
|
53
|
+
export declare const useBuiTransitionEnrollment: () => import('@tanstack/react-query').UseMutationResult<BuiEnrollment, Error, {
|
|
54
|
+
id: string;
|
|
55
|
+
status: BuiEnrollmentStatus;
|
|
56
|
+
}, unknown>;
|
|
57
|
+
export declare const useBuiRecordLessonProgress: () => import('@tanstack/react-query').UseMutationResult<BuiLessonProgress, Error, BuiRecordLessonProgressInput, unknown>;
|
|
58
|
+
export declare const useBuiCohorts: (courseId?: string, status?: BuiCohortStatus) => import('@tanstack/react-query').UseQueryResult<BuiCohort[], Error>;
|
|
59
|
+
export declare const useBuiCreateCohort: () => import('@tanstack/react-query').UseMutationResult<BuiCohort, Error, BuiCreateCohortInput, unknown>;
|
|
60
|
+
export declare const useBuiSkills: (category?: BuiSkillCategory) => import('@tanstack/react-query').UseQueryResult<BuiSkill[], Error>;
|
|
61
|
+
export declare const useBuiCreateSkill: () => import('@tanstack/react-query').UseMutationResult<BuiSkill, Error, BuiCreateSkillInput, unknown>;
|
|
62
|
+
export declare const useBuiCertificates: (courseId?: string, learnerId?: string) => import('@tanstack/react-query').UseQueryResult<BuiCertificate[], Error>;
|
|
63
|
+
export declare const useBuiIssueCertificate: () => import('@tanstack/react-query').UseMutationResult<BuiCertificate, Error, BuiIssueCertificateInput, unknown>;
|
|
64
|
+
export declare const useBuiRevokeCertificate: () => import('@tanstack/react-query').UseMutationResult<BuiCertificate, Error, string, unknown>;
|
|
65
|
+
//# sourceMappingURL=useBuildmyiqApi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBuildmyiqApi.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuildmyiqApi.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCursorPager — forward/back cursor pagination for Bui* list connections.
|
|
3
|
+
* @module buildmyiq/hooks
|
|
4
|
+
*
|
|
5
|
+
* The svc paginates by cursor only (`PaginationInput.offset` is a no-op — the
|
|
6
|
+
* resolver never sets Prisma `skip` from it). So a page must send
|
|
7
|
+
* `pagination.cursor = the previous page's pageInfo.cursor` to advance. This
|
|
8
|
+
* hook keeps a stack of the cursors that opened each visited page so Prev/Next
|
|
9
|
+
* are exact, and exposes a 0-indexed `page` for display.
|
|
10
|
+
*/
|
|
11
|
+
export interface CursorPager {
|
|
12
|
+
/** 0-indexed page number for display. */
|
|
13
|
+
page: number;
|
|
14
|
+
/**
|
|
15
|
+
* Cursor that opens the current page — `undefined` on the first page so it is
|
|
16
|
+
* OMITTED from the GraphQL variables (the svc rejects an explicit `cursor: null`;
|
|
17
|
+
* a first page must send no cursor at all, per cursor-pagination convention).
|
|
18
|
+
*/
|
|
19
|
+
cursor: string | undefined;
|
|
20
|
+
/** Advance to the next page using the current page's end cursor. No-op if null. */
|
|
21
|
+
next: (endCursor: string | null | undefined) => void;
|
|
22
|
+
/** Go back one page. No-op on the first page. */
|
|
23
|
+
prev: () => void;
|
|
24
|
+
/** Reset to the first page (call when filters change). */
|
|
25
|
+
reset: () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Adapter for `<Pagination onPageChange>` — compares the requested page to the
|
|
28
|
+
* current one and calls next()/prev() accordingly.
|
|
29
|
+
*/
|
|
30
|
+
onPageChange: (targetPage: number, endCursor: string | null | undefined) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare function useCursorPager(): CursorPager;
|
|
33
|
+
//# sourceMappingURL=useCursorPager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCursorPager.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useCursorPager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,mFAAmF;IACnF,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,IAAI,CAAC;IACrD,iDAAiD;IACjD,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;OAGG;IACH,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,IAAI,CAAC;CAClF;AAED,wBAAgB,cAAc,IAAI,WAAW,CA4B5C"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buildmyiq Module Exports
|
|
3
|
+
* @module buildmyiq
|
|
4
|
+
*/
|
|
5
|
+
export { BuildmyiqRoot, BuildmyiqRoot as BuildMyIQRoot } from './BuildmyiqRoot';
|
|
6
|
+
export type { BuildmyiqRootProps, NavItem, FooterItem, TrendDirection } from './types';
|
|
7
|
+
export { BuildmyiqProvider, useBuildmyiq } from './context';
|
|
8
|
+
export { cn, joinPath, formatDate, formatDateTime, formatRelativeTime, formatNumber, formatQuantity, formatPercent, formatDuration, formatHours, formatEnumLabel, formatPersonName, } from './utils';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/buildmyiq/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAE,aAAa,EAAE,aAAa,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhF,YAAY,EAAE,kBAAkB,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGvF,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG5D,OAAO,EACL,EAAE,EACF,QAAQ,EACR,UAAU,EACV,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,cAAc,EACd,WAAW,EACX,eAAe,EACf,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAyB,MAAM,OAAO,CAAC;AA0DvD,eAAO,MAAM,aAAa,EAAE,EAgW3B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { BuiCourse } from '../../types';
|
|
3
|
+
|
|
4
|
+
interface CourseCardProps {
|
|
5
|
+
course: BuiCourse;
|
|
6
|
+
onClick?: () => void;
|
|
7
|
+
/** Show the workflow status pill (authoring view) in addition to level. */
|
|
8
|
+
showStatus?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const CourseCard: FC<CourseCardProps>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=CourseCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CourseCard.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/_shared/CourseCard.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAMhC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,UAAU,eAAe;IACvB,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CAuD1C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/_shared/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssessmentDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/AssessmentDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAC;AAgHnE,eAAO,MAAM,oBAAoB,EAAE,EAyblC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssessmentDetailPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentDetailPage.tsx"],"sourcesContent":["/**\n * AssessmentDetailPage — a single assessment + its question builder.\n * @module buildmyiq/pages/assessments\n *\n * Reads `buiAssessment` (which includes `questions[]`). Educators can publish a\n * DRAFT assessment (`useBuiPublishAssessment`) and add questions\n * (`useBuiCreateQuestion`). The question builder writes an `options` JSON array\n * of `{ label }` for choice questions and an optional input-only `answerKey`\n * (correct label(s)) — the latter is never read back (not exposed on the type).\n */\n\nimport { useMemo, useState, type FC, type ReactNode } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Plus,\n Send,\n ClipboardCheck,\n ClipboardEdit,\n Circle,\n Square,\n Clock,\n Trophy,\n Repeat,\n Trash2,\n Lightbulb,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Checkbox,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n Section,\n EmptyState,\n LoadingView,\n ErrorView,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiPublishAssessment, useBuiCreateQuestion } from '../../hooks';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport { QUESTION_TYPES } from '../../types';\nimport type { BuiQuestion, BuiQuestionType } from '../../types';\n\n// ---------------------------------------------------------------------------\n// Safe narrowing for the untyped `options` JSON blob (array of { id?, label }).\n// ---------------------------------------------------------------------------\ninterface QuestionOption {\n id?: string;\n label: string;\n}\n\nconst parseOptions = (raw: unknown): QuestionOption[] => {\n if (!Array.isArray(raw)) return [];\n const out: QuestionOption[] = [];\n for (const item of raw) {\n if (item && typeof item === 'object' && 'label' in item) {\n const label = (item as { label: unknown }).label;\n if (typeof label === 'string') {\n const idVal = (item as { id?: unknown }).id;\n out.push({ label, id: typeof idVal === 'string' ? idVal : undefined });\n }\n }\n }\n return out;\n};\n\nconst PROMPT_MAX = 1000;\n\nconst CHOICE_TYPES = new Set<BuiQuestionType>(['SINGLE_CHOICE', 'MULTIPLE_CHOICE']);\nconst isExclusive = (type: BuiQuestionType): boolean =>\n type === 'SINGLE_CHOICE' || type === 'TRUE_FALSE';\n\ninterface OptionDraft {\n label: string;\n correct: boolean;\n}\n\ninterface QuestionFormState {\n prompt: string;\n type: BuiQuestionType;\n points: string;\n explanation: string;\n options: OptionDraft[];\n shortAnswer: string;\n}\n\nconst blankChoiceOptions = (): OptionDraft[] => [\n { label: '', correct: false },\n { label: '', correct: false },\n];\n\nconst trueFalseOptions = (): OptionDraft[] => [\n { label: 'True', correct: false },\n { label: 'False', correct: false },\n];\n\nconst makeEmptyForm = (): QuestionFormState => ({\n prompt: '',\n type: 'SINGLE_CHOICE',\n points: '1',\n explanation: '',\n options: blankChoiceOptions(),\n shortAnswer: '',\n});\n\nexport const AssessmentDetailPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const publishAssessment = useBuiPublishAssessment();\n const createQuestion = useBuiCreateQuestion();\n\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<QuestionFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const assessment = assessmentQuery.data ?? null;\n const questions = useMemo(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const nextPosition = useMemo(\n () => (questions.length ? Math.max(...questions.map((q) => q.position)) + 1 : 1),\n [questions]\n );\n\n const totalPoints = useMemo(\n () => questions.reduce((sum, q) => sum + q.points, 0),\n [questions]\n );\n\n const usesOptions = form.type === 'TRUE_FALSE' || CHOICE_TYPES.has(form.type);\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const changeType = (type: BuiQuestionType) => {\n setForm((prev) => {\n let options = prev.options;\n if (type === 'TRUE_FALSE') options = trueFalseOptions();\n else if (CHOICE_TYPES.has(type)) {\n options = prev.options.some((o) => o.label === 'True' || o.label === 'False')\n ? blankChoiceOptions()\n : prev.options;\n }\n return { ...prev, type, options };\n });\n };\n\n const setOptionLabel = (index: number, label: string) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) => (i === index ? { ...o, label } : o)),\n }));\n };\n\n const toggleCorrect = (index: number, value: boolean) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) =>\n i === index\n ? { ...o, correct: value }\n : isExclusive(prev.type)\n ? { ...o, correct: false }\n : o\n ),\n }));\n };\n\n const addOption = () => {\n setForm((prev) => ({ ...prev, options: [...prev.options, { label: '', correct: false }] }));\n };\n\n const removeOption = (index: number) => {\n setForm((prev) => ({ ...prev, options: prev.options.filter((_, i) => i !== index) }));\n };\n\n const handlePublish = async () => {\n if (!assessment) return;\n try {\n await publishAssessment.mutateAsync(assessment.id);\n } catch {\n // The mutation surfaces its own error state; nothing else to do here.\n }\n };\n\n const handleAddQuestion = async () => {\n if (!assessmentId) return;\n if (!form.prompt.trim()) {\n setFormError('Question prompt is required.');\n return;\n }\n\n let options: { label: string }[] | undefined;\n let answerKey: string[] | string | undefined;\n\n if (usesOptions) {\n const cleaned = form.options\n .map((o) => ({ label: o.label.trim(), correct: o.correct }))\n .filter((o) => o.label.length > 0);\n if (CHOICE_TYPES.has(form.type) && cleaned.length < 2) {\n setFormError('Add at least two options.');\n return;\n }\n options = cleaned.map((o) => ({ label: o.label }));\n const correct = cleaned.filter((o) => o.correct).map((o) => o.label);\n if (correct.length) answerKey = correct;\n } else if (form.type === 'SHORT_ANSWER') {\n answerKey = form.shortAnswer.trim() || undefined;\n }\n\n try {\n await createQuestion.mutateAsync({\n assessmentId,\n prompt: form.prompt.trim(),\n type: form.type,\n points: form.points ? Number(form.points) : undefined,\n position: nextPosition,\n explanation: form.explanation.trim() || undefined,\n options,\n answerKey,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to add question.');\n }\n };\n\n // ----- Loading / error / not-found -----\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError) {\n return (\n <PageLayout title=\"Assessment\">\n <ErrorView\n message=\"Couldn't load this assessment.\"\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!assessment) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Assessment not found\"\n description=\"This assessment may have been removed or you don't have access to it.\"\n action={<Button onClick={() => navigateTo('assessments')}>Back to assessments</Button>}\n />\n </PageLayout>\n );\n }\n\n const isDraft = assessment.status === 'DRAFT';\n\n return (\n <PageLayout\n title={\n <span className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <ClipboardEdit className=\"h-5 w-5\" />\n </span>\n <span className=\"truncate\">{assessment.title}</span>\n </span>\n }\n description={assessment.description ?? undefined}\n actions={\n <>\n <Button variant=\"ghost\" onClick={() => navigateTo('submissions')}>\n <ClipboardCheck className=\"mr-1.5 h-4 w-4\" /> Submissions\n </Button>\n {isDraft ? (\n <Button onClick={handlePublish} disabled={publishAssessment.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {publishAssessment.isPending ? 'Publishing…' : 'Publish'}\n </Button>\n ) : (\n <StatusBadge status={assessment.status} className=\"px-3 py-1.5\" />\n )}\n </>\n }\n >\n <button\n type=\"button\"\n onClick={() => navigateTo('assessments')}\n className=\"mb-4 inline-flex items-center gap-1.5 text-sm text-text-secondary transition-colors hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> All assessments\n </button>\n\n {/* Meta */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-5 sm:p-6\">\n <div className=\"mb-5 flex flex-wrap items-center gap-2\">\n <StatusBadge status={assessment.type} />\n <StatusBadge status={assessment.status} />\n {assessment.isGraded ? (\n <StatusBadge status=\"GRADED\" />\n ) : (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n Not graded\n </span>\n )}\n </div>\n <div className=\"grid grid-cols-2 gap-y-6 sm:grid-cols-4\">\n <MetaTile\n icon={<Trophy className=\"h-5 w-5\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n <MetaTile\n icon={<Clock className=\"h-5 w-5\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n divider\n />\n <MetaTile\n icon={<Repeat className=\"h-5 w-5\" />}\n label=\"Max attempts\"\n value={assessment.maxAttempts}\n divider\n />\n <MetaTile\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n label=\"Graded\"\n value={assessment.isGraded ? 'Yes' : 'No'}\n divider\n />\n </div>\n </GlassCard>\n\n {/* Questions */}\n <Section\n title={`Questions (${questions.length})`}\n description={\n questions.length > 0\n ? `${totalPoints} point${totalPoints === 1 ? '' : 's'} across ${questions.length} question${\n questions.length === 1 ? '' : 's'\n }`\n : undefined\n }\n action={\n <Button variant=\"outline\" onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n >\n {questions.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No questions yet\"\n description=\"Build this assessment by adding its first question.\"\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n />\n ) : (\n <ol className=\"flex flex-col gap-3\">\n {questions.map((q, idx) => (\n <QuestionRow key={q.id} index={idx + 1} question={q} />\n ))}\n </ol>\n )}\n </Section>\n\n {/* Add-question dialog */}\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add question</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Question {nextPosition} · pick a type, write the prompt and mark the correct\n answer(s).\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"prompt\">Prompt *</Label>\n <Textarea\n id=\"prompt\"\n value={form.prompt}\n onChange={(e) => setForm({ ...form, prompt: e.target.value.slice(0, PROMPT_MAX) })}\n placeholder=\"Enter your question prompt…\"\n rows={3}\n maxLength={PROMPT_MAX}\n />\n <p className=\"text-right text-xs tabular-nums text-text-secondary\">\n {form.prompt.length} / {PROMPT_MAX}\n </p>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"questionType\">Type</Label>\n <Select value={form.type} onValueChange={(v) => changeType(v as BuiQuestionType)}>\n <SelectTrigger id=\"questionType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {QUESTION_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"points\">Points</Label>\n <Input\n id=\"points\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.points}\n onChange={(e) => setForm({ ...form, points: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n </div>\n\n {usesOptions && (\n <div className=\"grid gap-2\">\n <Label>Options{form.type !== 'TRUE_FALSE' && ' *'}</Label>\n <p className=\"-mt-1 text-xs text-text-secondary\">\n Tick the {isExclusive(form.type) ? 'single correct answer' : 'correct answers'}.\n </p>\n <div className=\"flex flex-col gap-2\">\n {form.options.map((opt, i) => (\n <div\n key={i}\n className={cn(\n 'flex items-center gap-2.5 rounded-lg border px-2.5 py-1.5 transition-colors',\n opt.correct\n ? 'border-status-success-text bg-status-success-bg-subtle'\n : 'border-border-seam bg-bg-surface'\n )}\n >\n <Checkbox\n checked={opt.correct}\n onCheckedChange={(c) => toggleCorrect(i, c === true)}\n aria-label=\"Mark option correct\"\n />\n <Input\n value={opt.label}\n readOnly={form.type === 'TRUE_FALSE'}\n onChange={(e) => setOptionLabel(i, e.target.value)}\n placeholder={`Option ${i + 1}`}\n className=\"flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:ring-0\"\n />\n {opt.correct && (\n <span className=\"shrink-0 text-xs font-medium text-status-success-text\">\n Correct\n </span>\n )}\n {form.type !== 'TRUE_FALSE' && form.options.length > 2 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeOption(i)}\n aria-label=\"Remove option\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n ))}\n </div>\n {form.type !== 'TRUE_FALSE' && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={addOption}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add option\n </Button>\n )}\n </div>\n )}\n\n {form.type === 'SHORT_ANSWER' && (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"shortAnswer\">Expected answer</Label>\n <Input\n id=\"shortAnswer\"\n value={form.shortAnswer}\n onChange={(e) => setForm({ ...form, shortAnswer: e.target.value })}\n placeholder=\"The accepted answer (used as the grading key).\"\n />\n </div>\n )}\n\n {(form.type === 'ESSAY' || form.type === 'CODE') && (\n <p className=\"rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {formatEnumLabel(form.type)} responses are graded manually from the Submissions\n view.\n </p>\n )}\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"explanation\">Explanation</Label>\n <Textarea\n id=\"explanation\"\n value={form.explanation}\n onChange={(e) => setForm({ ...form, explanation: e.target.value })}\n placeholder=\"Shown to learners after grading (optional).\"\n rows={2}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleAddQuestion} disabled={createQuestion.isPending}>\n {createQuestion.isPending ? 'Adding…' : 'Add question'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Meta tile — an icon-circle stat used in the assessment summary card.\n// ---------------------------------------------------------------------------\ninterface MetaTileProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n /** Draw a subtle left divider on ≥sm (all tiles except the first in the row). */\n divider?: boolean;\n}\n\nconst MetaTile: FC<MetaTileProps> = ({ icon, label, value, divider }) => (\n <div\n className={cn(\n 'flex items-center gap-3',\n divider && 'sm:border-l sm:border-border-seam sm:pl-5'\n )}\n >\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Question row\n// ---------------------------------------------------------------------------\nconst freeTextHint = (type: BuiQuestionType): string => {\n switch (type) {\n case 'SHORT_ANSWER':\n return 'Learners type a short answer — auto-graded against the answer key.';\n case 'ESSAY':\n return 'Open long-form response — graded manually from Submissions.';\n case 'CODE':\n return 'Code response — graded manually from Submissions.';\n default:\n return 'Free-text response.';\n }\n};\n\ninterface QuestionRowProps {\n index: number;\n question: BuiQuestion;\n}\n\nconst QuestionRow: FC<QuestionRowProps> = ({ index, question }) => {\n const options = parseOptions(question.options);\n const isChoice = CHOICE_TYPES.has(question.type) || question.type === 'TRUE_FALSE';\n const isMulti = question.type === 'MULTIPLE_CHOICE';\n\n return (\n <li>\n <GlassCard treatment=\"plain\" className=\"p-4 sm:p-5\">\n <div className=\"flex items-start gap-3.5\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-sm font-bold tabular-nums text-[var(--color-accent-teal)]\">\n {index}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={question.type} />\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium tabular-nums text-text-secondary\">\n {question.points} pt{question.points === 1 ? '' : 's'}\n </span>\n </div>\n\n <p className=\"mt-2.5 font-medium text-text-primary\">{question.prompt}</p>\n\n {isChoice && options.length > 0 && (\n <ul className=\"mt-3 flex flex-col gap-2\">\n {options.map((opt, i) => (\n <li\n key={opt.id ?? `${i}-${opt.label}`}\n className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n {isMulti ? (\n <Square className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n ) : (\n <Circle className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n )}\n <span className=\"min-w-0\">{opt.label}</span>\n </li>\n ))}\n </ul>\n )}\n\n {!isChoice && (\n <p className=\"mt-3 rounded-lg border border-dashed border-border-seam bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {freeTextHint(question.type)}\n </p>\n )}\n\n {question.explanation && (\n <p className=\"mt-3 flex items-start gap-1.5 rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n <Lightbulb className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-status-warning-text\" />\n <span>{question.explanation}</span>\n </p>\n )}\n </div>\n </div>\n </GlassCard>\n </li>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssessmentDetailPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentDetailPage.tsx"],"sourcesContent":["/**\n * AssessmentDetailPage — a single assessment + its question builder.\n * @module buildmyiq/pages/assessments\n *\n * Reads `buiAssessment` (which includes `questions[]`). Educators can publish a\n * DRAFT assessment (`useBuiPublishAssessment`) and add questions\n * (`useBuiCreateQuestion`). The question builder writes an `options` JSON array\n * of `{ label }` for choice questions and an optional input-only `answerKey`\n * (correct label(s)) — the latter is never read back (not exposed on the type).\n */\n\nimport { useMemo, useState, type FC, type ReactNode } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Plus,\n Send,\n ClipboardCheck,\n ClipboardEdit,\n Circle,\n Square,\n Clock,\n Trophy,\n Repeat,\n Trash2,\n Lightbulb,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Checkbox,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n Section,\n EmptyState,\n LoadingView,\n ErrorView,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiPublishAssessment, useBuiCreateQuestion } from '../../hooks';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport { QUESTION_TYPES } from '../../types';\nimport type { BuiQuestion, BuiQuestionType } from '../../types';\n\n// ---------------------------------------------------------------------------\n// Safe narrowing for the untyped `options` JSON blob (array of { id?, label }).\n// ---------------------------------------------------------------------------\ninterface QuestionOption {\n id?: string;\n label: string;\n}\n\nconst parseOptions = (raw: unknown): QuestionOption[] => {\n if (!Array.isArray(raw)) return [];\n const out: QuestionOption[] = [];\n for (const item of raw) {\n if (item && typeof item === 'object' && 'label' in item) {\n const label = (item as { label: unknown }).label;\n if (typeof label === 'string') {\n const idVal = (item as { id?: unknown }).id;\n out.push({ label, id: typeof idVal === 'string' ? idVal : undefined });\n }\n }\n }\n return out;\n};\n\nconst PROMPT_MAX = 1000;\n\nconst CHOICE_TYPES = new Set<BuiQuestionType>(['SINGLE_CHOICE', 'MULTIPLE_CHOICE']);\nconst isExclusive = (type: BuiQuestionType): boolean =>\n type === 'SINGLE_CHOICE' || type === 'TRUE_FALSE';\n\ninterface OptionDraft {\n label: string;\n correct: boolean;\n}\n\ninterface QuestionFormState {\n prompt: string;\n type: BuiQuestionType;\n points: string;\n explanation: string;\n options: OptionDraft[];\n shortAnswer: string;\n}\n\nconst blankChoiceOptions = (): OptionDraft[] => [\n { label: '', correct: false },\n { label: '', correct: false },\n];\n\nconst trueFalseOptions = (): OptionDraft[] => [\n { label: 'True', correct: false },\n { label: 'False', correct: false },\n];\n\nconst makeEmptyForm = (): QuestionFormState => ({\n prompt: '',\n type: 'SINGLE_CHOICE',\n points: '1',\n explanation: '',\n options: blankChoiceOptions(),\n shortAnswer: '',\n});\n\nexport const AssessmentDetailPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const publishAssessment = useBuiPublishAssessment();\n const createQuestion = useBuiCreateQuestion();\n\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<QuestionFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const assessment = assessmentQuery.data ?? null;\n const questions = useMemo(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const nextPosition = useMemo(\n () => (questions.length ? Math.max(...questions.map((q) => q.position)) + 1 : 1),\n [questions]\n );\n\n const totalPoints = useMemo(() => questions.reduce((sum, q) => sum + q.points, 0), [questions]);\n\n const usesOptions = form.type === 'TRUE_FALSE' || CHOICE_TYPES.has(form.type);\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const changeType = (type: BuiQuestionType) => {\n setForm((prev) => {\n let options = prev.options;\n if (type === 'TRUE_FALSE') options = trueFalseOptions();\n else if (CHOICE_TYPES.has(type)) {\n options = prev.options.some((o) => o.label === 'True' || o.label === 'False')\n ? blankChoiceOptions()\n : prev.options;\n }\n return { ...prev, type, options };\n });\n };\n\n const setOptionLabel = (index: number, label: string) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) => (i === index ? { ...o, label } : o)),\n }));\n };\n\n const toggleCorrect = (index: number, value: boolean) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) =>\n i === index\n ? { ...o, correct: value }\n : isExclusive(prev.type)\n ? { ...o, correct: false }\n : o\n ),\n }));\n };\n\n const addOption = () => {\n setForm((prev) => ({ ...prev, options: [...prev.options, { label: '', correct: false }] }));\n };\n\n const removeOption = (index: number) => {\n setForm((prev) => ({ ...prev, options: prev.options.filter((_, i) => i !== index) }));\n };\n\n const handlePublish = async () => {\n if (!assessment) return;\n try {\n await publishAssessment.mutateAsync(assessment.id);\n } catch {\n // The mutation surfaces its own error state; nothing else to do here.\n }\n };\n\n const handleAddQuestion = async () => {\n if (!assessmentId) return;\n if (!form.prompt.trim()) {\n setFormError('Question prompt is required.');\n return;\n }\n\n let options: { label: string }[] | undefined;\n let answerKey: string[] | string | undefined;\n\n if (usesOptions) {\n const cleaned = form.options\n .map((o) => ({ label: o.label.trim(), correct: o.correct }))\n .filter((o) => o.label.length > 0);\n if (CHOICE_TYPES.has(form.type) && cleaned.length < 2) {\n setFormError('Add at least two options.');\n return;\n }\n options = cleaned.map((o) => ({ label: o.label }));\n const correct = cleaned.filter((o) => o.correct).map((o) => o.label);\n if (correct.length) answerKey = correct;\n } else if (form.type === 'SHORT_ANSWER') {\n answerKey = form.shortAnswer.trim() || undefined;\n }\n\n try {\n await createQuestion.mutateAsync({\n assessmentId,\n prompt: form.prompt.trim(),\n type: form.type,\n points: form.points ? Number(form.points) : undefined,\n position: nextPosition,\n explanation: form.explanation.trim() || undefined,\n options,\n answerKey,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to add question.');\n }\n };\n\n // ----- Loading / error / not-found -----\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError) {\n return (\n <PageLayout title=\"Assessment\">\n <ErrorView\n message=\"Couldn't load this assessment.\"\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!assessment) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Assessment not found\"\n description=\"This assessment may have been removed or you don't have access to it.\"\n action={<Button onClick={() => navigateTo('assessments')}>Back to assessments</Button>}\n />\n </PageLayout>\n );\n }\n\n const isDraft = assessment.status === 'DRAFT';\n\n return (\n <PageLayout\n title={\n <span className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <ClipboardEdit className=\"h-5 w-5\" />\n </span>\n <span className=\"truncate\">{assessment.title}</span>\n </span>\n }\n description={assessment.description ?? undefined}\n actions={\n <>\n <Button variant=\"ghost\" onClick={() => navigateTo('submissions')}>\n <ClipboardCheck className=\"mr-1.5 h-4 w-4\" /> Submissions\n </Button>\n {isDraft ? (\n <Button onClick={handlePublish} disabled={publishAssessment.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {publishAssessment.isPending ? 'Publishing…' : 'Publish'}\n </Button>\n ) : (\n <StatusBadge status={assessment.status} className=\"px-3 py-1.5\" />\n )}\n </>\n }\n >\n <button\n type=\"button\"\n onClick={() => navigateTo('assessments')}\n className=\"mb-4 inline-flex items-center gap-1.5 text-sm text-text-secondary transition-colors hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> All assessments\n </button>\n\n {/* Meta */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-5 sm:p-6\">\n <div className=\"mb-5 flex flex-wrap items-center gap-2\">\n <StatusBadge status={assessment.type} />\n <StatusBadge status={assessment.status} />\n {assessment.isGraded ? (\n <StatusBadge status=\"GRADED\" />\n ) : (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n Not graded\n </span>\n )}\n </div>\n <div className=\"grid grid-cols-2 gap-y-6 sm:grid-cols-4\">\n <MetaTile\n icon={<Trophy className=\"h-5 w-5\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n <MetaTile\n icon={<Clock className=\"h-5 w-5\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n divider\n />\n <MetaTile\n icon={<Repeat className=\"h-5 w-5\" />}\n label=\"Max attempts\"\n value={assessment.maxAttempts}\n divider\n />\n <MetaTile\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n label=\"Graded\"\n value={assessment.isGraded ? 'Yes' : 'No'}\n divider\n />\n </div>\n </GlassCard>\n\n {/* Questions */}\n <Section\n title={`Questions (${questions.length})`}\n description={\n questions.length > 0\n ? `${totalPoints} point${totalPoints === 1 ? '' : 's'} across ${questions.length} question${\n questions.length === 1 ? '' : 's'\n }`\n : undefined\n }\n action={\n <Button variant=\"outline\" onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n >\n {questions.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No questions yet\"\n description=\"Build this assessment by adding its first question.\"\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n />\n ) : (\n <ol className=\"flex flex-col gap-3\">\n {questions.map((q, idx) => (\n <QuestionRow key={q.id} index={idx + 1} question={q} />\n ))}\n </ol>\n )}\n </Section>\n\n {/* Add-question dialog */}\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add question</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Question {nextPosition} · pick a type, write the prompt and mark the correct\n answer(s).\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"prompt\">Prompt *</Label>\n <Textarea\n id=\"prompt\"\n value={form.prompt}\n onChange={(e) => setForm({ ...form, prompt: e.target.value.slice(0, PROMPT_MAX) })}\n placeholder=\"Enter your question prompt…\"\n rows={3}\n maxLength={PROMPT_MAX}\n />\n <p className=\"text-right text-xs tabular-nums text-text-secondary\">\n {form.prompt.length} / {PROMPT_MAX}\n </p>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"questionType\">Type</Label>\n <Select value={form.type} onValueChange={(v) => changeType(v as BuiQuestionType)}>\n <SelectTrigger id=\"questionType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {QUESTION_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"points\">Points</Label>\n <Input\n id=\"points\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.points}\n onChange={(e) => setForm({ ...form, points: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n </div>\n\n {usesOptions && (\n <div className=\"grid gap-2\">\n <Label>Options{form.type !== 'TRUE_FALSE' && ' *'}</Label>\n <p className=\"-mt-1 text-xs text-text-secondary\">\n Tick the {isExclusive(form.type) ? 'single correct answer' : 'correct answers'}.\n </p>\n <div className=\"flex flex-col gap-2\">\n {form.options.map((opt, i) => (\n <div\n key={i}\n className={cn(\n 'flex items-center gap-2.5 rounded-lg border px-2.5 py-1.5 transition-colors',\n opt.correct\n ? 'border-status-success-text bg-status-success-bg-subtle'\n : 'border-border-seam bg-bg-surface'\n )}\n >\n <Checkbox\n checked={opt.correct}\n onCheckedChange={(c) => toggleCorrect(i, c === true)}\n aria-label=\"Mark option correct\"\n />\n <Input\n value={opt.label}\n readOnly={form.type === 'TRUE_FALSE'}\n onChange={(e) => setOptionLabel(i, e.target.value)}\n placeholder={`Option ${i + 1}`}\n className=\"flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:ring-0\"\n />\n {opt.correct && (\n <span className=\"shrink-0 text-xs font-medium text-status-success-text\">\n Correct\n </span>\n )}\n {form.type !== 'TRUE_FALSE' && form.options.length > 2 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeOption(i)}\n aria-label=\"Remove option\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n ))}\n </div>\n {form.type !== 'TRUE_FALSE' && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={addOption}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add option\n </Button>\n )}\n </div>\n )}\n\n {form.type === 'SHORT_ANSWER' && (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"shortAnswer\">Expected answer</Label>\n <Input\n id=\"shortAnswer\"\n value={form.shortAnswer}\n onChange={(e) => setForm({ ...form, shortAnswer: e.target.value })}\n placeholder=\"The accepted answer (used as the grading key).\"\n />\n </div>\n )}\n\n {(form.type === 'ESSAY' || form.type === 'CODE') && (\n <p className=\"rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {formatEnumLabel(form.type)} responses are graded manually from the Submissions\n view.\n </p>\n )}\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"explanation\">Explanation</Label>\n <Textarea\n id=\"explanation\"\n value={form.explanation}\n onChange={(e) => setForm({ ...form, explanation: e.target.value })}\n placeholder=\"Shown to learners after grading (optional).\"\n rows={2}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleAddQuestion} disabled={createQuestion.isPending}>\n {createQuestion.isPending ? 'Adding…' : 'Add question'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Meta tile — an icon-circle stat used in the assessment summary card.\n// ---------------------------------------------------------------------------\ninterface MetaTileProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n /** Draw a subtle left divider on ≥sm (all tiles except the first in the row). */\n divider?: boolean;\n}\n\nconst MetaTile: FC<MetaTileProps> = ({ icon, label, value, divider }) => (\n <div\n className={cn(\n 'flex items-center gap-3',\n divider && 'sm:border-l sm:border-border-seam sm:pl-5'\n )}\n >\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Question row\n// ---------------------------------------------------------------------------\nconst freeTextHint = (type: BuiQuestionType): string => {\n switch (type) {\n case 'SHORT_ANSWER':\n return 'Learners type a short answer — auto-graded against the answer key.';\n case 'ESSAY':\n return 'Open long-form response — graded manually from Submissions.';\n case 'CODE':\n return 'Code response — graded manually from Submissions.';\n default:\n return 'Free-text response.';\n }\n};\n\ninterface QuestionRowProps {\n index: number;\n question: BuiQuestion;\n}\n\nconst QuestionRow: FC<QuestionRowProps> = ({ index, question }) => {\n const options = parseOptions(question.options);\n const isChoice = CHOICE_TYPES.has(question.type) || question.type === 'TRUE_FALSE';\n const isMulti = question.type === 'MULTIPLE_CHOICE';\n\n return (\n <li>\n <GlassCard treatment=\"plain\" className=\"p-4 sm:p-5\">\n <div className=\"flex items-start gap-3.5\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-sm font-bold tabular-nums text-[var(--color-accent-teal)]\">\n {index}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={question.type} />\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium tabular-nums text-text-secondary\">\n {question.points} pt{question.points === 1 ? '' : 's'}\n </span>\n </div>\n\n <p className=\"mt-2.5 font-medium text-text-primary\">{question.prompt}</p>\n\n {isChoice && options.length > 0 && (\n <ul className=\"mt-3 flex flex-col gap-2\">\n {options.map((opt, i) => (\n <li\n key={opt.id ?? `${i}-${opt.label}`}\n className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n {isMulti ? (\n <Square className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n ) : (\n <Circle className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n )}\n <span className=\"min-w-0\">{opt.label}</span>\n </li>\n ))}\n </ul>\n )}\n\n {!isChoice && (\n <p className=\"mt-3 rounded-lg border border-dashed border-border-seam bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {freeTextHint(question.type)}\n </p>\n )}\n\n {question.explanation && (\n <p className=\"mt-3 flex items-start gap-1.5 rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n <Lightbulb className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-status-warning-text\" />\n <span>{question.explanation}</span>\n </p>\n )}\n </div>\n </div>\n </GlassCard>\n </li>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssessmentsPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAoEnD,eAAO,MAAM,eAAe,EAAE,EAuY7B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TakeAssessmentPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/TakeAssessmentPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AAkE3C,eAAO,MAAM,kBAAkB,EAAE,EA0WhC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TakeAssessmentPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/TakeAssessmentPage.tsx"],"sourcesContent":["/**\n * TakeAssessmentPage — the LEARNER-facing surface to attempt a published\n * assessment and submit answers.\n * @module buildmyiq/pages/assessments\n *\n * Loads the assessment + its questions, collects an answer per question (typed\n * to the question kind), resolves the caller's enrollment for the assessment's\n * course, and submits via `buiSubmitAssessment` ({ assessmentId, enrollmentId?,\n * answers }). Grading is done by an educator in the Grading queue — this page is\n * honest about that (\"submitted, awaiting grading\"). All data is live.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CheckCircle2,\n ClipboardCheck,\n Clock,\n FileText,\n RefreshCw,\n Send,\n Target,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiEnrollments, useBuiSubmitAssessment } from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiQuestion } from '../../types';\n\ninterface Choice {\n label: string;\n}\n\n/** Narrow the untyped `options` JSON blob to a list of `{ label }` choices. */\nfunction parseChoices(options: unknown): Choice[] {\n if (!Array.isArray(options)) return [];\n return options\n .map((o) => (o && typeof o === 'object' && 'label' in o ? { label: String((o as { label: unknown }).label) } : null))\n .filter((c): c is Choice => c !== null && c.label.length > 0);\n}\n\ntype AnswerValue = string | string[];\n\nconst TRUE_FALSE_CHOICES: Choice[] = [{ label: 'True' }, { label: 'False' }];\n\ninterface MetaCellProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n}\n\nconst MetaCell: FC<MetaCellProps> = ({ icon, label, value }) => (\n <div className=\"flex items-center gap-3 py-3 first:pt-0 last:pb-0 sm:px-5 sm:py-0 sm:first:pl-0 sm:last:pr-0\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-text-secondary\">{label}</p>\n <p className=\"text-sm font-semibold text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\nexport const TakeAssessmentPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { currentUser, navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const assessment = assessmentQuery.data ?? null;\n\n const enrollmentsQuery = useBuiEnrollments({\n learnerId: currentUser?.id,\n courseId: assessment?.courseId,\n enabled: Boolean(currentUser?.id && assessment?.courseId),\n });\n const enrollmentId = enrollmentsQuery.data?.items?.[0]?.id;\n\n const submit = useBuiSubmitAssessment();\n const [answers, setAnswers] = useState<Record<string, AnswerValue>>({});\n const [submittedId, setSubmittedId] = useState<string | null>(null);\n\n const questions = useMemo<BuiQuestion[]>(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const setAnswer = (qid: string, value: AnswerValue): void => {\n setAnswers((prev) => ({ ...prev, [qid]: value }));\n };\n\n const toggleMulti = (qid: string, label: string): void => {\n setAnswers((prev) => {\n const cur = Array.isArray(prev[qid]) ? (prev[qid] as string[]) : [];\n return { ...prev, [qid]: cur.includes(label) ? cur.filter((l) => l !== label) : [...cur, label] };\n });\n };\n\n const isAnswered = (q: BuiQuestion): boolean => {\n const a = answers[q.id];\n return Array.isArray(a) ? a.length > 0 : Boolean(a && String(a).trim());\n };\n\n const answeredCount = questions.filter(isAnswered).length;\n const answeredPct = questions.length > 0 ? (answeredCount / questions.length) * 100 : 0;\n const remaining = questions.length - answeredCount;\n const encouragement =\n answeredCount === 0\n ? 'Answer a question to get started.'\n : remaining === 0\n ? 'All questions answered — ready to submit.'\n : `${remaining} question${remaining === 1 ? '' : 's'} left to answer.`;\n\n const handleSubmit = async (): Promise<void> => {\n if (!assessmentId) return;\n const result = await submit.mutateAsync({\n assessmentId,\n enrollmentId,\n attempt: 1,\n answers,\n });\n setSubmittedId(result.id);\n };\n\n const backToCourse = assessment\n ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> Back to course\n </Button>\n )\n : undefined;\n\n if (!assessmentId) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState illustration=\"empty-data\" title=\"No assessment selected\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading the assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError || !assessment) {\n return (\n <PageLayout title=\"Assessment\" actions={backToCourse}>\n <ErrorView\n title=\"Couldn't load this assessment\"\n message={assessmentQuery.error instanceof Error ? assessmentQuery.error.message : undefined}\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (assessment.status !== 'PUBLISHED') {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"This assessment isn't open yet\"\n description=\"It has to be published by an educator before you can take it.\"\n />\n </PageLayout>\n );\n }\n\n // Submitted confirmation\n if (submittedId) {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <div className=\"mx-auto max-w-lg\">\n <GlassCard treatment=\"glass\" glow className=\"flex flex-col items-center gap-4 p-8 text-center\">\n <div className=\"flex h-14 w-14 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <CheckCircle2 className=\"h-7 w-7 text-status-success-text\" />\n </div>\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Submitted</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Your answers for “{assessment.title}” were submitted.\n {assessment.isGraded\n ? ' An educator will grade them — you’ll see your score and feedback in My Learning.'\n : ' Thanks for completing it.'}\n </p>\n </div>\n <div className=\"flex flex-wrap justify-center gap-2\">\n <Button variant=\"secondary\" onClick={() => navigateTo('learning')}>\n Go to My Learning\n </Button>\n <Button variant=\"ghost\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n Back to course\n </Button>\n </div>\n </GlassCard>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={assessment.title}\n description={assessment.description ?? undefined}\n actions={backToCourse}\n >\n <div className=\"mx-auto max-w-3xl pb-4\">\n {/* Meta strip — type · passing score · time limit · attempts · answered */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-4 sm:px-5\">\n <div className=\"flex flex-col divide-y divide-border-seam sm:flex-row sm:flex-wrap sm:items-center sm:divide-x sm:divide-y-0\">\n <MetaCell\n icon={<FileText className=\"h-4 w-4\" />}\n label=\"Type\"\n value={<StatusBadge status={assessment.type} />}\n />\n <MetaCell\n icon={<Target className=\"h-4 w-4\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n {assessment.timeLimitMin > 0 && (\n <MetaCell\n icon={<Clock className=\"h-4 w-4\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n />\n )}\n {assessment.maxAttempts > 0 && (\n <MetaCell\n icon={<RefreshCw className=\"h-4 w-4\" />}\n label=\"Attempts\"\n value={assessment.maxAttempts}\n />\n )}\n <div className=\"flex items-center gap-3 py-3 sm:ml-auto sm:py-0 sm:pl-5\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length}\n </p>\n <p className=\"text-xs text-text-secondary\">answered</p>\n </div>\n </div>\n </div>\n </GlassCard>\n\n {questions.length === 0 ? (\n <EmptyState\n icon={<ClipboardCheck className=\"h-8 w-8\" />}\n title=\"No questions yet\"\n description=\"This assessment doesn't have any questions to answer.\"\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {questions.map((q, idx) => {\n const choices =\n q.type === 'TRUE_FALSE' ? TRUE_FALSE_CHOICES : parseChoices(q.options);\n const answer = answers[q.id];\n const answered = isAnswered(q);\n return (\n <GlassCard\n key={q.id}\n treatment={answered ? 'insight' : 'plain'}\n className={cn(\n 'p-5 transition-shadow',\n answered && 'ring-1 ring-[var(--color-accent-soft)]'\n )}\n >\n <div className=\"flex items-start gap-3 sm:gap-4\">\n <span\n className={cn(\n 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold tabular-nums transition-colors',\n answered\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-[var(--color-accent-soft)] text-text-primary'\n )}\n >\n {answered ? <CheckCircle2 className=\"h-4 w-4\" /> : idx + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-md bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n {formatEnumLabel(q.type)}\n </span>\n <span className=\"ml-auto text-xs text-text-secondary tabular-nums\">\n {q.points} {q.points === 1 ? 'point' : 'points'}\n </span>\n </div>\n <p className=\"mt-2.5 font-medium text-text-primary\">{q.prompt}</p>\n\n <div className=\"mt-4\">\n {(q.type === 'SINGLE_CHOICE' || q.type === 'TRUE_FALSE') && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n answer === c.label\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"radio\"\n name={q.id}\n className=\"h-4 w-4 accent-[var(--color-accent-teal)]\"\n checked={answer === c.label}\n onChange={() => setAnswer(q.id, c.label)}\n />\n {c.label}\n </label>\n ))}\n </div>\n )}\n\n {q.type === 'MULTIPLE_CHOICE' && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => {\n const checked = Array.isArray(answer) && answer.includes(c.label);\n return (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n checked\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded accent-[var(--color-accent-teal)]\"\n checked={checked}\n onChange={() => toggleMulti(q.id, c.label)}\n />\n {c.label}\n </label>\n );\n })}\n </div>\n )}\n\n {q.type === 'SHORT_ANSWER' && (\n <Input\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder=\"Type your answer here…\"\n />\n )}\n\n {(q.type === 'ESSAY' || q.type === 'CODE') && (\n <Textarea\n rows={q.type === 'CODE' ? 6 : 4}\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder={q.type === 'CODE' ? 'Write your code…' : 'Write your answer…'}\n className={q.type === 'CODE' ? 'font-mono text-sm' : undefined}\n />\n )}\n </div>\n </div>\n </div>\n </GlassCard>\n );\n })}\n\n {submit.isError && (\n <p className=\"text-sm text-status-error-text\">\n {submit.error instanceof Error ? submit.error.message : 'Could not submit your answers.'}\n </p>\n )}\n\n {/* Sticky submit bar */}\n <div className=\"sticky bottom-0 z-10 pt-2\">\n <div className=\"flex flex-col gap-3 rounded-2xl border border-border-seam bg-bg-surface/90 p-4 shadow-elevation-2 backdrop-blur sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length} answered\n </p>\n <p className=\"text-xs text-text-secondary\">{encouragement}</p>\n </div>\n </div>\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleSubmit}\n disabled={submit.isPending || answeredCount === 0}\n >\n <Send className=\"mr-1.5 h-4 w-4\" />\n {submit.isPending ? 'Submitting…' : 'Submit assessment'}\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TakeAssessmentPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/TakeAssessmentPage.tsx"],"sourcesContent":["/**\n * TakeAssessmentPage — the LEARNER-facing surface to attempt a published\n * assessment and submit answers.\n * @module buildmyiq/pages/assessments\n *\n * Loads the assessment + its questions, collects an answer per question (typed\n * to the question kind), resolves the caller's enrollment for the assessment's\n * course, and submits via `buiSubmitAssessment` ({ assessmentId, enrollmentId?,\n * answers }). Grading is done by an educator in the Grading queue — this page is\n * honest about that (\"submitted, awaiting grading\"). All data is live.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CheckCircle2,\n ClipboardCheck,\n Clock,\n FileText,\n RefreshCw,\n Send,\n Target,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiEnrollments, useBuiSubmitAssessment } from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiQuestion } from '../../types';\n\ninterface Choice {\n label: string;\n}\n\n/** Narrow the untyped `options` JSON blob to a list of `{ label }` choices. */\nfunction parseChoices(options: unknown): Choice[] {\n if (!Array.isArray(options)) return [];\n return options\n .map((o) =>\n o && typeof o === 'object' && 'label' in o\n ? { label: String((o as { label: unknown }).label) }\n : null\n )\n .filter((c): c is Choice => c !== null && c.label.length > 0);\n}\n\ntype AnswerValue = string | string[];\n\nconst TRUE_FALSE_CHOICES: Choice[] = [{ label: 'True' }, { label: 'False' }];\n\ninterface MetaCellProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n}\n\nconst MetaCell: FC<MetaCellProps> = ({ icon, label, value }) => (\n <div className=\"flex items-center gap-3 py-3 first:pt-0 last:pb-0 sm:px-5 sm:py-0 sm:first:pl-0 sm:last:pr-0\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-text-secondary\">{label}</p>\n <p className=\"text-sm font-semibold text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\nexport const TakeAssessmentPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { currentUser, navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const assessment = assessmentQuery.data ?? null;\n\n const enrollmentsQuery = useBuiEnrollments({\n learnerId: currentUser?.id,\n courseId: assessment?.courseId,\n enabled: Boolean(currentUser?.id && assessment?.courseId),\n });\n const enrollmentId = enrollmentsQuery.data?.items?.[0]?.id;\n\n const submit = useBuiSubmitAssessment();\n const [answers, setAnswers] = useState<Record<string, AnswerValue>>({});\n const [submittedId, setSubmittedId] = useState<string | null>(null);\n\n const questions = useMemo<BuiQuestion[]>(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const setAnswer = (qid: string, value: AnswerValue): void => {\n setAnswers((prev) => ({ ...prev, [qid]: value }));\n };\n\n const toggleMulti = (qid: string, label: string): void => {\n setAnswers((prev) => {\n const cur = Array.isArray(prev[qid]) ? (prev[qid] as string[]) : [];\n return {\n ...prev,\n [qid]: cur.includes(label) ? cur.filter((l) => l !== label) : [...cur, label],\n };\n });\n };\n\n const isAnswered = (q: BuiQuestion): boolean => {\n const a = answers[q.id];\n return Array.isArray(a) ? a.length > 0 : Boolean(a && String(a).trim());\n };\n\n const answeredCount = questions.filter(isAnswered).length;\n const answeredPct = questions.length > 0 ? (answeredCount / questions.length) * 100 : 0;\n const remaining = questions.length - answeredCount;\n const encouragement =\n answeredCount === 0\n ? 'Answer a question to get started.'\n : remaining === 0\n ? 'All questions answered — ready to submit.'\n : `${remaining} question${remaining === 1 ? '' : 's'} left to answer.`;\n\n const handleSubmit = async (): Promise<void> => {\n if (!assessmentId) return;\n const result = await submit.mutateAsync({\n assessmentId,\n enrollmentId,\n attempt: 1,\n answers,\n });\n setSubmittedId(result.id);\n };\n\n const backToCourse = assessment ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> Back to course\n </Button>\n ) : undefined;\n\n if (!assessmentId) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState illustration=\"empty-data\" title=\"No assessment selected\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading the assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError || !assessment) {\n return (\n <PageLayout title=\"Assessment\" actions={backToCourse}>\n <ErrorView\n title=\"Couldn't load this assessment\"\n message={\n assessmentQuery.error instanceof Error ? assessmentQuery.error.message : undefined\n }\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (assessment.status !== 'PUBLISHED') {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"This assessment isn't open yet\"\n description=\"It has to be published by an educator before you can take it.\"\n />\n </PageLayout>\n );\n }\n\n // Submitted confirmation\n if (submittedId) {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <div className=\"mx-auto max-w-lg\">\n <GlassCard\n treatment=\"glass\"\n glow\n className=\"flex flex-col items-center gap-4 p-8 text-center\"\n >\n <div className=\"flex h-14 w-14 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <CheckCircle2 className=\"h-7 w-7 text-status-success-text\" />\n </div>\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Submitted</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Your answers for “{assessment.title}” were submitted.\n {assessment.isGraded\n ? ' An educator will grade them — you’ll see your score and feedback in My Learning.'\n : ' Thanks for completing it.'}\n </p>\n </div>\n <div className=\"flex flex-wrap justify-center gap-2\">\n <Button variant=\"secondary\" onClick={() => navigateTo('learning')}>\n Go to My Learning\n </Button>\n <Button variant=\"ghost\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n Back to course\n </Button>\n </div>\n </GlassCard>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={assessment.title}\n description={assessment.description ?? undefined}\n actions={backToCourse}\n >\n <div className=\"mx-auto max-w-3xl pb-4\">\n {/* Meta strip — type · passing score · time limit · attempts · answered */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-4 sm:px-5\">\n <div className=\"flex flex-col divide-y divide-border-seam sm:flex-row sm:flex-wrap sm:items-center sm:divide-x sm:divide-y-0\">\n <MetaCell\n icon={<FileText className=\"h-4 w-4\" />}\n label=\"Type\"\n value={<StatusBadge status={assessment.type} />}\n />\n <MetaCell\n icon={<Target className=\"h-4 w-4\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n {assessment.timeLimitMin > 0 && (\n <MetaCell\n icon={<Clock className=\"h-4 w-4\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n />\n )}\n {assessment.maxAttempts > 0 && (\n <MetaCell\n icon={<RefreshCw className=\"h-4 w-4\" />}\n label=\"Attempts\"\n value={assessment.maxAttempts}\n />\n )}\n <div className=\"flex items-center gap-3 py-3 sm:ml-auto sm:py-0 sm:pl-5\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length}\n </p>\n <p className=\"text-xs text-text-secondary\">answered</p>\n </div>\n </div>\n </div>\n </GlassCard>\n\n {questions.length === 0 ? (\n <EmptyState\n icon={<ClipboardCheck className=\"h-8 w-8\" />}\n title=\"No questions yet\"\n description=\"This assessment doesn't have any questions to answer.\"\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {questions.map((q, idx) => {\n const choices =\n q.type === 'TRUE_FALSE' ? TRUE_FALSE_CHOICES : parseChoices(q.options);\n const answer = answers[q.id];\n const answered = isAnswered(q);\n return (\n <GlassCard\n key={q.id}\n treatment={answered ? 'insight' : 'plain'}\n className={cn(\n 'p-5 transition-shadow',\n answered && 'ring-1 ring-[var(--color-accent-soft)]'\n )}\n >\n <div className=\"flex items-start gap-3 sm:gap-4\">\n <span\n className={cn(\n 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold tabular-nums transition-colors',\n answered\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-[var(--color-accent-soft)] text-text-primary'\n )}\n >\n {answered ? <CheckCircle2 className=\"h-4 w-4\" /> : idx + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-md bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n {formatEnumLabel(q.type)}\n </span>\n <span className=\"ml-auto text-xs text-text-secondary tabular-nums\">\n {q.points} {q.points === 1 ? 'point' : 'points'}\n </span>\n </div>\n <p className=\"mt-2.5 font-medium text-text-primary\">{q.prompt}</p>\n\n <div className=\"mt-4\">\n {(q.type === 'SINGLE_CHOICE' || q.type === 'TRUE_FALSE') && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n answer === c.label\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"radio\"\n name={q.id}\n className=\"h-4 w-4 accent-[var(--color-accent-teal)]\"\n checked={answer === c.label}\n onChange={() => setAnswer(q.id, c.label)}\n />\n {c.label}\n </label>\n ))}\n </div>\n )}\n\n {q.type === 'MULTIPLE_CHOICE' && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => {\n const checked = Array.isArray(answer) && answer.includes(c.label);\n return (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n checked\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded accent-[var(--color-accent-teal)]\"\n checked={checked}\n onChange={() => toggleMulti(q.id, c.label)}\n />\n {c.label}\n </label>\n );\n })}\n </div>\n )}\n\n {q.type === 'SHORT_ANSWER' && (\n <Input\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder=\"Type your answer here…\"\n />\n )}\n\n {(q.type === 'ESSAY' || q.type === 'CODE') && (\n <Textarea\n rows={q.type === 'CODE' ? 6 : 4}\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder={\n q.type === 'CODE' ? 'Write your code…' : 'Write your answer…'\n }\n className={q.type === 'CODE' ? 'font-mono text-sm' : undefined}\n />\n )}\n </div>\n </div>\n </div>\n </GlassCard>\n );\n })}\n\n {submit.isError && (\n <p className=\"text-sm text-status-error-text\">\n {submit.error instanceof Error\n ? submit.error.message\n : 'Could not submit your answers.'}\n </p>\n )}\n\n {/* Sticky submit bar */}\n <div className=\"sticky bottom-0 z-10 pt-2\">\n <div className=\"flex flex-col gap-3 rounded-2xl border border-border-seam bg-bg-surface/90 p-4 shadow-elevation-2 backdrop-blur sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length} answered\n </p>\n <p className=\"text-xs text-text-secondary\">{encouragement}</p>\n </div>\n </div>\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleSubmit}\n disabled={submit.isPending || answeredCount === 0}\n >\n <Send className=\"mr-1.5 h-4 w-4\" />\n {submit.isPending ? 'Submitting…' : 'Submit assessment'}\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthoringPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/authoring/AuthoringPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAmLhC,eAAO,MAAM,aAAa,EAAE,EA0H3B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CourseBuilderPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/authoring/CourseBuilderPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AA6sB3C,eAAO,MAAM,iBAAiB,EAAE,EA2J/B,CAAC"}
|