diff --git a/package-lock.json b/package-lock.json index e1748f5b..6d6aa887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@fontsource/noto-sans-arabic": "5.2.10", "@fortawesome/fontawesome-svg-core": "6.6.0", "@fortawesome/free-brands-svg-icons": "6.6.0", + "@fortawesome/free-regular-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/react-fontawesome": "0.2.2", "@google-cloud/translate": "9.3.0", @@ -3251,6 +3252,19 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", + "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", + "dev": true, + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "6.6.0", "dev": true, diff --git a/package.json b/package.json index c3a38126..3c60200e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@fontsource/noto-sans-arabic": "5.2.10", "@fortawesome/fontawesome-svg-core": "6.6.0", "@fortawesome/free-brands-svg-icons": "6.6.0", + "@fortawesome/free-regular-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/react-fontawesome": "0.2.2", "@google-cloud/translate": "9.3.0", diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index b19c96f7..e234cc88 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -1,5 +1,6 @@ import { config } from "@fortawesome/fontawesome-svg-core"; import * as FaSolid from "@fortawesome/free-solid-svg-icons"; +import * as FaRegular from "@fortawesome/free-regular-svg-icons"; import * as FaBrands from "@fortawesome/free-brands-svg-icons"; import { FontAwesomeIcon, @@ -30,6 +31,8 @@ const iconMap = { "chevron-down": FaSolid.faChevronDown, "chevron-up": FaSolid.faChevronUp, "chart-line": FaSolid.faChartLine, + circle: FaSolid.faCircle, + "circle-hollow": FaRegular.faCircle, "circle-play": FaSolid.faCirclePlay, close: FaSolid.faXmark, database: FaSolid.faDatabase, diff --git a/src/messages/ar.json b/src/messages/ar.json index 2e7d56b3..d426436e 100644 --- a/src/messages/ar.json +++ b/src/messages/ar.json @@ -463,6 +463,12 @@ "title": "اختر الفصل", "reference": "مرجع الفصل", "back_to_books": "العودة إلى الأسفار", + "book_column": "السفر", + "glosses_column": "اللمعات", + "status_complete": "مكتمل", + "status_none": "لا شيء", + "status_many": "كثير", + "status_some": "بعض", "invalid": "يرجى إدخال مرجع فصل صحيح", "cancel": "إلغاء", "go": "اذهب", diff --git a/src/messages/en.json b/src/messages/en.json index cce2a83c..7df6ddd6 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -445,6 +445,12 @@ "title": "Choose chapter", "reference": "Chapter reference", "back_to_books": "Back to books", + "book_column": "Book", + "glosses_column": "Glosses", + "status_complete": "Complete", + "status_none": "None", + "status_many": "Many", + "status_some": "Some", "invalid": "Please enter a valid chapter reference", "cancel": "Cancel", "go": "Go", diff --git a/src/ui/study/components/ChapterPickerDialog.tsx b/src/ui/study/components/ChapterPickerDialog.tsx index f86ae19e..3e9c6beb 100644 --- a/src/ui/study/components/ChapterPickerDialog.tsx +++ b/src/ui/study/components/ChapterPickerDialog.tsx @@ -11,15 +11,18 @@ import { } from "@/verse-utils"; import { SubmitEvent, useEffect, useMemo, useRef, useState } from "react"; import { useTranslations } from "use-intl"; +import { ProgressByBookIdReadModel } from "../readModels/getReadBookProgressReadModel"; interface ChapterPickerDialogProps { chapterId: string; + progressByBookId: ProgressByBookIdReadModel; onCancel(): void; onSubmit(chapterId: string): void; } export default function ChapterPickerDialog({ chapterId, + progressByBookId, onCancel, onSubmit, }: ChapterPickerDialogProps) { @@ -159,24 +162,77 @@ export default function ChapterPickerDialog({
- :
    - {options.books.map((book) => { - return ( -
  1. - -
  2. - ); - })} -
+ +
+ {progress.approvedWords === progress.totalWords ? + <> + + {t("status_complete")} + + : progress.approvedWords === 0 ? + <> + + {t("status_none")} + + : progress.approvedWords / progress.totalWords > 0.8 ? + <> + + {t("status_many")} + + : <> + + {t("status_some")} + + } +
+ + ); + })} + +
}
diff --git a/src/ui/study/components/CommandInput.tsx b/src/ui/study/components/CommandInput.tsx index 1d9e1451..053e2aa7 100644 --- a/src/ui/study/components/CommandInput.tsx +++ b/src/ui/study/components/CommandInput.tsx @@ -11,8 +11,13 @@ import { } from "@/verse-utils"; import { useNavigate, useParams } from "@tanstack/react-router"; import ChapterPickerDialog from "./ChapterPickerDialog"; +import { ProgressByBookIdReadModel } from "../readModels/getReadBookProgressReadModel"; -export default function CommandInput() { +export default function CommandInput({ + progressByBookId, +}: { + progressByBookId: ProgressByBookIdReadModel; +}) { const { chapterId, code: languageCode } = useParams({ from: "/_main/read/$code/$chapterId", }); @@ -123,6 +128,7 @@ export default function CommandInput() { {openPicker && ( setOpenPicker(false)} onSubmit={async (nextChapterId) => { await navigate({ diff --git a/src/ui/study/components/ReadingToolbar.tsx b/src/ui/study/components/ReadingToolbar.tsx index 10c832c1..a8b7d081 100644 --- a/src/ui/study/components/ReadingToolbar.tsx +++ b/src/ui/study/components/ReadingToolbar.tsx @@ -10,14 +10,17 @@ import SettingsMenu from "./SettingsMenu"; import CommandInput from "./CommandInput"; import { useFlash } from "@/flash"; import { useNavigate, useParams } from "@tanstack/react-router"; +import { ProgressByBookIdReadModel } from "../readModels/getReadBookProgressReadModel"; export interface TranslationToolbarProps { languages: { englishName: string; localName: string; code: string }[]; + progressByBookId: ProgressByBookIdReadModel; children: ReactNode; } export default function ReadingToolbar({ languages, + progressByBookId, children, }: TranslationToolbarProps) { const t = useTranslations("ReadingToolbar"); @@ -48,7 +51,7 @@ export default function ReadingToolbar({ shadow-md dark:shadow-none dark:border-b dark:border-gray-700 bg-white dark:bg-gray-900 " > - + ({ diff --git a/src/ui/study/readModels/getReadBookProgressReadModel.ts b/src/ui/study/readModels/getReadBookProgressReadModel.ts new file mode 100644 index 00000000..b602d58e --- /dev/null +++ b/src/ui/study/readModels/getReadBookProgressReadModel.ts @@ -0,0 +1,60 @@ +import { getDb } from "@/db"; + +export interface BookProgressReadModel { + totalWords: number; + approvedWords: number; +} + +export type ProgressByBookIdReadModel = Record; + +export async function getReadBookProgressReadModel( + code: string, +): Promise { + const language = await getDb() + .selectFrom("language") + .select("id") + .where("code", "=", code) + .executeTakeFirst(); + + if (!language) { + return {}; + } + + const bookProgressRows = await getDb() + .with("word_count", (db) => + db + .selectFrom("book_word_map") + .groupBy("book_id") + .select(["book_id", (eb) => eb.fn.countAll().as("count")]), + ) + .with("book_progress", (db) => + db + .selectFrom("book_completion_progress") + .where("language_id", "=", language.id) + .groupBy("book_id") + .select([ + "book_id", + (eb) => eb.fn.sum("word_count").as("count"), + ]), + ) + .selectFrom("book") + .innerJoin("word_count", "word_count.book_id", "book.id") + .leftJoin("book_progress", "book_progress.book_id", "book.id") + .select([ + "book.id as bookId", + "word_count.count as totalWords", + (eb) => + eb.fn.coalesce("book_progress.count", eb.lit(0)).as("approvedWords"), + ]) + .execute(); + + const progressByBookId: ProgressByBookIdReadModel = {}; + for (const row of bookProgressRows) { + progressByBookId[row.bookId.toString()] = { + totalWords: row.totalWords, + approvedWords: row.approvedWords, + }; + } + + return progressByBookId; +} diff --git a/src/ui/study/routes/$code.tsx b/src/ui/study/routes/$code.tsx index 3e852959..678af98d 100644 --- a/src/ui/study/routes/$code.tsx +++ b/src/ui/study/routes/$code.tsx @@ -8,10 +8,10 @@ export const Route = createFileRoute("/_main/read/$code")({ }); function ReadingLayout() { - const { languages } = Route.useLoaderData(); + const { languages, progressByBookId } = Route.useLoaderData(); return ( - + ); diff --git a/src/ui/study/serverFns/getReadLayoutData.ts b/src/ui/study/serverFns/getReadLayoutData.ts index 89167bec..041a1906 100644 --- a/src/ui/study/serverFns/getReadLayoutData.ts +++ b/src/ui/study/serverFns/getReadLayoutData.ts @@ -1,6 +1,7 @@ import { createServerFn } from "@tanstack/react-start"; import * as z from "zod"; import { getReadLanguagesReadModel } from "../readModels/getReadLanguagesReadModel"; +import { getReadBookProgressReadModel } from "../readModels/getReadBookProgressReadModel"; const requestSchema = z.object({ code: z.string(), @@ -8,8 +9,9 @@ const requestSchema = z.object({ export const getReadLayoutData = createServerFn({ method: "GET" }) .inputValidator((input: unknown) => requestSchema.parse(input)) - .handler(async () => { + .handler(async ({ data }) => { const languages = await getReadLanguagesReadModel(); + const progressByBookId = await getReadBookProgressReadModel(data.code); - return { languages }; + return { languages, progressByBookId }; });