diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 17fb2fb4c..1c2abd0cb 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -46,7 +46,7 @@ jobs: run: pnpm generate - name: Run Prettier - run: pnpm prettier --check + run: pnpm prettier:check - name: Run Lint run: pnpm lint diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index ae9343aea..9d92bbfe0 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -10,7 +10,7 @@ "allowSyntheticDefaultImports": true, "strict": true, "types": ["react"], - "lib": ["esnext", "dom"] + "lib": ["esnext", "dom"], }, - "files": [".storybook/preview.tsx"] + "files": [".storybook/preview.tsx"], } diff --git a/apps/e2e-web/tsconfig.json b/apps/e2e-web/tsconfig.json index 87f53f8d5..ad2bb620f 100644 --- a/apps/e2e-web/tsconfig.json +++ b/apps/e2e-web/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "sourceMap": false, - "types": ["cypress", "node"] + "types": ["cypress", "node"], }, - "include": ["src/**/*.ts", "cypress.config.ts"] + "include": ["src/**/*.ts", "cypress.config.ts"], } diff --git a/apps/next-app/pages/modules/[questionBank]/questions/index.page.tsx b/apps/next-app/pages/modules/[questionBank]/questions/index.page.tsx index 5617c33ce..2a4a502b8 100644 --- a/apps/next-app/pages/modules/[questionBank]/questions/index.page.tsx +++ b/apps/next-app/pages/modules/[questionBank]/questions/index.page.tsx @@ -23,7 +23,7 @@ const Page: NextPage = ({ questionBank }) => { return ( - + ); }; diff --git a/apps/next-app/pages/modules/[questionBank]/tests/index.page.tsx b/apps/next-app/pages/modules/[questionBank]/tests/index.page.tsx index beeffdfd4..4d2a9562b 100644 --- a/apps/next-app/pages/modules/[questionBank]/tests/index.page.tsx +++ b/apps/next-app/pages/modules/[questionBank]/tests/index.page.tsx @@ -1,6 +1,6 @@ import * as fs from "node:fs/promises"; import { AppHead } from "@chair-flight/react/components"; -import { LayoutModule, TestsOverview } from "@chair-flight/react/containers"; +import { LayoutModule, TestSearch } from "@chair-flight/react/containers"; import { staticHandler } from "@chair-flight/trpc/server"; import type { QuestionBankName } from "@chair-flight/base/types"; import type { Breadcrumbs } from "@chair-flight/react/containers"; @@ -21,13 +21,9 @@ const Page: NextPage = ({ questionBank }) => { ] as Breadcrumbs; return ( - + - + ); }; @@ -35,6 +31,7 @@ const Page: NextPage = ({ questionBank }) => { export const getStaticProps = staticHandler( async ({ params, helper }) => { await LayoutModule.getData({ helper, params }); + await TestSearch.getData({ helper, params }); return { props: params }; }, fs, diff --git a/apps/next-app/tsconfig.json b/apps/next-app/tsconfig.json index 1afe21e3b..f56dee02a 100644 --- a/apps/next-app/tsconfig.json +++ b/apps/next-app/tsconfig.json @@ -10,17 +10,17 @@ "esModuleInterop": true, "plugins": [ { - "name": "next" - } + "name": "next", + }, ], - "strictNullChecks": true + "strictNullChecks": true, }, "include": [ "index.d.ts", "next-env.d.ts", "pages/**/*.ts", "pages/**/*.tsx", - ".next/types/**/*.ts" + ".next/types/**/*.ts", ], - "exclude": ["node_modules"] + "exclude": ["node_modules"], } diff --git a/libs/base/env/tsconfig.json b/libs/base/env/tsconfig.json index 60de56452..cf5207786 100644 --- a/libs/base/env/tsconfig.json +++ b/libs/base/env/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], } diff --git a/libs/base/errors/tsconfig.json b/libs/base/errors/tsconfig.json index 60de56452..cf5207786 100644 --- a/libs/base/errors/tsconfig.json +++ b/libs/base/errors/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], } diff --git a/libs/base/types/tsconfig.json b/libs/base/types/tsconfig.json index 60de56452..cf5207786 100644 --- a/libs/base/types/tsconfig.json +++ b/libs/base/types/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], } diff --git a/libs/core/analytics/tsconfig.json b/libs/core/analytics/tsconfig.json index 38b50d525..095dd39fa 100644 --- a/libs/core/analytics/tsconfig.json +++ b/libs/core/analytics/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": {}, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], } diff --git a/libs/core/app/src/index.ts b/libs/core/app/src/index.ts index 5c9d392ad..d7293355d 100644 --- a/libs/core/app/src/index.ts +++ b/libs/core/app/src/index.ts @@ -4,3 +4,4 @@ export * from "./questions/get-question-preview"; export * from "./questions/get-new-variant"; export * from "./random/random"; export * from "./tests/create-test"; +export * from "./tests/process-test"; diff --git a/libs/core/app/src/tests/process-test.tsx b/libs/core/app/src/tests/process-test.tsx new file mode 100644 index 000000000..30e16d008 --- /dev/null +++ b/libs/core/app/src/tests/process-test.tsx @@ -0,0 +1,49 @@ +import { DateTime, Duration } from "luxon"; +import type { Test } from "@chair-flight/base/types"; + +const getClockTime = (milliseconds: number) => { + const duration = Duration.fromMillis(milliseconds); + console.log(milliseconds, duration); + return duration.toFormat("hh:mm"); +}; + +/** + * returns a bunch of derivative information from data in Test. + */ +export const processTest = (test: Test) => { + const correctAnswers = test.questions.reduce( + (s, q) => s + (q.selectedOptionId === q.correctOptionId ? 1 : 0), + 0, + ); + + const rawScore = (correctAnswers / test.questions.length) * 100; + const score = Math.round(Math.min(100, Math.max(0, rawScore))); + + const color = ((): "warning" | "success" | "danger" | "primary" => { + if (test.status === "created") return "primary"; + if (test.status === "started") return "warning"; + if (score >= 75) return "success"; + return "danger"; + })(); + + const timeLeft = (() => { + if (test.status === "finished") return "-"; + if (test.mode === "study") return "-"; + const tLeft = test.durationInMs - test.timeSpentInMs; + return getClockTime(tLeft); + })(); + + const timeSpent = getClockTime(test.timeSpentInMs); + + const timeStarted = + test.startedAtEpochMs && + DateTime.fromMillis(test.startedAtEpochMs).toFormat("DDD"); + + return { + color, + score, + timeSpent, + timeLeft, + timeStarted, + }; +}; diff --git a/libs/core/app/tsconfig.json b/libs/core/app/tsconfig.json index 7679ab17c..5e7280d8a 100644 --- a/libs/core/app/tsconfig.json +++ b/libs/core/app/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../../tsconfig.base.json", "include": ["src/**/*.ts"], - "exclude": ["src/**/*.test.*"] + "exclude": ["src/**/*.test.*"], } diff --git a/libs/core/github/tsconfig.json b/libs/core/github/tsconfig.json index 858dc5c45..b25d09e31 100644 --- a/libs/core/github/tsconfig.json +++ b/libs/core/github/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "types": ["node"] + "types": ["node"], }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], } diff --git a/libs/core/question-bank/tsconfig.json b/libs/core/question-bank/tsconfig.json index bde20acde..287d3efb3 100644 --- a/libs/core/question-bank/tsconfig.json +++ b/libs/core/question-bank/tsconfig.json @@ -2,6 +2,6 @@ "extends": "../../../tsconfig.base.json", "include": ["src/**/*.ts", "executors/**/*.mts", "executors/**/*.ts"], "compilerOptions": { - "types": ["vitest/globals", "@testing-library/jest-dom"] - } + "types": ["vitest/globals", "@testing-library/jest-dom"], + }, } diff --git a/libs/core/schemas/tsconfig.json b/libs/core/schemas/tsconfig.json index 60de56452..cf5207786 100644 --- a/libs/core/schemas/tsconfig.json +++ b/libs/core/schemas/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], } diff --git a/libs/react/analytics/tsconfig.json b/libs/react/analytics/tsconfig.json index ec8473e0a..79c8eaa06 100644 --- a/libs/react/analytics/tsconfig.json +++ b/libs/react/analytics/tsconfig.json @@ -3,6 +3,6 @@ "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], "compilerOptions": { - "lib": ["dom"] - } + "lib": ["dom"], + }, } diff --git a/libs/react/components/src/index.ts b/libs/react/components/src/index.ts index 896f937d8..82ba6e695 100644 --- a/libs/react/components/src/index.ts +++ b/libs/react/components/src/index.ts @@ -26,8 +26,8 @@ export * from "./question-navigation"; export * from "./question-variant-preview"; export * from "./search-filters"; export * from "./search-query"; +export * from "./search-list"; export * from "./sidebar"; -export * from "./test-preview"; export * from "./test-question-result"; export * from "./theme"; export * from "./toaster"; diff --git a/libs/react/components/src/nested-checkbox-select/nested-checkbox-select.stories.tsx b/libs/react/components/src/nested-checkbox-select/nested-checkbox-select.stories.tsx index 3c18735a6..1419981fe 100644 --- a/libs/react/components/src/nested-checkbox-select/nested-checkbox-select.stories.tsx +++ b/libs/react/components/src/nested-checkbox-select/nested-checkbox-select.stories.tsx @@ -17,11 +17,10 @@ export const Playground: Story = { items={items} onChange={(...val) => { args.onChange?.(...val); - setItems( - (oldItems) => - oldItems?.map((oldItem) => { - return oldItem.id === val[0].id ? val[0] : oldItem; - }), + setItems((oldItems) => + oldItems?.map((oldItem) => { + return oldItem.id === val[0].id ? val[0] : oldItem; + }), ); }} /> diff --git a/libs/react/components/src/question-list/question-list.stories.tsx b/libs/react/components/src/question-list/question-list.stories.tsx index 4927d6239..f2ae33831 100644 --- a/libs/react/components/src/question-list/question-list.stories.tsx +++ b/libs/react/components/src/question-list/question-list.stories.tsx @@ -13,7 +13,7 @@ const meta: Meta = { loading: false, error: false, forceMode: undefined, - questions: [ + items: [ { id: "gdjhd", questionId: "26mpt", diff --git a/libs/react/components/src/question-list/question-list.tsx b/libs/react/components/src/question-list/question-list.tsx index efad6023e..ad6b38498 100644 --- a/libs/react/components/src/question-list/question-list.tsx +++ b/libs/react/components/src/question-list/question-list.tsx @@ -1,191 +1,110 @@ -import { Fragment, forwardRef, useRef } from "react"; -import { NoSsr } from "@mui/base"; -import { - Box, - CircularProgress, - Link, - List, - ListDivider, - ListItem, - ListItemContent, - Sheet, - Table, - Typography, - useTheme, -} from "@mui/joy"; -import { useMediaQuery } from "../hooks/use-media-query"; +import { forwardRef } from "react"; +import { Box, Link, ListItemContent, Typography } from "@mui/joy"; import { MarkdownClientCompressed } from "../markdown-client"; -import { Ups } from "../ups"; -import type { SheetProps } from "@mui/joy"; +import { SearchList } from "../search-list"; +import type { SearchListProps } from "../search-list"; -export type QuestionListProps = { - loading?: boolean; - error?: boolean; - forceMode?: "mobile" | "desktop"; - questions?: Array<{ - id: string; - questionId: string; - variantId: string; +export type QuestionListItem = { + id: string; + questionId: string; + variantId: string; + href: string; + text: string; + learningObjectives: Array<{ + name: string; href: string; - text: string; - learningObjectives: Array<{ - name: string; - href: string; - }>; - externalIds: string[]; }>; - onFetchNextPage?: () => Promise; -} & SheetProps; + externalIds: string[]; +}; -export const QuestionList = forwardRef( - ( - { - loading, - error, - questions = [], - forceMode, - onFetchNextPage, - ...sheetProps - }, - ref, - ) => { - const isFetchingMore = useRef(false); - const theme = useTheme(); - const isMobileMq = useMediaQuery(theme.breakpoints.down("md")); - const hasNoResults = !error && !loading && !questions.length; - const hasResults = !error && !loading && questions.length; - - const isMobile = (() => { - if (forceMode && forceMode === "desktop") return false; - if (forceMode === "mobile") return true; - return isMobileMq; - })(); - - const onScroll = (e: React.UIEvent) => { - const target = e.target as HTMLUListElement; - const { scrollHeight, scrollTop, clientHeight } = target; - const distance = scrollHeight - scrollTop - clientHeight; - if (distance < 500 && !loading && !isFetchingMore.current) { - isFetchingMore.current = true; - onFetchNextPage?.().finally(() => { - isFetchingMore.current = false; - }); - } - }; +export type QuestionListProps = Omit< + SearchListProps, + | "items" + | "renderThead" + | "renderTableRow" + | "renderListItemContent" + | "errorMessage" + | "noDataMessage" +> & { + items?: SearchListProps["items"]; +}; +export const QuestionList = forwardRef( + ({ items = [], ...otherProps }, ref) => { return ( - - - - {loading && ( - - )} - {error && } - {hasNoResults && ( - - )} - {hasResults && !isMobile && ( - - - - - - - - - - - {questions.map((result) => ( - - - - - - - ))} - -
IDQuestionLearning ObjectivesExternal IDs
- - {result.questionId} -
- - {result.variantId} - - -
- - {result.text} - - - {result.learningObjectives.map(({ name, href }) => ( - - ))} - - {result.externalIds.map((id) => ( - - {id} - - ))} -
- )} - {hasResults && isMobile && ( - - {questions.map((result) => ( - - - - - {result.questionId} - - - {result.variantId} - - - {result.learningObjectives - .map((lo) => lo.name) - .join(", ")} - - - - {result.text} - - - - - - - ))} - - )} -
-
-
+ ( + + + ID + Question + Learning Objectives + External IDs + + + )} + renderTableRow={(result) => ( + + + + {result.questionId} +
+ {result.variantId} + + + + {result.text} + + + {result.learningObjectives.map(({ name, href }) => ( + + ))} + + + {result.externalIds.map((id) => ( + + {id} + + ))} + + + )} + renderListItemContent={(result) => ( + + + {result.questionId} + + + {result.variantId} + + + {result.learningObjectives.map((lo) => lo.name).join(", ")} + + + {result.text} + + + )} + /> ); }, ); diff --git a/libs/react/components/src/search-list/index.ts b/libs/react/components/src/search-list/index.ts new file mode 100644 index 000000000..b5e6a1ae1 --- /dev/null +++ b/libs/react/components/src/search-list/index.ts @@ -0,0 +1,2 @@ +export { SearchList } from "./search-list"; +export type { SearchListProps } from "./search-list"; diff --git a/libs/react/components/src/search-list/search-list.stories.tsx b/libs/react/components/src/search-list/search-list.stories.tsx new file mode 100644 index 000000000..3af41e14a --- /dev/null +++ b/libs/react/components/src/search-list/search-list.stories.tsx @@ -0,0 +1,24 @@ +import { QuestionList } from "../question-list"; +import * as QuestionListStories from "../question-list/question-list.stories"; +import { SearchList } from "./search-list"; +import type { Meta, StoryObj } from "@storybook/react"; + +export const QuestionListStory: StoryObj = { + ...QuestionListStories.Playground, + name: "QuestionList", + args: QuestionListStories.default.args, + render: (props) => , +}; + +const meta: Meta = { + title: "Components/SearchList", + component: SearchList, + tags: ["autodocs"], + argTypes: { + renderThead: { control: false }, + renderListItemContent: { control: false }, + renderTableRow: { control: false }, + }, +}; + +export default meta; diff --git a/libs/react/components/src/search-list/search-list.tsx b/libs/react/components/src/search-list/search-list.tsx new file mode 100644 index 000000000..6df37cce3 --- /dev/null +++ b/libs/react/components/src/search-list/search-list.tsx @@ -0,0 +1,141 @@ +import { Fragment, forwardRef, useRef } from "react"; +import { NoSsr } from "@mui/base"; +import { + Box, + List, + ListDivider, + ListItem, + Sheet, + Skeleton, + Table, + useTheme, +} from "@mui/joy"; +import { useMediaQuery } from "../hooks/use-media-query"; +import { Ups } from "../ups"; +import type { SheetProps } from "@mui/joy"; +import type { ForwardedRef, FunctionComponent } from "react"; + +export type SearchListProps = { + items: T[]; + loading?: boolean; + error?: boolean; + errorMessage?: string; + noDataMessage?: string; + forceMode?: "mobile" | "desktop"; + /** Should return a `` */ + renderThead: FunctionComponent; + /** Should return a `` */ + renderTableRow: FunctionComponent; + /** Should return a `` */ + renderListItemContent: FunctionComponent; + onFetchNextPage?: () => Promise; +} & SheetProps; + +const LoadingPlaceholder: FunctionComponent = () => ( + + + {[...new Array(20).keys()].map((k) => ( + + ))} + + +); + +function SearchListInner( + { + items, + loading, + error, + forceMode, + errorMessage = "Error Fetching Data", + noDataMessage = "No Data Found", + renderTableRow: TableRow, + renderListItemContent: ListItemContent, + renderThead: RenderThead, + onFetchNextPage, + ...sheetProps + }: SearchListProps, + ref: ForwardedRef, +) { + const isFetchingMore = useRef(false); + const theme = useTheme(); + const isMobileMq = useMediaQuery(theme.breakpoints.down("md")); + const hasNoResults = !error && !loading && !items.length; + const hasResults = !error && !loading && !!items.length; + + const isMobile = (() => { + if (forceMode && forceMode === "desktop") return false; + if (forceMode === "mobile") return true; + return isMobileMq; + })(); + + const onScroll = (e: React.UIEvent) => { + const target = e.target as HTMLUListElement; + const { scrollHeight, scrollTop, clientHeight } = target; + const distance = scrollHeight - scrollTop - clientHeight; + if (distance < 500 && !loading && !isFetchingMore.current) { + isFetchingMore.current = true; + onFetchNextPage?.().finally(() => { + isFetchingMore.current = false; + }); + } + }; + + return ( + + + }> + {loading && } + {error && } + {hasNoResults && } + {hasResults && !isMobile && ( + + + + {items.map((v) => ( + + ))} + +
+ )} + {hasResults && isMobile && ( + + {items.map((v) => ( + + + + + + + ))} + + )} +
+
+
+ ); +} + +const ForwardRefSearchList = forwardRef(SearchListInner); +ForwardRefSearchList.displayName = "SearchList"; + +export const SearchList = forwardRef(SearchListInner) as typeof SearchListInner; diff --git a/libs/react/components/src/test-preview/index.ts b/libs/react/components/src/test-preview/index.ts deleted file mode 100644 index 7a52dc600..000000000 --- a/libs/react/components/src/test-preview/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { TestPreview } from "./test-preview"; -export type { TestPreviewProps } from "./test-preview"; diff --git a/libs/react/components/src/test-preview/test-preview.stories.tsx b/libs/react/components/src/test-preview/test-preview.stories.tsx deleted file mode 100644 index 00f9ccf2c..000000000 --- a/libs/react/components/src/test-preview/test-preview.stories.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Grid, Link } from "@mui/joy"; -import { DateTime } from "luxon"; -import { TestPreview } from "./test-preview"; -import type { Meta, StoryObj } from "@storybook/react"; - -type Story = StoryObj; - -export const Playground: Story = { - args: { - title: "010 - Air Law", - status: "finished", - score: 80, - epochTimeInMs: DateTime.local().toMillis(), - timeToCompleteInMs: 259000, - timeLeftInMs: 1242414, - numberOfQuestions: 100, - }, - argTypes: {}, -}; - -export const Overview: Story = { - ...Playground, - render: function Render(args) { - return ( - - - - - - - - - - - - - - - - - - - - - ); - }, -}; - -const meta: Meta = { - title: "Components/TestPreview", - component: TestPreview, - tags: ["autodocs"], - argTypes: { - status: { - options: ["finished", "started", "created"], - control: { type: "radio" }, - }, - score: { - control: { - type: "range", - min: 0, - max: 100, - step: 1, - }, - }, - component: { - control: { type: "radio" }, - options: ["none", "Link"], - mapping: { - none: undefined, - Link: Link, - }, - }, - }, -}; - -export default meta; diff --git a/libs/react/components/src/test-preview/test-preview.tsx b/libs/react/components/src/test-preview/test-preview.tsx deleted file mode 100644 index e985bad5b..000000000 --- a/libs/react/components/src/test-preview/test-preview.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { forwardRef } from "react"; -import { Box, Sheet, Typography, CircularProgress } from "@mui/joy"; -import { DateTime } from "luxon"; -import { getClockTime } from "../utils/get-clock-time"; -import type { SheetProps } from "@mui/joy"; - -export type TestPreviewProps = { - status?: "finished" | "started" | "created"; - title?: string; - score?: number; - epochTimeInMs?: number; - timeLeftInMs?: number; - timeToCompleteInMs?: number; - numberOfQuestions?: number; - - /** - * this component is used as a link but the component override is not - * working. This prop has no effect unless you pass `component={Link}` - */ - href?: string; -} & Pick; - -export const TestPreview = forwardRef( - ( - { - title, - status = "finished", - score = 0, - epochTimeInMs, - timeLeftInMs, - timeToCompleteInMs, - numberOfQuestions, - ...props - }, - ref, - ) => { - const color = ((): "warning" | "success" | "danger" | "primary" => { - if (status === "created") return "primary"; - if (status === "started") return "warning"; - if (score >= 75) return "success"; - return "danger"; - })(); - - const boundedScore = Math.round(Math.min(100, Math.max(0, score))); - - return ( - - - {status === "finished" && ( - - )} - {status === "started" && ( - - )} - - - {title && ( - - {title} - - )} - {status === "finished" && timeToCompleteInMs && ( - - {`Completed in ${getClockTime( - timeToCompleteInMs / 1000, - )} minutes`} - - )} - {status !== "finished" && timeLeftInMs && ( - - {`Time Left: ${getClockTime(timeLeftInMs / 1000)} minutes`} - - )} - - {[ - epochTimeInMs && - DateTime.fromMillis(epochTimeInMs).toFormat("DDD"), - numberOfQuestions && `${numberOfQuestions} Questions`, - ] - .filter(Boolean) - .join(" | ")} - - - - ); - }, -); - -TestPreview.displayName = "TestPreview"; diff --git a/libs/react/components/tsconfig.json b/libs/react/components/tsconfig.json index 3902925c1..35c83e282 100644 --- a/libs/react/components/tsconfig.json +++ b/libs/react/components/tsconfig.json @@ -4,6 +4,6 @@ "files": ["../../../node_modules/@nx/next/typings/image.d.ts"], "compilerOptions": { "lib": ["dom"], - "types": ["vitest/globals", "@testing-library/jest-dom"] - } + "types": ["vitest/globals", "@testing-library/jest-dom"], + }, } diff --git a/libs/react/containers/src/index.ts b/libs/react/containers/src/index.ts index a6101d8af..71f0222e2 100644 --- a/libs/react/containers/src/index.ts +++ b/libs/react/containers/src/index.ts @@ -1,20 +1,20 @@ -export * from "./flashcards/flashcard-test"; -export * from "./flashcards/flashcard-list"; export * from "./flashcards/flashcard-collection-list"; +export * from "./flashcards/flashcard-list"; +export * from "./flashcards/flashcard-test"; +export * from "./layouts/layout-module"; +export * from "./layouts/layout-public"; +export * from "./learning-objectives/learning-objective-overview"; +export * from "./learning-objectives/learning-objective-questions"; +export * from "./learning-objectives/learning-objectives-search"; +export * from "./overviews/overview-module"; +export * from "./overviews/overview-modules"; +export * from "./overviews/overview-welcome"; export * from "./questions/question-editor"; export * from "./questions/question-overview"; export * from "./questions/question-search"; -export * from "./tests/test-study"; export * from "./tests/test-exam"; export * from "./tests/test-maker"; export * from "./tests/test-review"; -export * from "./tests/tests-overview"; -export * from "./layouts/layout-module"; -export * from "./layouts/layout-public"; -export * from "./learning-objectives/learning-objectives-search"; -export * from "./learning-objectives/learning-objective-overview"; -export * from "./learning-objectives/learning-objective-questions"; -export * from "./overviews/overview-welcome"; -export * from "./overviews/overview-modules"; -export * from "./overviews/overview-module"; +export * from "./tests/test-search"; +export * from "./tests/test-study"; export * from "./user/user-settings"; diff --git a/libs/react/containers/src/learning-objectives/learning-objective-questions/learning-objective-questions.tsx b/libs/react/containers/src/learning-objectives/learning-objective-questions/learning-objective-questions.tsx index 1ca32ea3d..44012228f 100644 --- a/libs/react/containers/src/learning-objectives/learning-objective-questions/learning-objective-questions.tsx +++ b/libs/react/containers/src/learning-objectives/learning-objective-questions/learning-objective-questions.tsx @@ -35,7 +35,7 @@ export const LearningObjectiveQuestions = container( p.items)} + items={(data?.pages ?? []).flatMap((p) => p.items)} component={component} onFetchNextPage={fetchNextPage} sx={sx} diff --git a/libs/react/containers/src/questions/question-search/question-search.tsx b/libs/react/containers/src/questions/question-search/question-search.tsx index ad0c0b159..14911701e 100644 --- a/libs/react/containers/src/questions/question-search/question-search.tsx +++ b/libs/react/containers/src/questions/question-search/question-search.tsx @@ -54,7 +54,7 @@ export const QuestionSearch = container( Number(searchField !== "all") + Number(subject !== "all"); return ( - + ( p.items)} + items={(data?.pages ?? []).flatMap((p) => p.items)} onFetchNextPage={fetchNextPage} sx={{ flex: 1, overflow: "hidden" }} /> diff --git a/libs/react/containers/src/tests/hooks/use-test-progress/use-test-progress.ts b/libs/react/containers/src/tests/hooks/use-test-progress/use-test-progress.ts index 0b1549854..b4086b3f1 100644 --- a/libs/react/containers/src/tests/hooks/use-test-progress/use-test-progress.ts +++ b/libs/react/containers/src/tests/hooks/use-test-progress/use-test-progress.ts @@ -18,6 +18,7 @@ type TestProgress = { goToPreviousQuestion: (args: { testId: string }) => void; tickTestTimer: (args: { testId: string; timeSpentInMs: number }) => void; finishTest: (args: { testId: string }) => void; + deleteTest: (args: { testId: string }) => void; }; export const useTestProgress = create()( @@ -146,6 +147,12 @@ export const useTestProgress = create()( }; set({ tests: { ...get().tests, [testId]: newTest } }); }, + deleteTest: ({ testId }) => { + const newTests = { ...get().tests }; + delete newTests[testId]; + + set({ tests: { ...newTests } }); + }, }), { name: "cf-test-progress" }, ), diff --git a/libs/react/containers/src/tests/test-search/index.ts b/libs/react/containers/src/tests/test-search/index.ts new file mode 100644 index 000000000..6fcebc252 --- /dev/null +++ b/libs/react/containers/src/tests/test-search/index.ts @@ -0,0 +1 @@ +export { TestSearch } from "./test-search"; diff --git a/libs/react/containers/src/tests/test-search/test-search.stories.tsx b/libs/react/containers/src/tests/test-search/test-search.stories.tsx new file mode 100644 index 000000000..207b6d210 --- /dev/null +++ b/libs/react/containers/src/tests/test-search/test-search.stories.tsx @@ -0,0 +1,30 @@ +import { TestSearch } from "./test-search"; +import type { Meta, StoryObj } from "@storybook/react"; + +type Story = StoryObj; + +export const Playground: Story = {}; + +const meta: Meta = { + title: "Containers/Test/TestSearch", + component: TestSearch, + tags: ["autodocs"], + args: { + questionBank: "atpl", + }, + argTypes: { + questionBank: { control: false }, + }, + parameters: { + docs: { + story: { + height: "500px", + }, + }, + msw: { + handlers: [], + }, + }, +}; + +export default meta; diff --git a/libs/react/containers/src/tests/test-search/test-search.tsx b/libs/react/containers/src/tests/test-search/test-search.tsx new file mode 100644 index 000000000..08a516dbe --- /dev/null +++ b/libs/react/containers/src/tests/test-search/test-search.tsx @@ -0,0 +1,222 @@ +import { default as DeleteIcon } from "@mui/icons-material/DeleteOutlineOutlined"; +import { default as PlayIcon } from "@mui/icons-material/PlayArrowOutlined"; +import { default as EyeIcon } from "@mui/icons-material/VisibilityOutlined"; +import { + Box, + Button, + CircularProgress, + IconButton, + Link, + ListItemContent, + Stack, + Tooltip, + Typography, +} from "@mui/joy"; +import { processTest } from "@chair-flight/core/app"; +import { SearchList } from "@chair-flight/react/components"; +import { container } from "../../wraper/container"; +import { useTestProgress } from "../hooks/use-test-progress"; +import type { QuestionBankName } from "@chair-flight/base/types"; + +type Props = { + questionBank: QuestionBankName; +}; + +export const TestSearch = container( + ({ questionBank, sx, component = "section" }) => { + const tests = useTestProgress((s) => s.tests); + const deleteTest = useTestProgress((s) => s.deleteTest); + const testsAsList = Object.values(tests) + .sort((a, b) => b.createdAtEpochMs - a.createdAtEpochMs) + .filter((test) => test.questionBank === questionBank) + .map((test) => ({ ...test, ...processTest(test) })); + + return ( + + + + + ( + + + Score + Title + Type + + No. Questions + + Time Left + + Time Spent + + + Start Date + + + + + )} + renderTableRow={(test) => ( + + + {test.status === "finished" && ( + + )} + {test.status === "started" && ( + + )} + + + {test.title} + + {test.mode} + + {test.questions.length} + + + {test.timeLeft} + + + {test.timeSpent} + + + {test.timeStarted} + + + {test.status === "finished" ? ( + + + + + + ) : ( + + + + + + )} + + deleteTest({ testId: test.id })}> + + + + + + )} + renderListItemContent={(test) => ( + + {test.status === "finished" && ( + + )} + {test.status === "started" && ( + + )} + + {test.title && ( + + {test.title} + + )} + {test.status === "finished" && ( + + {`Completed in ${test.timeSpent} minutes`} + + )} + {test.status !== "finished" && test.mode === "exam" && ( + + {`Time Left: ${test.timeLeft} minutes`} + + )} + {test.status !== "finished" && test.mode === "study" && ( + + {`Time Spent: ${test.timeSpent} minutes`} + + )} + + {[test.timeStarted, `${test.questions.length} Questions`] + .filter(Boolean) + .join(" | ")} + + + + {test.status === "finished" ? ( + + + + + + ) : ( + + + + + + )} + + deleteTest({ testId: test.id })}> + + + + + + )} + /> + + ); + }, +); + +TestSearch.displayName = "TestSearch"; +TestSearch.getData = async () => ({}); +TestSearch.useData = () => ({}); diff --git a/libs/react/containers/src/tests/tests-overview/index.ts b/libs/react/containers/src/tests/tests-overview/index.ts deleted file mode 100644 index 12221ac02..000000000 --- a/libs/react/containers/src/tests/tests-overview/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TestsOverview } from "./tests-overview"; diff --git a/libs/react/containers/src/tests/tests-overview/tests-overview.tsx b/libs/react/containers/src/tests/tests-overview/tests-overview.tsx deleted file mode 100644 index d2251eccc..000000000 --- a/libs/react/containers/src/tests/tests-overview/tests-overview.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Fragment } from "react"; -import { NoSsr } from "@mui/base"; -import { Box, Divider, Grid, Link, Stack, Typography } from "@mui/joy"; -import { TestPreview, Ups } from "@chair-flight/react/components"; -import { container } from "../../wraper/container"; -import { useTestProgress } from "../hooks/use-test-progress"; -import type { QuestionBankName } from "@chair-flight/base/types"; - -type Props = { - questionBank: QuestionBankName; -}; - -export const TestsOverview = container( - ({ questionBank, sx, component = "section" }) => { - const createTestHref = `/modules/${questionBank}/tests/create`; - const tests = useTestProgress((s) => s.tests); - const testsAsList = Object.values(tests) - .sort((a, b) => b.createdAtEpochMs - a.createdAtEpochMs) - .filter((test) => test.questionBank === questionBank); - - const entries = [ - { - title: "In Progress tests", - items: testsAsList.filter((test) => test.status !== "finished"), - noItemsMessage: ( - - No tests in progress. You can{" "} - Create a New Test! - - ), - topRightCorner: Create New Test, - }, - { - title: "Completed tests", - items: testsAsList.filter((test) => test.status === "finished"), - noItemsMessage: "No tests completed so far", - }, - ]; - - return ( - - {entries.map(({ title, items, noItemsMessage, topRightCorner }) => ( - - - {title} - {topRightCorner} - - - - - {items.map((test) => ( - - - s + - (q.selectedOptionId === q.correctOptionId ? 1 : 0), - 0, - ) / - test.questions.length) * - 100 - } - /> - - ))} - {items.length === 0 && ( - - )} - - - - ))} - - ); - }, -); - -TestsOverview.displayName = "TestsOverview"; -TestsOverview.getData = async () => ({}); -TestsOverview.useData = () => ({}); diff --git a/libs/react/containers/tsconfig.json b/libs/react/containers/tsconfig.json index 3902925c1..35c83e282 100644 --- a/libs/react/containers/tsconfig.json +++ b/libs/react/containers/tsconfig.json @@ -4,6 +4,6 @@ "files": ["../../../node_modules/@nx/next/typings/image.d.ts"], "compilerOptions": { "lib": ["dom"], - "types": ["vitest/globals", "@testing-library/jest-dom"] - } + "types": ["vitest/globals", "@testing-library/jest-dom"], + }, } diff --git a/libs/react/games/tsconfig.json b/libs/react/games/tsconfig.json index ec8473e0a..79c8eaa06 100644 --- a/libs/react/games/tsconfig.json +++ b/libs/react/games/tsconfig.json @@ -3,6 +3,6 @@ "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], "compilerOptions": { - "lib": ["dom"] - } + "lib": ["dom"], + }, } diff --git a/libs/trpc/client/tsconfig.json b/libs/trpc/client/tsconfig.json index 662314246..462a40a59 100644 --- a/libs/trpc/client/tsconfig.json +++ b/libs/trpc/client/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "lib": ["dom"] + "lib": ["dom"], }, - "include": ["src/**/*.ts", "src/**/*.tsx", "../mock/src/trpc-mock.tsx"] + "include": ["src/**/*.ts", "src/**/*.tsx", "../mock/src/trpc-mock.tsx"], } diff --git a/libs/trpc/mock/tsconfig.json b/libs/trpc/mock/tsconfig.json index c207a17f4..0b4b1fa7f 100644 --- a/libs/trpc/mock/tsconfig.json +++ b/libs/trpc/mock/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": {}, - "include": ["src/**/*.ts", "src/**/*.tsx"] + "include": ["src/**/*.ts", "src/**/*.tsx"], } diff --git a/libs/trpc/server/tsconfig.json b/libs/trpc/server/tsconfig.json index 008a16072..dbaab529c 100644 --- a/libs/trpc/server/tsconfig.json +++ b/libs/trpc/server/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "lib": ["dom"] + "lib": ["dom"], }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], } diff --git a/package.json b/package.json index c6b4deff4..272c34080 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "typecheck": "nx run-many --target=typecheck", "infra": "docker-compose up -d", "prettier": "prettier . --write", + "prettier:check": "prettier . --check", "preinstall": "npx only-allow pnpm", "prepare": "husky install" },