From cb3b65006af723fcc055117f7f44d5608ca85d1f Mon Sep 17 00:00:00 2001 From: Egor Gorbachev <7gorbachevm@gmail.com> Date: Sun, 12 May 2024 19:18:12 +0700 Subject: [PATCH] Card input mode (#39) * Card input mode --- src/api/api.ts | 31 +++++ src/lib/platform/browser/browser-platform.ts | 2 +- ...ess-browser.ts => use-progress-browser.ts} | 2 +- src/lib/platform/platform.ts | 2 +- .../platform/telegram/telegram-platform.ts | 2 +- ...telegram.tsx => use-progress-telegram.tsx} | 2 +- src/lib/platform/use-main-button-progress.tsx | 9 -- src/lib/platform/use-progress.tsx | 9 ++ src/lib/request/request.ts | 2 +- src/main.tsx | 3 + .../ai-mass-creation-form.tsx | 4 +- .../ai-mass-creation/api-keys-screen.tsx | 4 +- .../cards-generated-screen.tsx | 4 +- .../generated-card-preview-screen.tsx | 4 +- .../previous-prompts-screen.tsx | 8 +- .../store/ai-mass-creation-store.ts | 9 +- src/screens/ai-mass-creation/translations.ts | 4 +- src/screens/app.tsx | 12 +- .../card-input-mode-screen.tsx | 59 +++++++++ .../store/card-input-mode-store.ts | 65 +++++++++ .../component-catalog/card-preview-story.tsx | 4 +- src/screens/component-catalog/components.tsx | 5 + .../component-catalog/radio-list-story.tsx | 31 +++++ src/screens/deck-catalog/deck-catalog.tsx | 4 +- .../{ => card-form}/answer-form-view.tsx | 26 ++-- .../{ => card-form}/card-ai-speech.tsx | 36 ++--- .../{ => card-form}/card-answer-errors.tsx | 4 +- .../{ => card-form}/card-example.tsx | 20 +-- .../deck-form/card-form/card-form-view.tsx | 24 ++++ .../{ => card-form}/card-form-wrapper.tsx | 4 +- .../{ => card-form}/card-preview.tsx | 8 +- .../deck-form/{ => card-form}/card-type.tsx | 25 ++-- .../create-mock-card-preview-form.ts | 10 +- .../{ => card-form}/format-card-type.ts | 4 +- .../{ => card-form}/formatting-switcher.tsx | 10 +- .../card-form/generated-card-form-view.tsx | 93 +++++++++++++ .../manual-card-form-view.tsx} | 55 ++++---- .../quick-add-card-form-page.tsx | 7 +- .../store/ai-generated-card-form-store.ts | 71 ++++++++++ .../store/ai-speech-generator-store.ts | 14 +- .../store/quick-add-card-form-store.ts | 38 +++--- .../deck-form/{ => deck-form}/card-list.tsx | 30 ++--- .../{ => deck-form}/deck-form-screen.tsx | 4 +- .../deck-form/{ => deck-form}/deck-form.tsx | 104 +++++++++------ .../{ => deck-form}/speaking-cards.tsx | 40 +++--- .../store/card-form-store-interface.ts | 13 +- .../store/deck-form-store-context.tsx | 2 +- .../store/deck-form-store.test.ts | 55 ++++---- .../{ => deck-form}/store/deck-form-store.ts | 125 ++++++++++-------- src/screens/deck-list/main-screen.tsx | 6 +- src/screens/deck-review/deck-finished.tsx | 4 +- src/screens/deck-review/deck-preview.tsx | 4 +- .../deck-review/store/card-preview-store.ts | 5 +- .../deck-review/store/review-store.test.ts | 1 + src/screens/folder-form/folder-form.tsx | 4 +- src/screens/folder-review/folder-preview.tsx | 4 +- .../freeze-cards/freeze-cards-screen.tsx | 4 +- src/screens/plans/plans-screen.tsx | 4 +- .../share-deck/share-deck-settings.tsx | 4 +- ...{deck-loading.tsx => card-row-loading.tsx} | 2 +- .../user-settings/user-settings-screen.tsx | 4 +- .../user-statistics-screen.tsx | 8 +- src/store/deck-list-store.test.ts | 2 + src/store/deck-list-store.ts | 28 +++- src/store/screen-store.ts | 16 ++- src/translations/t.ts | 12 ++ src/ui/radio-list/radio-box-empty.tsx | 17 +++ src/ui/radio-list/radio-box-filled.tsx | 17 +++ src/ui/radio-list/radio-list.tsx | 47 +++++++ 69 files changed, 930 insertions(+), 366 deletions(-) rename src/lib/platform/browser/{use-main-button-progress-browser.ts => use-progress-browser.ts} (90%) rename src/lib/platform/telegram/{use-main-button-progress-telegram.tsx => use-progress-telegram.tsx} (81%) delete mode 100644 src/lib/platform/use-main-button-progress.tsx create mode 100644 src/lib/platform/use-progress.tsx create mode 100644 src/screens/card-input-mode/card-input-mode-screen.tsx create mode 100644 src/screens/card-input-mode/store/card-input-mode-store.ts create mode 100644 src/screens/component-catalog/radio-list-story.tsx rename src/screens/deck-form/{ => card-form}/answer-form-view.tsx (77%) rename src/screens/deck-form/{ => card-form}/card-ai-speech.tsx (68%) rename src/screens/deck-form/{ => card-form}/card-answer-errors.tsx (78%) rename src/screens/deck-form/{ => card-form}/card-example.tsx (60%) create mode 100644 src/screens/deck-form/card-form/card-form-view.tsx rename src/screens/deck-form/{ => card-form}/card-form-wrapper.tsx (91%) rename src/screens/deck-form/{ => card-form}/card-preview.tsx (82%) rename src/screens/deck-form/{ => card-form}/card-type.tsx (81%) rename src/screens/deck-form/{ => card-form}/create-mock-card-preview-form.ts (71%) rename src/screens/deck-form/{ => card-form}/format-card-type.ts (81%) rename src/screens/deck-form/{ => card-form}/formatting-switcher.tsx (74%) create mode 100644 src/screens/deck-form/card-form/generated-card-form-view.tsx rename src/screens/deck-form/{card-form-view.tsx => card-form/manual-card-form-view.tsx} (78%) rename src/screens/deck-form/{ => card-form}/quick-add-card-form-page.tsx (77%) create mode 100644 src/screens/deck-form/card-form/store/ai-generated-card-form-store.ts rename src/screens/deck-form/{ => card-form}/store/ai-speech-generator-store.ts (82%) rename src/screens/deck-form/{ => card-form}/store/quick-add-card-form-store.ts (68%) rename src/screens/deck-form/{ => deck-form}/card-list.tsx (78%) rename src/screens/deck-form/{ => deck-form}/deck-form-screen.tsx (85%) rename src/screens/deck-form/{ => deck-form}/deck-form.tsx (56%) rename src/screens/deck-form/{ => deck-form}/speaking-cards.tsx (59%) rename src/screens/deck-form/{ => deck-form}/store/card-form-store-interface.ts (66%) rename src/screens/deck-form/{ => deck-form}/store/deck-form-store-context.tsx (89%) rename src/screens/deck-form/{ => deck-form}/store/deck-form-store.test.ts (86%) rename src/screens/deck-form/{ => deck-form}/store/deck-form-store.ts (79%) rename src/screens/shared/{deck-loading.tsx => card-row-loading.tsx} (94%) create mode 100644 src/ui/radio-list/radio-box-empty.tsx create mode 100644 src/ui/radio-list/radio-box-filled.tsx create mode 100644 src/ui/radio-list/radio-list.tsx diff --git a/src/api/api.ts b/src/api/api.ts index ae4e6432..770c9376 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -69,6 +69,15 @@ import { AiSpeechGenerateRequest, AiSpeechGenerateResponse, } from "../../functions/ai-speech-generate.ts"; +import { + AiSingleCardRequest, + AiSingleCardResponse, +} from "../../functions/ai-single-card-generate.ts"; +import { CardInputModeListResponse } from "../../functions/card-input-mode-list.ts"; +import { + CardInputModeChangeRequest, + CardInputModeChangeResponse, +} from "../../functions/card-input-mode-change.ts"; export const healthRequest = () => { return request("/health"); @@ -263,3 +272,25 @@ export const aiSpeechGenerateRequest = (body: AiSpeechGenerateRequest) => { body, ); }; + +export const aiSingleCardGenerateRequest = (body: AiSingleCardRequest) => { + return request( + "/ai-single-card-generate", + "POST", + body, + ); +}; + +export const cardInputModeListRequest = () => { + return request("/card-input-mode-list"); +}; + +export const deckChangeInputModeRequest = ( + body: CardInputModeChangeRequest, +) => { + return request( + "/card-input-mode-change", + "PUT", + body, + ); +}; diff --git a/src/lib/platform/browser/browser-platform.ts b/src/lib/platform/browser/browser-platform.ts index a8abc4e5..81f3fce1 100644 --- a/src/lib/platform/browser/browser-platform.ts +++ b/src/lib/platform/browser/browser-platform.ts @@ -2,7 +2,7 @@ import { Platform, PlatformTheme } from "../platform.ts"; import { makeAutoObservable } from "mobx"; import { assert } from "../../typescript/assert.ts"; import { BooleanToggle } from "mobx-form-lite"; -import { PlatformSchemaType } from "../../../../functions/services/get-user.ts"; +import { PlatformSchemaType } from "../../../../functions/services/get-telegram-user.ts"; import { Language } from "../../../translations/t.ts"; const cssVariables = { diff --git a/src/lib/platform/browser/use-main-button-progress-browser.ts b/src/lib/platform/browser/use-progress-browser.ts similarity index 90% rename from src/lib/platform/browser/use-main-button-progress-browser.ts rename to src/lib/platform/browser/use-progress-browser.ts index b0333e06..3bfbe51c 100644 --- a/src/lib/platform/browser/use-main-button-progress-browser.ts +++ b/src/lib/platform/browser/use-progress-browser.ts @@ -4,7 +4,7 @@ import { platform } from "../platform.ts"; import { assert } from "../../typescript/assert.ts"; import { BrowserPlatform } from "./browser-platform.ts"; -export const useMainButtonProgressBrowser = (cb: () => boolean) => { +export const useProgressBrowser = (cb: () => boolean) => { useMount(() => { return autorun(() => { if (cb()) { diff --git a/src/lib/platform/platform.ts b/src/lib/platform/platform.ts index e478cb9e..3163d69a 100644 --- a/src/lib/platform/platform.ts +++ b/src/lib/platform/platform.ts @@ -1,7 +1,7 @@ import { TelegramPlatform } from "./telegram/telegram-platform.ts"; import { BrowserPlatform } from "./browser/browser-platform.ts"; import { isRunningWithinTelegram } from "./is-running-within-telegram.ts"; -import { PlatformSchemaType } from "../../../functions/services/get-user.ts"; +import { PlatformSchemaType } from "../../../functions/services/get-telegram-user.ts"; import { Language } from "../../translations/t.ts"; export type PlatformTheme = { diff --git a/src/lib/platform/telegram/telegram-platform.ts b/src/lib/platform/telegram/telegram-platform.ts index f4260da7..936b5802 100644 --- a/src/lib/platform/telegram/telegram-platform.ts +++ b/src/lib/platform/telegram/telegram-platform.ts @@ -1,7 +1,7 @@ import WebApp from "@twa-dev/sdk"; import { Platform, PlatformTheme } from "../platform.ts"; import { cssVarToValue } from "./css-var-to-value.ts"; -import { PlatformSchemaType } from "../../../../functions/services/get-user.ts"; +import { PlatformSchemaType } from "../../../../functions/services/get-telegram-user.ts"; import { Language } from "../../../translations/t.ts"; const buttonColor = "var(--tg-theme-button-color)"; diff --git a/src/lib/platform/telegram/use-main-button-progress-telegram.tsx b/src/lib/platform/telegram/use-progress-telegram.tsx similarity index 81% rename from src/lib/platform/telegram/use-main-button-progress-telegram.tsx rename to src/lib/platform/telegram/use-progress-telegram.tsx index 0665b0db..6846be9f 100644 --- a/src/lib/platform/telegram/use-main-button-progress-telegram.tsx +++ b/src/lib/platform/telegram/use-progress-telegram.tsx @@ -2,7 +2,7 @@ import { useMount } from "../../react/use-mount.ts"; import { autorun } from "mobx"; import WebApp from "@twa-dev/sdk"; -export const useMainButtonProgressTelegram = (cb: () => boolean) => { +export const useProgressTelegram = (cb: () => boolean) => { return useMount(() => { return autorun(() => { if (cb()) { diff --git a/src/lib/platform/use-main-button-progress.tsx b/src/lib/platform/use-main-button-progress.tsx deleted file mode 100644 index a61bad3a..00000000 --- a/src/lib/platform/use-main-button-progress.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { platform } from "./platform.ts"; -import { TelegramPlatform } from "./telegram/telegram-platform.ts"; -import { useMainButtonProgressTelegram } from "./telegram/use-main-button-progress-telegram.tsx"; -import { useMainButtonProgressBrowser } from "./browser/use-main-button-progress-browser.ts"; - -export const useMainButtonProgress = - platform instanceof TelegramPlatform - ? useMainButtonProgressTelegram - : useMainButtonProgressBrowser; diff --git a/src/lib/platform/use-progress.tsx b/src/lib/platform/use-progress.tsx new file mode 100644 index 00000000..1424a535 --- /dev/null +++ b/src/lib/platform/use-progress.tsx @@ -0,0 +1,9 @@ +import { platform } from "./platform.ts"; +import { TelegramPlatform } from "./telegram/telegram-platform.ts"; +import { useProgressTelegram } from "./telegram/use-progress-telegram.tsx"; +import { useProgressBrowser } from "./browser/use-progress-browser.ts"; + +export const useProgress = + platform instanceof TelegramPlatform + ? useProgressTelegram + : useProgressBrowser; diff --git a/src/lib/request/request.ts b/src/lib/request/request.ts index 85346966..daea1825 100644 --- a/src/lib/request/request.ts +++ b/src/lib/request/request.ts @@ -1,7 +1,7 @@ import { trimEnd, trimStart } from "../string/trim.ts"; import { platform } from "../platform/platform.ts"; import { collectClientData } from "./collect-client-data.ts"; -import { UserHeaders } from "../../../functions/services/get-user.ts"; +import { UserHeaders } from "../../../functions/services/get-telegram-user.ts"; const baseUrl = import.meta.env.VITE_API_URL || ""; diff --git a/src/main.tsx b/src/main.tsx index 404170ae..2ff947c3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,6 +4,9 @@ import { App } from "./screens/app.tsx"; import "./index.css"; import "@mdi/font/css/materialdesignicons.min.css"; import { platform } from "./lib/platform/platform.ts"; +import { applyFormatters } from "mobx-log"; + +applyFormatters(); platform.initialize(); diff --git a/src/screens/ai-mass-creation/ai-mass-creation-form.tsx b/src/screens/ai-mass-creation/ai-mass-creation-form.tsx index a5845268..fe51d159 100644 --- a/src/screens/ai-mass-creation/ai-mass-creation-form.tsx +++ b/src/screens/ai-mass-creation/ai-mass-creation-form.tsx @@ -12,7 +12,7 @@ import { Input } from "../../ui/input.tsx"; import React from "react"; import { ValidationError } from "../../ui/validation-error.tsx"; import { useMainButton } from "../../lib/platform/use-main-button.ts"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; import { useBackButton } from "../../lib/platform/use-back-button.ts"; import { screenStore } from "../../store/screen-store.ts"; @@ -28,7 +28,7 @@ export const AiMassCreationForm = observer(() => { screenStore.back(); }); - useMainButtonProgress(() => store.aiMassGenerateRequest.isLoading); + useProgress(() => store.aiMassGenerateRequest.isLoading); return ( diff --git a/src/screens/ai-mass-creation/api-keys-screen.tsx b/src/screens/ai-mass-creation/api-keys-screen.tsx index 9e04acef..20b1c415 100644 --- a/src/screens/ai-mass-creation/api-keys-screen.tsx +++ b/src/screens/ai-mass-creation/api-keys-screen.tsx @@ -14,7 +14,7 @@ import { theme } from "../../ui/theme.tsx"; import { Flex } from "../../ui/flex.tsx"; import { chatGptModels } from "./store/ai-mass-creation-store.ts"; import { useBackButton } from "../../lib/platform/use-back-button.ts"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; import { TextField } from "mobx-form-lite"; export const ApiKeysScreen = observer(() => { @@ -29,7 +29,7 @@ export const ApiKeysScreen = observer(() => { store.screen.onChange(null); }); - useMainButtonProgress(() => store.upsertUserAiCredentialsRequest.isLoading); + useProgress(() => store.upsertUserAiCredentialsRequest.isLoading); const isRegularInput = store.isApiKeyRegularInput; diff --git a/src/screens/ai-mass-creation/cards-generated-screen.tsx b/src/screens/ai-mass-creation/cards-generated-screen.tsx index 014a7622..03dab185 100644 --- a/src/screens/ai-mass-creation/cards-generated-screen.tsx +++ b/src/screens/ai-mass-creation/cards-generated-screen.tsx @@ -12,7 +12,7 @@ import { theme } from "../../ui/theme.tsx"; import React from "react"; import { t } from "../../translations/t.ts"; import { screenStore } from "../../store/screen-store.ts"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; import { CardNumber } from "../../ui/card-number.tsx"; import { translateAddCards } from "./translations.ts"; @@ -37,7 +37,7 @@ export const CardsGeneratedScreen = observer(() => { }, ); - useMainButtonProgress(() => store.addCardsMultipleRequest.isLoading); + useProgress(() => store.addCardsMultipleRequest.isLoading); return ( { const store = useAiMassCreationStore(); - const localStore = useLocalStore(() => ({ + const localStore = useLocalObservable(() => ({ selectedIndex: new TextField(null), get isMainButtonVisible() { return localStore.selectedIndex.value !== null; @@ -41,7 +41,7 @@ export const PreviousPromptsScreen = observer(() => { return ( - + {store.userPreviousPromptsRequest.isLoading && } {store.userPreviousPromptsRequest.result.status === "success" && store.userPreviousPromptsRequest.result.data.length === 0 && ( @@ -72,7 +72,7 @@ export const PreviousPromptsScreen = observer(() => { }), isSelected && css({ - border: `2px solid ${theme.buttonColor}`, + outline: `2px solid ${theme.buttonColor}`, }), )} key={i} diff --git a/src/screens/ai-mass-creation/store/ai-mass-creation-store.ts b/src/screens/ai-mass-creation/store/ai-mass-creation-store.ts index 684eb010..8e998670 100644 --- a/src/screens/ai-mass-creation/store/ai-mass-creation-store.ts +++ b/src/screens/ai-mass-creation/store/ai-mass-creation-store.ts @@ -270,10 +270,12 @@ export class AiMassCreationStore { return; } - assert(screenStore.screen.type === "aiMassCreation", "Invalid screen type"); + const { screen } = screenStore; + + assert(screen.type === "aiMassCreation", "Invalid screen type"); const result = await this.addCardsMultipleRequest.execute({ - deckId: screenStore.screen.deckId, + deckId: screen.deckId, cards: this.massCreationForm.cards.value, }); @@ -284,9 +286,10 @@ export class AiMassCreationStore { notifySuccess(t("ai_cards_added")); deckListStore.replaceDeck(result.data.deck); + deckListStore.updateCardsToReview(result.data.cardsToReview); screenStore.go({ type: "deckForm", - deckId: screenStore.screen.deckId, + deckId: screen.deckId, }); } } diff --git a/src/screens/ai-mass-creation/translations.ts b/src/screens/ai-mass-creation/translations.ts index bc60a77e..72c38163 100644 --- a/src/screens/ai-mass-creation/translations.ts +++ b/src/screens/ai-mass-creation/translations.ts @@ -76,13 +76,13 @@ export const translateHowMassCreationWorksText = () => { example2: "Пример 2", description: "Генерируйте множество карточек за раз с использованием ИИ и вашего собственного ключа API", - promptExample1: "Сгенерировать 3 карточки со столицами мира", + promptExample1: "Сгенерируй 3 карточки со столицами мира", frontExample1: "Страна", backExample1: "Столица", resultExample1: "Пример результата: Германия - Берлин, Франция - Париж, Канада - Оттава", promptExample2: - "Сгенерировать 2 карточки с русскими и французскими словами на тему фруктов", + "Сгенерируй 2 карточки с русскими и французскими словами на тему фруктов", frontExample2: "Фрукт на русском", backExample2: "Фрукт на французском", resultExample2: "Пример результата: Яблоко - Pomme, Банан - Banane", diff --git a/src/screens/app.tsx b/src/screens/app.tsx index 9803d5f3..b4cdd2b3 100644 --- a/src/screens/app.tsx +++ b/src/screens/app.tsx @@ -3,9 +3,9 @@ import { MainScreen } from "./deck-list/main-screen.tsx"; import { DeckScreen } from "./deck-review/deck-screen.tsx"; import { ReviewStoreProvider } from "./deck-review/store/review-store-context.tsx"; import { screenStore } from "../store/screen-store.ts"; -import { DeckFormScreen } from "./deck-form/deck-form-screen.tsx"; -import { DeckFormStoreProvider } from "./deck-form/store/deck-form-store-context.tsx"; -import { QuickAddCardFormPage } from "./deck-form/quick-add-card-form-page.tsx"; +import { DeckFormScreen } from "./deck-form/deck-form/deck-form-screen.tsx"; +import { DeckFormStoreProvider } from "./deck-form/deck-form/store/deck-form-store-context.tsx"; +import { QuickAddCardFormPage } from "./deck-form/card-form/quick-add-card-form-page.tsx"; import { VersionWarning } from "./shared/version-warning.tsx"; import React from "react"; import { deckListStore } from "../store/deck-list-store.ts"; @@ -40,6 +40,7 @@ import { SnackbarProviderWrapper } from "./shared/snackbar/snackbar-provider-wra import { Debug } from "./debug/debug.tsx"; import { BrowserHeader } from "./shared/browser-platform/browser-header.tsx"; import { BrowserMainButton } from "./shared/browser-platform/browser-main-button.tsx"; +import { CardInputModeScreen } from "./card-input-mode/card-input-mode-screen.tsx"; export const App = observer(() => { useRestoreFullScreenExpand(); @@ -169,6 +170,11 @@ export const App = observer(() => { )} + {screenStore.screen.type === "cardInputMode" && ( + + + + )} ); diff --git a/src/screens/card-input-mode/card-input-mode-screen.tsx b/src/screens/card-input-mode/card-input-mode-screen.tsx new file mode 100644 index 00000000..deecfacf --- /dev/null +++ b/src/screens/card-input-mode/card-input-mode-screen.tsx @@ -0,0 +1,59 @@ +import { observer } from "mobx-react-lite"; +import { Screen } from "../shared/screen.tsx"; +import { useBackButton } from "../../lib/platform/use-back-button.ts"; +import { screenStore } from "../../store/screen-store.ts"; +import { useState } from "react"; +import { CardInputModeStore } from "./store/card-input-mode-store.ts"; +import { useMount } from "../../lib/react/use-mount.ts"; +import { FullScreenLoader } from "../../ui/full-screen-loader.tsx"; +import { RadioList } from "../../ui/radio-list/radio-list.tsx"; +import { useMainButton } from "../../lib/platform/use-main-button.ts"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; +import { t } from "../../translations/t.ts"; + +export const CardInputModeScreen = observer(() => { + const [store] = useState(() => new CardInputModeStore()); + + useBackButton(() => { + screenStore.back(); + }); + + useMount(() => { + store.load(); + }); + + useMainButton(t("save"), () => { + store.submit(); + }); + + useProgress(() => { + return store.cardInputModesRequest.isLoading; + }); + + return ( + + {store.cardInputModesRequest.result.status === "loading" ? ( + + ) : null} + {store.cardInputModesRequest.result.status === "success" ? ( +
+ ({ + id: inputMode.id, + title: inputMode.title, + })), + )} + onChange={store.modeId.onChange} + /> +
+ ) : null} +
+ ); +}); diff --git a/src/screens/card-input-mode/store/card-input-mode-store.ts b/src/screens/card-input-mode/store/card-input-mode-store.ts new file mode 100644 index 00000000..f1886b51 --- /dev/null +++ b/src/screens/card-input-mode/store/card-input-mode-store.ts @@ -0,0 +1,65 @@ +import { RequestStore } from "../../../lib/mobx-request/request-store.ts"; +import { + cardInputModeListRequest, + deckChangeInputModeRequest, +} from "../../../api/api.ts"; +import { makeAutoObservable } from "mobx"; +import { TextField } from "mobx-form-lite"; +import { screenStore } from "../../../store/screen-store.ts"; +import { assert } from "../../../lib/typescript/assert.ts"; +import { notifyError, notifySuccess } from "../../shared/snackbar/snackbar.tsx"; +import { deckListStore } from "../../../store/deck-list-store.ts"; +import { t } from "../../../translations/t.ts"; + +export const createCachedCardInputModesRequest = () => { + return new RequestStore(cardInputModeListRequest, { + cacheId: "cardInputModeList", + }); +}; + +export class CardInputModeStore { + cardInputModesRequest = createCachedCardInputModesRequest(); + deckChangeInputModeRequest = new RequestStore(deckChangeInputModeRequest); + modeId = new TextField(null); + + constructor() { + makeAutoObservable(this, {}, { autoBind: true }); + } + + load() { + const { screen } = screenStore; + assert(screen.type === "cardInputMode"); + + this.modeId.onChange(screen.cardInputModeId); + this.cardInputModesRequest.execute(); + } + + async submit() { + if (this.cardInputModesRequest.isLoading) { + return; + } + + const { screen } = screenStore; + assert(screen.type === "cardInputMode"); + + const result = await this.deckChangeInputModeRequest.execute({ + deckId: screen.deckId, + cardInputModeId: this.modeId.value, + }); + + if (result.status === "error") { + notifyError({ + e: result.error, + info: "Failed to change card input mode", + }); + return; + } + + deckListStore.updateDeckCardInputMode(screen.deckId, this.modeId.value); + notifySuccess(t("card_input_mode_changed")); + screenStore.go({ + type: "deckForm", + deckId: screen.deckId, + }); + } +} diff --git a/src/screens/component-catalog/card-preview-story.tsx b/src/screens/component-catalog/card-preview-story.tsx index 88cf7570..b82b3d11 100644 --- a/src/screens/component-catalog/card-preview-story.tsx +++ b/src/screens/component-catalog/card-preview-story.tsx @@ -1,6 +1,6 @@ -import { CardPreview } from "../deck-form/card-preview.tsx"; +import { CardPreview } from "../deck-form/card-form/card-preview.tsx"; import { useState } from "react"; -import { createMockCardPreviewForm } from "../deck-form/create-mock-card-preview-form.ts"; +import { createMockCardPreviewForm } from "../deck-form/card-form/create-mock-card-preview-form.ts"; export const CardPreviewStory = (props: { card: { diff --git a/src/screens/component-catalog/components.tsx b/src/screens/component-catalog/components.tsx index 683f4633..2e7272ae 100644 --- a/src/screens/component-catalog/components.tsx +++ b/src/screens/component-catalog/components.tsx @@ -6,6 +6,7 @@ import { PieChartCanvasStory } from "./pie-chart-canvas-story.tsx"; import { SnackbarStory } from "./snackbar-story.tsx"; import { ListStory } from "./list-story.tsx"; import { ListStoryMultipleIcons } from "./list-story-multiple-icons.tsx"; +import { RadioListStory } from "./radio-list-story.tsx"; export type Component = { name: string; @@ -66,4 +67,8 @@ export const components: Array = [ name: ListStoryMultipleIcons.name, component: , }, + { + name: RadioListStory.name, + component: , + }, ]; diff --git a/src/screens/component-catalog/radio-list-story.tsx b/src/screens/component-catalog/radio-list-story.tsx new file mode 100644 index 00000000..4c596e98 --- /dev/null +++ b/src/screens/component-catalog/radio-list-story.tsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { RadioList } from "../../ui/radio-list/radio-list.tsx"; +import { Flex } from "../../ui/flex.tsx"; + +export const RadioListStory = () => { + const [selectedId, setSelectedId] = useState<"1" | "2">("1"); + const [inputModeId, setInputModeId] = useState(null); + + return ( + + + selectedId={selectedId} + options={[ + { id: "1", title: "Option 1" }, + { id: "2", title: "Option 2" }, + ]} + onChange={setSelectedId} + /> + + + selectedId={inputModeId} + options={[ + { id: null, title: "None" }, + { id: "1", title: "Option 1" }, + { id: "2", title: "Option 2" }, + ]} + onChange={setInputModeId} + /> + + ); +}; diff --git a/src/screens/deck-catalog/deck-catalog.tsx b/src/screens/deck-catalog/deck-catalog.tsx index 64cf35cc..eac583dc 100644 --- a/src/screens/deck-catalog/deck-catalog.tsx +++ b/src/screens/deck-catalog/deck-catalog.tsx @@ -10,7 +10,7 @@ import { Select } from "../../ui/select.tsx"; import { DeckLanguage } from "./store/deck-catalog-store.ts"; import { DeckListItemWithDescription } from "../../ui/deck-list-item-with-description.tsx"; import { range } from "../../lib/array/range.ts"; -import { DeckLoading } from "../shared/deck-loading.tsx"; +import { CardRowLoading } from "../shared/card-row-loading.tsx"; import { NoDecksMatchingFilters } from "./no-decks-matching-filters.tsx"; import { deckListStore } from "../../store/deck-list-store.ts"; import { DeckAddedLabel } from "./deck-added-label.tsx"; @@ -70,7 +70,7 @@ export const DeckCatalog = observer(() => { {(() => { if (store.catalogRequest.result.status === "loading") { - return range(5).map((i) => ); + return range(5).map((i) => ); } if (store.catalogRequest.result.status === "success") { diff --git a/src/screens/deck-form/answer-form-view.tsx b/src/screens/deck-form/card-form/answer-form-view.tsx similarity index 77% rename from src/screens/deck-form/answer-form-view.tsx rename to src/screens/deck-form/card-form/answer-form-view.tsx index 99529c06..efe76961 100644 --- a/src/screens/deck-form/answer-form-view.tsx +++ b/src/screens/deck-form/card-form/answer-form-view.tsx @@ -1,18 +1,18 @@ import { observer } from "mobx-react-lite"; -import { CardFormType } from "./store/deck-form-store.ts"; -import { assert } from "../../lib/typescript/assert.ts"; -import { Screen } from "../shared/screen.tsx"; -import { Label } from "../../ui/label.tsx"; -import { Input } from "../../ui/input.tsx"; -import { CardRow } from "../../ui/card-row.tsx"; -import { RadioSwitcher } from "../../ui/radio-switcher.tsx"; -import { HintTransparent } from "../../ui/hint-transparent.tsx"; +import { CardFormType } from "../deck-form/store/deck-form-store.ts"; +import { assert } from "../../../lib/typescript/assert.ts"; +import { Screen } from "../../shared/screen.tsx"; +import { Label } from "../../../ui/label.tsx"; +import { Input } from "../../../ui/input.tsx"; +import { CardRow } from "../../../ui/card-row.tsx"; +import { RadioSwitcher } from "../../../ui/radio-switcher.tsx"; +import { HintTransparent } from "../../../ui/hint-transparent.tsx"; import React from "react"; -import { useBackButton } from "../../lib/platform/use-back-button.ts"; -import { useMainButton } from "../../lib/platform/use-main-button.ts"; -import { ButtonGrid } from "../../ui/button-grid.tsx"; -import { t } from "../../translations/t.ts"; -import { ButtonSideAligned } from "../../ui/button-side-aligned.tsx"; +import { useBackButton } from "../../../lib/platform/use-back-button.ts"; +import { useMainButton } from "../../../lib/platform/use-main-button.ts"; +import { ButtonGrid } from "../../../ui/button-grid.tsx"; +import { t } from "../../../translations/t.ts"; +import { ButtonSideAligned } from "../../../ui/button-side-aligned.tsx"; import { v4 } from "uuid"; import { BooleanField, isFormValid } from "mobx-form-lite"; import { action } from "mobx"; diff --git a/src/screens/deck-form/card-ai-speech.tsx b/src/screens/deck-form/card-form/card-ai-speech.tsx similarity index 68% rename from src/screens/deck-form/card-ai-speech.tsx rename to src/screens/deck-form/card-form/card-ai-speech.tsx index ab8da98f..236c11fa 100644 --- a/src/screens/deck-form/card-ai-speech.tsx +++ b/src/screens/deck-form/card-form/card-ai-speech.tsx @@ -1,17 +1,17 @@ import { observer } from "mobx-react-lite"; -import { CardFormType } from "./store/deck-form-store.ts"; -import { useBackButton } from "../../lib/platform/use-back-button.ts"; -import { Screen } from "../shared/screen.tsx"; -import { AudioPlayer } from "../../ui/audio-player.tsx"; -import { useMainButton } from "../../lib/platform/use-main-button.ts"; -import { t } from "../../translations/t.ts"; -import { Button } from "../../ui/button.tsx"; -import { ButtonGrid } from "../../ui/button-grid.tsx"; +import { CardFormType } from "../deck-form/store/deck-form-store.ts"; +import { useBackButton } from "../../../lib/platform/use-back-button.ts"; +import { Screen } from "../../shared/screen.tsx"; +import { AudioPlayer } from "../../../ui/audio-player.tsx"; +import { useMainButton } from "../../../lib/platform/use-main-button.ts"; +import { t } from "../../../translations/t.ts"; +import { Button } from "../../../ui/button.tsx"; +import { ButtonGrid } from "../../../ui/button-grid.tsx"; import { css } from "@emotion/css"; -import { theme } from "../../ui/theme.tsx"; -import { Flex } from "../../ui/flex.tsx"; -import { Chip } from "../../ui/chip.tsx"; -import { Input } from "../../ui/input.tsx"; +import { theme } from "../../../ui/theme.tsx"; +import { Flex } from "../../../ui/flex.tsx"; +import { Chip } from "../../../ui/chip.tsx"; +import { Input } from "../../../ui/input.tsx"; import { useState } from "react"; import { AiSpeechGeneratorStore } from "./store/ai-speech-generator-store.ts"; @@ -87,14 +87,20 @@ export const CardAiSpeech = observer((props: Props) => { )} diff --git a/src/screens/deck-form/card-answer-errors.tsx b/src/screens/deck-form/card-form/card-answer-errors.tsx similarity index 78% rename from src/screens/deck-form/card-answer-errors.tsx rename to src/screens/deck-form/card-form/card-answer-errors.tsx index 3514b08b..420010f1 100644 --- a/src/screens/deck-form/card-answer-errors.tsx +++ b/src/screens/deck-form/card-form/card-answer-errors.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react-lite"; -import { CardFormType } from "./store/deck-form-store.ts"; +import { CardFormType } from "../deck-form/store/deck-form-store.ts"; import { isFormDirty, isFormTouched } from "mobx-form-lite"; -import { ValidationError } from "../../ui/validation-error.tsx"; +import { ValidationError } from "../../../ui/validation-error.tsx"; import React from "react"; type Props = { diff --git a/src/screens/deck-form/card-example.tsx b/src/screens/deck-form/card-form/card-example.tsx similarity index 60% rename from src/screens/deck-form/card-example.tsx rename to src/screens/deck-form/card-form/card-example.tsx index 9be3e323..35b14e0b 100644 --- a/src/screens/deck-form/card-example.tsx +++ b/src/screens/deck-form/card-form/card-example.tsx @@ -1,16 +1,16 @@ import { observer } from "mobx-react-lite"; -import { Screen } from "../shared/screen.tsx"; -import { Label } from "../../ui/label.tsx"; -import { t } from "../../translations/t.ts"; +import { Screen } from "../../shared/screen.tsx"; +import { Label } from "../../../ui/label.tsx"; +import { t } from "../../../translations/t.ts"; import { FormattingSwitcher } from "./formatting-switcher.tsx"; -import { WysiwygField } from "../../ui/wysiwyg-field/wysiwig-field.tsx"; -import { Input } from "../../ui/input.tsx"; -import { HintTransparent } from "../../ui/hint-transparent.tsx"; +import { WysiwygField } from "../../../ui/wysiwyg-field/wysiwig-field.tsx"; +import { Input } from "../../../ui/input.tsx"; +import { HintTransparent } from "../../../ui/hint-transparent.tsx"; import React from "react"; -import { userStore } from "../../store/user-store.ts"; -import { CardFormType } from "./store/deck-form-store.ts"; -import { useBackButton } from "../../lib/platform/use-back-button.ts"; -import { useMainButton } from "../../lib/platform/use-main-button.ts"; +import { userStore } from "../../../store/user-store.ts"; +import { CardFormType } from "../deck-form/store/deck-form-store.ts"; +import { useBackButton } from "../../../lib/platform/use-back-button.ts"; +import { useMainButton } from "../../../lib/platform/use-main-button.ts"; type Props = { cardForm: CardFormType; diff --git a/src/screens/deck-form/card-form/card-form-view.tsx b/src/screens/deck-form/card-form/card-form-view.tsx new file mode 100644 index 00000000..1753509a --- /dev/null +++ b/src/screens/deck-form/card-form/card-form-view.tsx @@ -0,0 +1,24 @@ +import { observer } from "mobx-react-lite"; +import { CardFormStoreInterface } from "../deck-form/store/card-form-store-interface.ts"; +import { assert } from "../../../lib/typescript/assert.ts"; +import React from "react"; +import { GeneratedCardFormView } from "./generated-card-form-view.tsx"; +import { ManualCardFormView } from "./manual-card-form-view.tsx"; + +type Props = { cardFormStore: CardFormStoreInterface }; + +export const CardFormView = observer((props: Props) => { + const { cardFormStore } = props; + const { cardForm } = cardFormStore; + + assert(cardForm, "Card should not be empty before editing"); + assert( + cardFormStore.deckForm, + "Deck form should not be empty before editing", + ); + + if (cardFormStore.deckForm.cardInputModeId === null || cardForm.id) { + return ; + } + return ; +}); diff --git a/src/screens/deck-form/card-form-wrapper.tsx b/src/screens/deck-form/card-form/card-form-wrapper.tsx similarity index 91% rename from src/screens/deck-form/card-form-wrapper.tsx rename to src/screens/deck-form/card-form/card-form-wrapper.tsx index 743fea1f..d8acec69 100644 --- a/src/screens/deck-form/card-form-wrapper.tsx +++ b/src/screens/deck-form/card-form/card-form-wrapper.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; import React from "react"; import { AnswerFormView } from "./answer-form-view.tsx"; -import { assert } from "../../lib/typescript/assert.ts"; -import { CardFormStoreInterface } from "./store/card-form-store-interface.ts"; +import { assert } from "../../../lib/typescript/assert.ts"; +import { CardFormStoreInterface } from "../deck-form/store/card-form-store-interface.ts"; import { CardPreview } from "./card-preview.tsx"; import { CardFormView } from "./card-form-view.tsx"; import { CardExample } from "./card-example.tsx"; diff --git a/src/screens/deck-form/card-preview.tsx b/src/screens/deck-form/card-form/card-preview.tsx similarity index 82% rename from src/screens/deck-form/card-preview.tsx rename to src/screens/deck-form/card-form/card-preview.tsx index 0079b8e3..2a9f073c 100644 --- a/src/screens/deck-form/card-preview.tsx +++ b/src/screens/deck-form/card-form/card-preview.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react-lite"; -import { useBackButton } from "../../lib/platform/use-back-button.ts"; +import { useBackButton } from "../../../lib/platform/use-back-button.ts"; import { css } from "@emotion/css"; -import { CardReviewWithControls } from "../deck-review/card-review-with-controls.tsx"; +import { CardReviewWithControls } from "../../deck-review/card-review-with-controls.tsx"; import React, { useState } from "react"; -import { CardPreviewStore } from "../deck-review/store/card-preview-store.ts"; -import { CardFormStoreInterface } from "./store/card-form-store-interface.ts"; +import { CardPreviewStore } from "../../deck-review/store/card-preview-store.ts"; +import { CardFormStoreInterface } from "../deck-form/store/card-form-store-interface.ts"; import { createPortal } from "react-dom"; type Props = { diff --git a/src/screens/deck-form/card-type.tsx b/src/screens/deck-form/card-form/card-type.tsx similarity index 81% rename from src/screens/deck-form/card-type.tsx rename to src/screens/deck-form/card-form/card-type.tsx index b8bf5c67..01dfa535 100644 --- a/src/screens/deck-form/card-type.tsx +++ b/src/screens/deck-form/card-form/card-type.tsx @@ -1,18 +1,21 @@ import { observer } from "mobx-react-lite"; -import { Screen } from "../shared/screen.tsx"; -import { t } from "../../translations/t.ts"; -import { Flex } from "../../ui/flex.tsx"; +import { Screen } from "../../shared/screen.tsx"; +import { t } from "../../../translations/t.ts"; +import { Flex } from "../../../ui/flex.tsx"; import { css, cx } from "@emotion/css"; -import { theme } from "../../ui/theme.tsx"; -import { SelectWithChevron } from "../../ui/select-with-chevron.tsx"; -import { HintTransparent } from "../../ui/hint-transparent.tsx"; -import { List } from "../../ui/list.tsx"; +import { theme } from "../../../ui/theme.tsx"; +import { SelectWithChevron } from "../../../ui/select-with-chevron.tsx"; +import { HintTransparent } from "../../../ui/hint-transparent.tsx"; +import { List } from "../../../ui/list.tsx"; import { action } from "mobx"; -import { CardRow } from "../../ui/card-row.tsx"; -import { CardFormType, createAnswerForm } from "./store/deck-form-store.ts"; +import { CardRow } from "../../../ui/card-row.tsx"; +import { + CardFormType, + createAnswerForm, +} from "../deck-form/store/deck-form-store.ts"; import React from "react"; -import { useBackButton } from "../../lib/platform/use-back-button.ts"; -import { useMainButton } from "../../lib/platform/use-main-button.ts"; +import { useBackButton } from "../../../lib/platform/use-back-button.ts"; +import { useMainButton } from "../../../lib/platform/use-main-button.ts"; import { formatCardType, formatCardTypeDescription, diff --git a/src/screens/deck-form/create-mock-card-preview-form.ts b/src/screens/deck-form/card-form/create-mock-card-preview-form.ts similarity index 71% rename from src/screens/deck-form/create-mock-card-preview-form.ts rename to src/screens/deck-form/card-form/create-mock-card-preview-form.ts index 8733d716..4cf4bd6f 100644 --- a/src/screens/deck-form/create-mock-card-preview-form.ts +++ b/src/screens/deck-form/card-form/create-mock-card-preview-form.ts @@ -1,11 +1,11 @@ import { CardFormStoreInterface, CardInnerScreenType, -} from "./store/card-form-store-interface.ts"; +} from "../deck-form/store/card-form-store-interface.ts"; import { ListField, TextField } from "mobx-form-lite"; -import { CardAnswerType } from "../../../functions/db/custom-types.ts"; -import { CardAnswerFormType } from "./store/deck-form-store.ts"; -import { DeckCardOptionsDbType } from "../../../functions/db/deck/decks-with-cards-schema.ts"; +import { CardAnswerType } from "../../../../functions/db/custom-types.ts"; +import { CardAnswerFormType } from "../deck-form/store/deck-form-store.ts"; +import { DeckCardOptionsDbType } from "../../../../functions/db/deck/decks-with-cards-schema.ts"; export const createMockCardPreviewForm = (card: { front: string; @@ -23,7 +23,7 @@ export const createMockCardPreviewForm = (card: { answers: new ListField([]), answerId: "0", }, - form: undefined, + deckForm: undefined, cardInnerScreen: new TextField(null), onBackCard: () => {}, onSaveCard: () => {}, diff --git a/src/screens/deck-form/format-card-type.ts b/src/screens/deck-form/card-form/format-card-type.ts similarity index 81% rename from src/screens/deck-form/format-card-type.ts rename to src/screens/deck-form/card-form/format-card-type.ts index c47f9344..60d1d598 100644 --- a/src/screens/deck-form/format-card-type.ts +++ b/src/screens/deck-form/card-form/format-card-type.ts @@ -1,5 +1,5 @@ -import { CardAnswerType } from "../../../functions/db/custom-types.ts"; -import { t } from "../../translations/t.ts"; +import { CardAnswerType } from "../../../../functions/db/custom-types.ts"; +import { t } from "../../../translations/t.ts"; export const formatCardType = (type: CardAnswerType) => { switch (type) { diff --git a/src/screens/deck-form/formatting-switcher.tsx b/src/screens/deck-form/card-form/formatting-switcher.tsx similarity index 74% rename from src/screens/deck-form/formatting-switcher.tsx rename to src/screens/deck-form/card-form/formatting-switcher.tsx index ba6d5064..da1387cd 100644 --- a/src/screens/deck-form/formatting-switcher.tsx +++ b/src/screens/deck-form/card-form/formatting-switcher.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react-lite"; -import { userStore } from "../../store/user-store.ts"; +import { userStore } from "../../../store/user-store.ts"; import { css, cx } from "@emotion/css"; -import { reset } from "../../ui/reset.ts"; -import { theme } from "../../ui/theme.tsx"; -import { ChevronIcon } from "../../ui/chevron-icon.tsx"; -import { t } from "../../translations/t.ts"; +import { reset } from "../../../ui/reset.ts"; +import { theme } from "../../../ui/theme.tsx"; +import { ChevronIcon } from "../../../ui/chevron-icon.tsx"; +import { t } from "../../../translations/t.ts"; import React from "react"; export const FormattingSwitcher = observer(() => { diff --git a/src/screens/deck-form/card-form/generated-card-form-view.tsx b/src/screens/deck-form/card-form/generated-card-form-view.tsx new file mode 100644 index 00000000..fa637825 --- /dev/null +++ b/src/screens/deck-form/card-form/generated-card-form-view.tsx @@ -0,0 +1,93 @@ +import { observer } from "mobx-react-lite"; +import { CardFormStoreInterface } from "../deck-form/store/card-form-store-interface.ts"; +import { assert } from "../../../lib/typescript/assert.ts"; +import { useBackButton } from "../../../lib/platform/use-back-button.ts"; +import React, { useState } from "react"; +import { AiGeneratedCardFormStore } from "./store/ai-generated-card-form-store.ts"; +import { useMainButton } from "../../../lib/platform/use-main-button.ts"; +import { t } from "../../../translations/t.ts"; +import { useProgress } from "../../../lib/platform/use-progress.tsx"; +import { useMount } from "../../../lib/react/use-mount.ts"; +import { Screen } from "../../shared/screen.tsx"; +import { Label } from "../../../ui/label.tsx"; +import { Input } from "../../../ui/input.tsx"; +import { HintTransparent } from "../../../ui/hint-transparent.tsx"; +import { CardRowLoading } from "../../shared/card-row-loading.tsx"; +import { CardRow } from "../../../ui/card-row.tsx"; +import { css } from "@emotion/css"; +import { theme } from "../../../ui/theme.tsx"; +import { screenStore } from "../../../store/screen-store.ts"; + +type Props = { cardFormStore: CardFormStoreInterface }; + +export const GeneratedCardFormView = observer((props: Props) => { + const { cardFormStore } = props; + + assert( + cardFormStore.deckForm, + "Deck form should not be empty before editing", + ); + const cardInputModeId = cardFormStore.deckForm.cardInputModeId; + assert(cardInputModeId, "Card input mode should not be empty before editing"); + + useBackButton(() => { + cardFormStore.onBackCard(); + }); + + const [localStore] = useState(() => new AiGeneratedCardFormStore()); + + useMainButton(t("generate"), () => { + localStore.submit(); + }); + + useProgress(() => localStore.isSaveLoading); + + useMount(() => { + localStore.cardInputModesRequest.execute(); + }); + + return ( + + + + + + ); +}); diff --git a/src/screens/deck-form/card-form-view.tsx b/src/screens/deck-form/card-form/manual-card-form-view.tsx similarity index 78% rename from src/screens/deck-form/card-form-view.tsx rename to src/screens/deck-form/card-form/manual-card-form-view.tsx index 11187ead..74ebee30 100644 --- a/src/screens/deck-form/card-form-view.tsx +++ b/src/screens/deck-form/card-form/manual-card-form-view.tsx @@ -1,37 +1,34 @@ import { observer } from "mobx-react-lite"; -import { CardFormStoreInterface } from "./store/card-form-store-interface.ts"; -import { assert } from "../../lib/typescript/assert.ts"; -import { useMainButton } from "../../lib/platform/use-main-button.ts"; -import { t } from "../../translations/t.ts"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; -import { useBackButton } from "../../lib/platform/use-back-button.ts"; -import { formTouchAll, isFormValid } from "mobx-form-lite"; -import { Screen } from "../shared/screen.tsx"; -import { Label } from "../../ui/label.tsx"; -import { HintTransparent } from "../../ui/hint-transparent.tsx"; -import { css } from "@emotion/css"; -import { theme } from "../../ui/theme.tsx"; -import { ButtonGrid } from "../../ui/button-grid.tsx"; -import { ButtonSideAligned } from "../../ui/button-side-aligned.tsx"; -import React from "react"; -import { WysiwygField } from "../../ui/wysiwyg-field/wysiwig-field.tsx"; -import { userStore } from "../../store/user-store.ts"; -import { Input } from "../../ui/input.tsx"; +import { CardFormStoreInterface } from "../deck-form/store/card-form-store-interface.ts"; +import { assert } from "../../../lib/typescript/assert.ts"; +import { useMainButton } from "../../../lib/platform/use-main-button.ts"; +import { t } from "../../../translations/t.ts"; +import { useProgress } from "../../../lib/platform/use-progress.tsx"; +import { useBackButton } from "../../../lib/platform/use-back-button.ts"; +import { userStore } from "../../../store/user-store.ts"; +import { Screen } from "../../shared/screen.tsx"; +import { Flex } from "../../../ui/flex.tsx"; +import { Label } from "../../../ui/label.tsx"; import { FormattingSwitcher } from "./formatting-switcher.tsx"; -import { Flex } from "../../ui/flex.tsx"; -import { List } from "../../ui/list.tsx"; -import { FilledIcon } from "../../ui/filled-icon.tsx"; -import { ListHeader } from "../../ui/list-header.tsx"; +import { WysiwygField } from "../../../ui/wysiwyg-field/wysiwig-field.tsx"; +import { Input } from "../../../ui/input.tsx"; +import { HintTransparent } from "../../../ui/hint-transparent.tsx"; +import { ListHeader } from "../../../ui/list-header.tsx"; +import { List } from "../../../ui/list.tsx"; +import { FilledIcon } from "../../../ui/filled-icon.tsx"; +import { theme } from "../../../ui/theme.tsx"; +import { ListRightText } from "../../../ui/list-right-text.tsx"; import { formatCardType } from "./format-card-type.ts"; -import { ListRightText } from "../../ui/list-right-text.tsx"; +import { formTouchAll, isFormValid } from "mobx-form-lite"; +import { ButtonSideAligned } from "../../../ui/button-side-aligned.tsx"; +import { ButtonGrid } from "../../../ui/button-grid.tsx"; +import { boolNarrow } from "../../../lib/typescript/bool-narrow.ts"; import { CardAnswerErrors } from "./card-answer-errors.tsx"; -import { boolNarrow } from "../../lib/typescript/bool-narrow.ts"; +import { css } from "@emotion/css"; -type Props = { - cardFormStore: CardFormStoreInterface; -}; +type Props = { cardFormStore: CardFormStoreInterface }; -export const CardFormView = observer((props: Props) => { +export const ManualCardFormView = observer((props: Props) => { const { cardFormStore } = props; const { cardForm, markCardAsRemoved } = cardFormStore; assert(cardForm, "Card should not be empty before editing"); @@ -40,7 +37,7 @@ export const CardFormView = observer((props: Props) => { cardFormStore.onSaveCard(); }); - useMainButtonProgress(() => cardFormStore.isSending); + useProgress(() => cardFormStore.isSending); useBackButton(() => { cardFormStore.onBackCard(); diff --git a/src/screens/deck-form/quick-add-card-form-page.tsx b/src/screens/deck-form/card-form/quick-add-card-form-page.tsx similarity index 77% rename from src/screens/deck-form/quick-add-card-form-page.tsx rename to src/screens/deck-form/card-form/quick-add-card-form-page.tsx index 8f720c3f..576c2e8e 100644 --- a/src/screens/deck-form/quick-add-card-form-page.tsx +++ b/src/screens/deck-form/card-form/quick-add-card-form-page.tsx @@ -2,9 +2,9 @@ import { observer } from "mobx-react-lite"; import React, { useState } from "react"; import { QuickAddCardFormStore } from "./store/quick-add-card-form-store.ts"; import { CardFormWrapper } from "./card-form-wrapper.tsx"; -import { deckListStore } from "../../store/deck-list-store.ts"; -import { screenStore } from "../../store/screen-store.ts"; -import { assert } from "../../lib/typescript/assert.ts"; +import { deckListStore } from "../../../store/deck-list-store.ts"; +import { screenStore } from "../../../store/screen-store.ts"; +import { assert } from "../../../lib/typescript/assert.ts"; import { TextField } from "mobx-form-lite"; const createQuickAddCardFormStore = () => { @@ -17,6 +17,7 @@ const createQuickAddCardFormStore = () => { ? { speakingCardsField: new TextField(deck.speak_field), speakingCardsLocale: new TextField(deck.speak_locale), + cardInputModeId: deck.card_input_mode_id || null, } : undefined, ); diff --git a/src/screens/deck-form/card-form/store/ai-generated-card-form-store.ts b/src/screens/deck-form/card-form/store/ai-generated-card-form-store.ts new file mode 100644 index 00000000..a821e074 --- /dev/null +++ b/src/screens/deck-form/card-form/store/ai-generated-card-form-store.ts @@ -0,0 +1,71 @@ +import { createCardSideField } from "../../deck-form/store/deck-form-store.ts"; +import { createCachedCardInputModesRequest } from "../../../card-input-mode/store/card-input-mode-store.ts"; +import { RequestStore } from "../../../../lib/mobx-request/request-store.ts"; +import { makeAutoObservable } from "mobx"; +import { formTouchAll, isFormValid } from "mobx-form-lite"; +import { screenStore } from "../../../../store/screen-store.ts"; +import { assert } from "../../../../lib/typescript/assert.ts"; +import { notifyError } from "../../../shared/snackbar/snackbar.tsx"; +import { aiSingleCardGenerateRequest } from "../../../../api/api.ts"; +import { deckListStore } from "../../../../store/deck-list-store.ts"; + +export class AiGeneratedCardFormStore { + form = { + prompt: createCardSideField(""), + }; + cardInputModesRequest = createCachedCardInputModesRequest(); + aiSingleCardGenerateRequest = new RequestStore(aiSingleCardGenerateRequest); + + constructor() { + makeAutoObservable(this, {}, { autoBind: true }); + } + + async submit() { + if (!isFormValid(this.form)) { + formTouchAll(this.form); + return; + } + + + const result = await this.aiSingleCardGenerateRequest.execute({ + text: this.form.prompt.value, + deckId: this.deckId, + }); + + if (result.status === "error") { + notifyError({ + e: result.error, + info: "Error while generating single card", + }); + return; + } + + if (!result.data.data) { + notifyError(false, { message: result.data.error }); + return; + } + + const { card } = result.data.data; + deckListStore.addCardOptimistic(card); + screenStore.go({ type: "deckForm", deckId: card.deck_id, cardId: card.id }); + } + + get deckId() { + const { screen } = screenStore; + + assert( + screen.type === "cardQuickAddForm" || screen.type === "deckForm", + "Screen does not have deckId", + ); + assert(screen.deckId); + + return screen.deckId; + } + + get isSaveLoading() { + return ( + this.aiSingleCardGenerateRequest.isLoading || + this.cardInputModesRequest.isLoading + ); + } +} diff --git a/src/screens/deck-form/store/ai-speech-generator-store.ts b/src/screens/deck-form/card-form/store/ai-speech-generator-store.ts similarity index 82% rename from src/screens/deck-form/store/ai-speech-generator-store.ts rename to src/screens/deck-form/card-form/store/ai-speech-generator-store.ts index c070dbc6..def5eb50 100644 --- a/src/screens/deck-form/store/ai-speech-generator-store.ts +++ b/src/screens/deck-form/card-form/store/ai-speech-generator-store.ts @@ -1,10 +1,10 @@ -import { RequestStore } from "../../../lib/mobx-request/request-store.ts"; -import { aiSpeechGenerateRequest } from "../../../api/api.ts"; +import { RequestStore } from "../../../../lib/mobx-request/request-store.ts"; +import { aiSpeechGenerateRequest } from "../../../../api/api.ts"; import { formTouchAll, isFormValid, TextField } from "mobx-form-lite"; -import { CardFormType } from "./deck-form-store.ts"; +import { CardFormType } from "../../deck-form/store/deck-form-store.ts"; import { makeAutoObservable } from "mobx"; -import { notifyError } from "../../shared/snackbar/snackbar.tsx"; -import { t } from "../../../translations/t.ts"; +import { notifyError } from "../../../shared/snackbar/snackbar.tsx"; +import { t } from "../../../../translations/t.ts"; export class AiSpeechGeneratorStore { speechGenerateRequest = new RequestStore(aiSpeechGenerateRequest); @@ -35,10 +35,6 @@ export class AiSpeechGeneratorStore { ); } - get isLoading() { - return this.speechGenerateRequest.isLoading; - } - async generate() { if (!isFormValid(this.form)) { formTouchAll(this.form); diff --git a/src/screens/deck-form/store/quick-add-card-form-store.ts b/src/screens/deck-form/card-form/store/quick-add-card-form-store.ts similarity index 68% rename from src/screens/deck-form/store/quick-add-card-form-store.ts rename to src/screens/deck-form/card-form/store/quick-add-card-form-store.ts index 0c8fb536..b87490ff 100644 --- a/src/screens/deck-form/store/quick-add-card-form-store.ts +++ b/src/screens/deck-form/card-form/store/quick-add-card-form-store.ts @@ -3,7 +3,7 @@ import { createAnswerListField, createAnswerTypeField, createCardSideField, -} from "./deck-form-store.ts"; +} from "../../deck-form/store/deck-form-store.ts"; import { makeAutoObservable } from "mobx"; import { formTouchAll, @@ -12,23 +12,24 @@ import { isFormValid, TextField, } from "mobx-form-lite"; -import { screenStore } from "../../../store/screen-store.ts"; -import { showConfirm } from "../../../lib/platform/show-confirm.ts"; -import { addCardRequest } from "../../../api/api.ts"; -import { assert } from "../../../lib/typescript/assert.ts"; -import { AddCardRequest } from "../../../../functions/add-card.ts"; -import { deckListStore } from "../../../store/deck-list-store.ts"; -import { t } from "../../../translations/t.ts"; +import { screenStore } from "../../../../store/screen-store.ts"; +import { showConfirm } from "../../../../lib/platform/show-confirm.ts"; +import { addCardRequest } from "../../../../api/api.ts"; +import { assert } from "../../../../lib/typescript/assert.ts"; +import { AddCardRequest } from "../../../../../functions/add-card.ts"; +import { deckListStore } from "../../../../store/deck-list-store.ts"; +import { t } from "../../../../translations/t.ts"; import { CardFormStoreInterface, CardInnerScreenType, -} from "./card-form-store-interface.ts"; + LimitedDeckForm, +} from "../../deck-form/store/card-form-store-interface.ts"; +import { DeckCardOptionsDbType } from "../../../../../functions/db/deck/decks-with-cards-schema.ts"; +import { RequestStore } from "../../../../lib/mobx-request/request-store.ts"; import { - DeckCardOptionsDbType, - DeckSpeakFieldEnum, -} from "../../../../functions/db/deck/decks-with-cards-schema.ts"; -import { RequestStore } from "../../../lib/mobx-request/request-store.ts"; -import { notifyError, notifySuccess } from "../../shared/snackbar/snackbar.tsx"; + notifyError, + notifySuccess, +} from "../../../shared/snackbar/snackbar.tsx"; export class QuickAddCardFormStore implements CardFormStoreInterface { cardForm: CardFormType = { @@ -42,12 +43,7 @@ export class QuickAddCardFormStore implements CardFormStoreInterface { addCardRequest = new RequestStore(addCardRequest); cardInnerScreen = new TextField(null); - constructor( - public form?: { - speakingCardsLocale: TextField; - speakingCardsField: TextField; - }, - ) { + constructor(public deckForm?: LimitedDeckForm) { makeAutoObservable(this, {}, { autoBind: true }); } @@ -86,7 +82,7 @@ export class QuickAddCardFormStore implements CardFormStoreInterface { } screenStore.back(); - deckListStore.load(); + deckListStore.addCardOptimistic(result.data.card); notifySuccess(t("card_added")); } diff --git a/src/screens/deck-form/card-list.tsx b/src/screens/deck-form/deck-form/card-list.tsx similarity index 78% rename from src/screens/deck-form/card-list.tsx rename to src/screens/deck-form/deck-form/card-list.tsx index 2a73e4eb..28afc8ab 100644 --- a/src/screens/deck-form/card-list.tsx +++ b/src/screens/deck-form/deck-form/card-list.tsx @@ -1,20 +1,20 @@ import { observer } from "mobx-react-lite"; import { useDeckFormStore } from "./store/deck-form-store-context.tsx"; -import { screenStore } from "../../store/screen-store.ts"; -import { assert } from "../../lib/typescript/assert.ts"; -import { useBackButton } from "../../lib/platform/use-back-button.ts"; +import { screenStore } from "../../../store/screen-store.ts"; +import { assert } from "../../../lib/typescript/assert.ts"; +import { useBackButton } from "../../../lib/platform/use-back-button.ts"; import { css, cx } from "@emotion/css"; -import { Input } from "../../ui/input.tsx"; -import { theme } from "../../ui/theme.tsx"; -import { Button } from "../../ui/button.tsx"; +import { Input } from "../../../ui/input.tsx"; +import { theme } from "../../../ui/theme.tsx"; +import { Button } from "../../../ui/button.tsx"; import React from "react"; -import { reset } from "../../ui/reset.ts"; -import { t } from "../../translations/t.ts"; -import { Screen } from "../shared/screen.tsx"; -import { removeAllTags } from "../../lib/sanitize-html/remove-all-tags.ts"; -import { tapScale } from "../../lib/animations/tap-scale.ts"; -import { Flex } from "../../ui/flex.tsx"; -import { CardNumber } from "../../ui/card-number.tsx"; +import { reset } from "../../../ui/reset.ts"; +import { t } from "../../../translations/t.ts"; +import { Screen } from "../../shared/screen.tsx"; +import { removeAllTags } from "../../../lib/sanitize-html/remove-all-tags.ts"; +import { tapScale } from "../../../lib/animations/tap-scale.ts"; +import { Flex } from "../../../ui/flex.tsx"; +import { CardNumber } from "../../../ui/card-number.tsx"; export const CardList = observer(() => { const deckFormStore = useDeckFormStore(); @@ -25,13 +25,13 @@ export const CardList = observer(() => { deckFormStore.quitInnerScreen(); }); - if (!deckFormStore.form) { + if (!deckFormStore.deckForm) { return null; } return ( - {deckFormStore.form.cards.length > 1 && ( + {deckFormStore.deckForm.cards.length > 1 && ( <> { diff --git a/src/screens/deck-form/deck-form.tsx b/src/screens/deck-form/deck-form/deck-form.tsx similarity index 56% rename from src/screens/deck-form/deck-form.tsx rename to src/screens/deck-form/deck-form/deck-form.tsx index 9c634097..0ef21212 100644 --- a/src/screens/deck-form/deck-form.tsx +++ b/src/screens/deck-form/deck-form/deck-form.tsx @@ -1,31 +1,31 @@ import { observer } from "mobx-react-lite"; import { css, cx } from "@emotion/css"; -import { Label } from "../../ui/label.tsx"; -import { Input } from "../../ui/input.tsx"; +import { Label } from "../../../ui/label.tsx"; +import { Input } from "../../../ui/input.tsx"; import React from "react"; -import { useMainButton } from "../../lib/platform/use-main-button.ts"; +import { useMainButton } from "../../../lib/platform/use-main-button.ts"; import { useDeckFormStore } from "./store/deck-form-store-context.tsx"; -import { screenStore } from "../../store/screen-store.ts"; -import { useMount } from "../../lib/react/use-mount.ts"; -import { useBackButton } from "../../lib/platform/use-back-button.ts"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; -import { assert } from "../../lib/typescript/assert.ts"; -import { CardRow } from "../../ui/card-row.tsx"; -import { Button } from "../../ui/button.tsx"; -import { theme } from "../../ui/theme.tsx"; -import { t } from "../../translations/t.ts"; -import { deckListStore } from "../../store/deck-list-store.ts"; -import { reset } from "../../ui/reset.ts"; -import { Screen } from "../shared/screen.tsx"; -import { CenteredUnstyledButton } from "../../ui/centered-unstyled-button.tsx"; -import { ListHeader } from "../../ui/list-header.tsx"; -import { List } from "../../ui/list.tsx"; -import { FilledIcon } from "../../ui/filled-icon.tsx"; -import { ListRightText } from "../../ui/list-right-text.tsx"; -import { Flex } from "../../ui/flex.tsx"; -import { boolNarrow } from "../../lib/typescript/bool-narrow.ts"; +import { screenStore } from "../../../store/screen-store.ts"; +import { useMount } from "../../../lib/react/use-mount.ts"; +import { useBackButton } from "../../../lib/platform/use-back-button.ts"; +import { useProgress } from "../../../lib/platform/use-progress.tsx"; +import { assert } from "../../../lib/typescript/assert.ts"; +import { CardRow } from "../../../ui/card-row.tsx"; +import { Button } from "../../../ui/button.tsx"; +import { theme } from "../../../ui/theme.tsx"; +import { t } from "../../../translations/t.ts"; +import { deckListStore } from "../../../store/deck-list-store.ts"; +import { reset } from "../../../ui/reset.ts"; +import { Screen } from "../../shared/screen.tsx"; +import { CenteredUnstyledButton } from "../../../ui/centered-unstyled-button.tsx"; +import { ListHeader } from "../../../ui/list-header.tsx"; +import { List } from "../../../ui/list.tsx"; +import { FilledIcon } from "../../../ui/filled-icon.tsx"; +import { ListRightText } from "../../../ui/list-right-text.tsx"; +import { Flex } from "../../../ui/flex.tsx"; +import { boolNarrow } from "../../../lib/typescript/bool-narrow.ts"; import { isFormValid } from "mobx-form-lite"; -import { userStore } from "../../store/user-store.ts"; +import { userStore } from "../../../store/user-store.ts"; export const DeckForm = observer(() => { const deckFormStore = useDeckFormStore(); @@ -43,9 +43,9 @@ export const DeckForm = observer(() => { screenStore.back(); }); }); - useMainButtonProgress(() => deckFormStore.isSending); + useProgress(() => deckFormStore.isSending); - if (!deckFormStore.form) { + if (!deckFormStore.deckForm) { return null; } @@ -78,18 +78,18 @@ export const DeckForm = observer(() => { } > - {deckFormStore.form.cards.length > 0 && ( + {deckFormStore.deckForm.cards.length > 0 && ( { deckFormStore.goToCardList(); @@ -107,12 +107,12 @@ export const DeckForm = observer(() => { color: theme.hintColor, })} > - {deckFormStore.form.cards.length} + {deckFormStore.deckForm.cards.length} )} - {deckFormStore.form.id && ( + {deckFormStore.deckForm.id && (
{ right: ( { ), onClick: () => { if ( - !deckFormStore.form || - !isFormValid(deckFormStore.form) + !deckFormStore.deckForm || + !isFormValid(deckFormStore.deckForm) ) { return; } - const deckId = deckFormStore.form.id; + const deckId = deckFormStore.deckForm.id; assert(deckId, "Deck id should be defined"); screenStore.go({ type: "aiMassCreation", deckId: deckId, - deckTitle: deckFormStore.form.title.value, + deckTitle: deckFormStore.deckForm.title.value, + }); + }, + } + : undefined, + userStore.canUseAiMassGenerate + ? { + text: t("card_input_mode_screen"), + icon: ( + + ), + onClick: () => { + if ( + !deckFormStore.deckForm || + !isFormValid(deckFormStore.deckForm) + ) { + return; + } + const deckId = deckFormStore.deckForm.id; + assert(deckId, "Deck id should be defined"); + screenStore.go({ + type: "cardInputMode", + deckId: deckId, + cardInputModeId: deckFormStore.deckForm.cardInputModeId, }); }, } @@ -180,12 +206,12 @@ export const DeckForm = observer(() => { > {t("add_card")} - {deckFormStore.form.id ? ( + {deckFormStore.deckForm.id ? ( { - assert(deckFormStore.form); - assert(deckFormStore.form.id); - deckListStore.goDeckById(deckFormStore.form.id); + assert(deckFormStore.deckForm); + assert(deckFormStore.deckForm.id); + deckListStore.goDeckById(deckFormStore.deckForm.id); }} > {t("deck_preview")} diff --git a/src/screens/deck-form/speaking-cards.tsx b/src/screens/deck-form/deck-form/speaking-cards.tsx similarity index 59% rename from src/screens/deck-form/speaking-cards.tsx rename to src/screens/deck-form/deck-form/speaking-cards.tsx index 804c2a0b..e3eb909b 100644 --- a/src/screens/deck-form/speaking-cards.tsx +++ b/src/screens/deck-form/deck-form/speaking-cards.tsx @@ -1,28 +1,28 @@ import { observer } from "mobx-react-lite"; -import { Screen } from "../shared/screen.tsx"; -import { t } from "../../translations/t.ts"; -import { CardRow } from "../../ui/card-row.tsx"; -import { RadioSwitcher } from "../../ui/radio-switcher.tsx"; -import { Flex } from "../../ui/flex.tsx"; +import { Screen } from "../../shared/screen.tsx"; +import { t } from "../../../translations/t.ts"; +import { CardRow } from "../../../ui/card-row.tsx"; +import { RadioSwitcher } from "../../../ui/radio-switcher.tsx"; +import { Flex } from "../../../ui/flex.tsx"; import { css } from "@emotion/css"; -import { theme } from "../../ui/theme.tsx"; -import { Select } from "../../ui/select.tsx"; -import { enumEntries } from "../../lib/typescript/enum-values.ts"; +import { theme } from "../../../ui/theme.tsx"; +import { Select } from "../../../ui/select.tsx"; +import { enumEntries } from "../../../lib/typescript/enum-values.ts"; import { languageKeyToHuman, SpeakLanguageEnum, -} from "../../lib/voice-playback/speak.ts"; -import { DeckSpeakFieldEnum } from "../../../functions/db/deck/decks-with-cards-schema.ts"; -import { HintTransparent } from "../../ui/hint-transparent.tsx"; +} from "../../../lib/voice-playback/speak.ts"; +import { DeckSpeakFieldEnum } from "../../../../functions/db/deck/decks-with-cards-schema.ts"; +import { HintTransparent } from "../../../ui/hint-transparent.tsx"; import React from "react"; -import { useBackButton } from "../../lib/platform/use-back-button.ts"; -import { useMainButton } from "../../lib/platform/use-main-button.ts"; +import { useBackButton } from "../../../lib/platform/use-back-button.ts"; +import { useMainButton } from "../../../lib/platform/use-main-button.ts"; import { useDeckFormStore } from "./store/deck-form-store-context.tsx"; export const SpeakingCards = observer(() => { const deckFormStore = useDeckFormStore(); - if (!deckFormStore.form) { + if (!deckFormStore.deckForm) { return null; } @@ -51,10 +51,10 @@ export const SpeakingCards = observer(() => {
{t("voice_language")}
- {deckFormStore.form.speakingCardsLocale.value ? ( + {deckFormStore.deckForm.speakingCardsLocale.value ? ( - value={deckFormStore.form.speakingCardsLocale.value} - onChange={deckFormStore.form.speakingCardsLocale.onChange} + value={deckFormStore.deckForm.speakingCardsLocale.value} + onChange={deckFormStore.deckForm.speakingCardsLocale.onChange} options={enumEntries(SpeakLanguageEnum).map(([name, key]) => ({ value: key, label: languageKeyToHuman(name), @@ -67,10 +67,10 @@ export const SpeakingCards = observer(() => {
{t("card_speak_side")}
- {deckFormStore.form.speakingCardsField.value ? ( + {deckFormStore.deckForm.speakingCardsField.value ? ( - value={deckFormStore.form.speakingCardsField.value} - onChange={deckFormStore.form.speakingCardsField.onChange} + value={deckFormStore.deckForm.speakingCardsField.value} + onChange={deckFormStore.deckForm.speakingCardsField.onChange} options={[ { value: "front", label: t("front") }, { value: "back", label: t("back") }, diff --git a/src/screens/deck-form/store/card-form-store-interface.ts b/src/screens/deck-form/deck-form/store/card-form-store-interface.ts similarity index 66% rename from src/screens/deck-form/store/card-form-store-interface.ts rename to src/screens/deck-form/deck-form/store/card-form-store-interface.ts index 9d77b1cc..98f47d4c 100644 --- a/src/screens/deck-form/store/card-form-store-interface.ts +++ b/src/screens/deck-form/deck-form/store/card-form-store-interface.ts @@ -1,6 +1,6 @@ import { CardFormType } from "./deck-form-store.ts"; import { TextField } from "mobx-form-lite"; -import { DeckSpeakFieldEnum } from "../../../../functions/db/deck/decks-with-cards-schema.ts"; +import { DeckSpeakFieldEnum } from "../../../../../functions/db/deck/decks-with-cards-schema.ts"; export type CardInnerScreenType = | "cardPreview" @@ -9,6 +9,12 @@ export type CardInnerScreenType = | "aiSpeech" | null; +export type LimitedDeckForm = { + speakingCardsLocale: TextField; + speakingCardsField: TextField; + cardInputModeId: string | null; +}; + export interface CardFormStoreInterface { cardForm?: CardFormType | null; onSaveCard: () => void; @@ -17,10 +23,7 @@ export interface CardFormStoreInterface { isSending: boolean; markCardAsRemoved?: () => void; - form?: { - speakingCardsLocale: TextField; - speakingCardsField: TextField; - }; + deckForm?: LimitedDeckForm; // Navigation next and previous card isPreviousCardVisible?: boolean; diff --git a/src/screens/deck-form/store/deck-form-store-context.tsx b/src/screens/deck-form/deck-form/store/deck-form-store-context.tsx similarity index 89% rename from src/screens/deck-form/store/deck-form-store-context.tsx rename to src/screens/deck-form/deck-form/store/deck-form-store-context.tsx index 5a32b092..476ff5fe 100644 --- a/src/screens/deck-form/store/deck-form-store-context.tsx +++ b/src/screens/deck-form/deck-form/store/deck-form-store-context.tsx @@ -1,5 +1,5 @@ import React, { createContext, ReactNode, useContext } from "react"; -import { assert } from "../../../lib/typescript/assert.ts"; +import { assert } from "../../../../lib/typescript/assert.ts"; import { DeckFormStore } from "./deck-form-store.ts"; const Context = createContext(null); diff --git a/src/screens/deck-form/store/deck-form-store.test.ts b/src/screens/deck-form/deck-form/store/deck-form-store.test.ts similarity index 86% rename from src/screens/deck-form/store/deck-form-store.test.ts rename to src/screens/deck-form/deck-form/store/deck-form-store.test.ts index 02f11fba..eedc55e3 100644 --- a/src/screens/deck-form/store/deck-form-store.test.ts +++ b/src/screens/deck-form/deck-form/store/deck-form-store.test.ts @@ -1,12 +1,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { CardFormType, DeckFormStore } from "./deck-form-store.ts"; -import { DeckCardDbType } from "../../../../functions/db/deck/decks-with-cards-schema.ts"; -import { type DeckWithCardsWithReviewType } from "../../../store/deck-list-store.ts"; -import { assert } from "../../../lib/typescript/assert.ts"; +import { DeckCardDbType } from "../../../../../functions/db/deck/decks-with-cards-schema.ts"; +import { type DeckWithCardsWithReviewType } from "../../../../store/deck-list-store.ts"; +import { assert } from "../../../../lib/typescript/assert.ts"; import { UpsertDeckRequest, UpsertDeckResponse, -} from "../../../../functions/upsert-deck.ts"; +} from "../../../../../functions/upsert-deck.ts"; import { isFormValid } from "mobx-form-lite"; const mapUpsertDeckRequestToResponse = ( @@ -27,6 +27,7 @@ const mapUpsertDeckRequestToResponse = ( speak_field: null, deck_category: null, category_id: null, + card_input_mode_id: null, deck_card: input.cards.map((card) => { assert(input.id); return { @@ -53,7 +54,7 @@ const mocks = vi.hoisted(() => { }; }); -vi.mock("./../../../store/screen-store", () => { +vi.mock("../../../../store/screen-store.ts", () => { return { screenStore: { go: () => {}, @@ -65,7 +66,7 @@ vi.mock("./../../../store/screen-store", () => { }; }); -vi.mock("./../../../store/deck-list-store.ts", () => { +vi.mock("../../../../store/deck-list-store.ts", () => { const deckCardsMock: DeckCardDbType[] = [ { id: 3, @@ -125,37 +126,37 @@ vi.mock("./../../../store/deck-list-store.ts", () => { }; }); -vi.mock("../../../lib/platform/show-confirm.ts", () => { +vi.mock("../../../../lib/platform/show-confirm.ts", () => { return { showConfirm: () => {}, }; }); -vi.mock("../../../lib/platform/show-alert.ts", () => { +vi.mock("../../../../lib/platform/show-alert.ts", () => { return { showAlert: () => {}, }; }); -vi.mock("../../../translations/t.ts", () => { +vi.mock("../../../../translations/t.ts", () => { return { t: (val: string) => val, }; }); -vi.mock("../../../api/api.ts", () => { +vi.mock("../../../../api/api.ts", () => { return { upsertDeckRequest: mocks.upsertDeckRequest, }; }); -vi.mock("../../../lib/voice-playback/speak.ts", async () => { +vi.mock("../../../../lib/voice-playback/speak.ts", async () => { return { speak: () => {}, }; }); -vi.mock("../../../store/user-store.ts", () => { +vi.mock("../../../../store/user-store.ts", () => { return { userStore: { defaultCardType: "remember", @@ -163,7 +164,7 @@ vi.mock("../../../store/user-store.ts", () => { }; }); -vi.mock("../../shared/snackbar/snackbar.tsx", () => { +vi.mock("../../../shared/snackbar/snackbar.tsx", () => { return { notifyError: vi.fn(), notifySuccess: vi.fn(), @@ -178,8 +179,8 @@ describe("deck form store", () => { it("add 1 card, save", () => { const store = new DeckFormStore(); store.loadForm(); - assert(store.form); - expect(store.form.cards).toHaveLength(3); + assert(store.deckForm); + expect(store.deckForm.cards).toHaveLength(3); store.onDeckSave(); @@ -222,8 +223,8 @@ describe("deck form store", () => { it("edit 2 cards, add 1 new, save", () => { const store = new DeckFormStore(); store.loadForm(); - assert(store.form); - expect(store.form.cards).toHaveLength(3); + assert(store.deckForm); + expect(store.deckForm.cards).toHaveLength(3); store.editCardFormByIndex(0); assert(store.cardForm); @@ -280,25 +281,25 @@ describe("deck form store", () => { it("bug with card amount", () => { const store = new DeckFormStore(); store.loadForm(); - assert(store.form); - expect(store.form.cards).toHaveLength(3); + assert(store.deckForm); + expect(store.deckForm.cards).toHaveLength(3); store.openNewCardForm(); store.quitCardForm(); - expect(store.form.cards).toHaveLength(3); + expect(store.deckForm.cards).toHaveLength(3); store.editCardFormByIndex(2); store.quitCardForm(); - expect(store.form.cards).toHaveLength(3); + expect(store.deckForm.cards).toHaveLength(3); }); it("sorting - filtering cards", () => { const store = new DeckFormStore(); store.loadForm(); - assert(store.form); - expect(store.form.cards).toHaveLength(3); + assert(store.deckForm); + expect(store.deckForm.cards).toHaveLength(3); const cardToId = (card: CardFormType) => card.id; @@ -354,8 +355,8 @@ describe("deck form store", () => { expect(store.isNextCardVisible).toBeFalsy(); store.loadForm(); - assert(store.form); - expect(store.form.cards).toHaveLength(3); + assert(store.deckForm); + expect(store.deckForm.cards).toHaveLength(3); expect(store.filteredCards[0].id).toBe(5); store.editCardFormById(store.filteredCards[0].id); @@ -386,8 +387,8 @@ describe("deck form store", () => { expect(store.isNextCardVisible).toBeFalsy(); store.loadForm(); - assert(store.form); - expect(store.form.cards).toHaveLength(3); + assert(store.deckForm); + expect(store.deckForm.cards).toHaveLength(3); expect(store.filteredCards.map((card) => card.id)).toStrictEqual([5, 4, 3]); store.cardFilter.sortBy.onChange("frontAlpha"); diff --git a/src/screens/deck-form/store/deck-form-store.ts b/src/screens/deck-form/deck-form/store/deck-form-store.ts similarity index 79% rename from src/screens/deck-form/store/deck-form-store.ts rename to src/screens/deck-form/deck-form/store/deck-form-store.ts index 7301f522..efda0118 100644 --- a/src/screens/deck-form/store/deck-form-store.ts +++ b/src/screens/deck-form/deck-form/store/deck-form-store.ts @@ -10,30 +10,30 @@ import { validators, } from "mobx-form-lite"; import { action, makeAutoObservable, runInAction } from "mobx"; -import { assert } from "../../../lib/typescript/assert.ts"; -import { upsertDeckRequest } from "../../../api/api.ts"; -import { screenStore } from "../../../store/screen-store.ts"; -import { deckListStore } from "../../../store/deck-list-store.ts"; -import { showConfirm } from "../../../lib/platform/show-confirm.ts"; -import { fuzzySearch } from "../../../lib/string/fuzzy-search.ts"; +import { assert } from "../../../../lib/typescript/assert.ts"; +import { upsertDeckRequest } from "../../../../api/api.ts"; +import { screenStore } from "../../../../store/screen-store.ts"; +import { deckListStore } from "../../../../store/deck-list-store.ts"; +import { showConfirm } from "../../../../lib/platform/show-confirm.ts"; +import { fuzzySearch } from "../../../../lib/string/fuzzy-search.ts"; import { DeckCardDbType, DeckCardOptionsDbType, DeckSpeakFieldEnum, DeckWithCardsDbType, -} from "../../../../functions/db/deck/decks-with-cards-schema.ts"; -import { SpeakLanguageEnum } from "../../../lib/voice-playback/speak.ts"; -import { t } from "../../../translations/t.ts"; -import { CardAnswerType } from "../../../../functions/db/custom-types.ts"; +} from "../../../../../functions/db/deck/decks-with-cards-schema.ts"; +import { SpeakLanguageEnum } from "../../../../lib/voice-playback/speak.ts"; +import { t } from "../../../../translations/t.ts"; +import { CardAnswerType } from "../../../../../functions/db/custom-types.ts"; import { v4 } from "uuid"; import { CardFormStoreInterface, CardInnerScreenType, } from "./card-form-store-interface.ts"; -import { UpsertDeckRequest } from "../../../../functions/upsert-deck.ts"; -import { UnwrapArray } from "../../../lib/typescript/unwrap-array.ts"; -import { RequestStore } from "../../../lib/mobx-request/request-store.ts"; -import { notifyError } from "../../shared/snackbar/snackbar.tsx"; +import { UpsertDeckRequest } from "../../../../../functions/upsert-deck.ts"; +import { UnwrapArray } from "../../../../lib/typescript/unwrap-array.ts"; +import { RequestStore } from "../../../../lib/mobx-request/request-store.ts"; +import { notifyError } from "../../../shared/snackbar/snackbar.tsx"; export type CardAnswerFormType = { id: string; @@ -61,6 +61,7 @@ type DeckFormType = { speakingCardsLocale: TextField; speakingCardsField: TextField; folderId?: number; + cardInputModeId: string | null; cardsToRemoveIds: number[]; }; @@ -126,6 +127,7 @@ const createUpdateForm = ( description: new TextField(deck.description ?? ""), speakingCardsLocale: new TextField(deck.speak_locale), speakingCardsField: new TextField(deck.speak_field), + cardInputModeId: deck.card_input_mode_id || null, cards: deck.deck_card.map((card) => ({ id: card.id, front: createCardSideField(card.front), @@ -172,7 +174,7 @@ export type CardFilterDirection = "desc" | "asc"; export class DeckFormStore implements CardFormStoreInterface { cardFormIndex?: number; cardFormType?: "new" | "edit"; - form?: DeckFormType; + deckForm?: DeckFormType; upsertDeckRequest = new RequestStore(upsertDeckRequest); cardInnerScreen = new TextField(null); deckInnerScreen?: "cardList" | "speakingCards"; @@ -201,7 +203,7 @@ export class DeckFormStore implements CardFormStoreInterface { } loadForm() { - if (this.form) { + if (this.deckForm) { return; } @@ -211,9 +213,13 @@ export class DeckFormStore implements CardFormStoreInterface { if (screen.deckId) { const deck = deckListStore.searchDeckById(screen.deckId); assert(deck, "Deck not found in deckListStore"); - this.form = createUpdateForm(screen.deckId, deck, () => this.cardForm); + this.deckForm = createUpdateForm( + screen.deckId, + deck, + () => this.cardForm, + ); } else { - this.form = { + this.deckForm = { title: createDeckTitleField(""), description: new TextField(""), cards: [], @@ -221,23 +227,28 @@ export class DeckFormStore implements CardFormStoreInterface { speakingCardsField: new TextField(null), folderId: screen.folder?.id ?? undefined, cardsToRemoveIds: [], + cardInputModeId: null, }; } + + if (screen.cardId) { + this.editCardFormById(screen.cardId); + } } goToSpeakingCards() { - if (!this.form || !isFormValid(this.form)) { + if (!this.deckForm || !isFormValid(this.deckForm)) { return; } this.deckInnerScreen = "speakingCards"; } goToCardList() { - if (!this.form) { + if (!this.deckForm) { return; } - if (!isFormValid(this.form)) { - formTouchAll(this.form); + if (!isFormValid(this.deckForm)) { + formTouchAll(this.deckForm); return; } this.deckInnerScreen = "cardList"; @@ -248,11 +259,11 @@ export class DeckFormStore implements CardFormStoreInterface { } get filteredCards() { - if (!this.form) { + if (!this.deckForm) { return []; } - return this.form.cards + return this.deckForm.cards .filter((card) => { if (this.cardFilter.text.value) { const textFilter = this.cardFilter.text.value.toLowerCase(); @@ -307,10 +318,10 @@ export class DeckFormStore implements CardFormStoreInterface { } toggleIsSpeakingCardEnabled() { - if (!this.form) { + if (!this.deckForm) { return; } - const { speakingCardsLocale, speakingCardsField } = this.form; + const { speakingCardsLocale, speakingCardsField } = this.deckForm; if (speakingCardsLocale.value && speakingCardsField.value) { speakingCardsLocale.onChange(null); @@ -323,27 +334,27 @@ export class DeckFormStore implements CardFormStoreInterface { get isSpeakingCardEnabled() { return ( - !!this.form?.speakingCardsLocale.value && - !!this.form?.speakingCardsField.value + !!this.deckForm?.speakingCardsLocale.value && + !!this.deckForm?.speakingCardsField.value ); } get cardForm() { - return this.form !== undefined && this.cardFormIndex !== undefined - ? this.form.cards[this.cardFormIndex] + return this.deckForm !== undefined && this.cardFormIndex !== undefined + ? this.deckForm.cards[this.cardFormIndex] : null; } openNewCardForm() { - assert(this.form, "openNewCardForm: form is empty"); - if (!isFormValid(this.form)) { - formTouchAll(this.form); + assert(this.deckForm, "openNewCardForm: form is empty"); + if (!isFormValid(this.deckForm)) { + formTouchAll(this.deckForm); return; } - this.cardFormIndex = this.form.cards.length; + this.cardFormIndex = this.deckForm.cards.length; this.cardFormType = "new"; - this.form.cards.push({ + this.deckForm.cards.push({ front: createCardSideField(""), back: createCardSideField(""), example: new TextField(""), @@ -359,10 +370,12 @@ export class DeckFormStore implements CardFormStoreInterface { } editCardFormById(cardId?: number) { - if (!cardId || !this.form) { + if (!cardId || !this.deckForm) { return; } - const cardIndex = this.form.cards.findIndex((card) => card.id === cardId); + const cardIndex = this.deckForm.cards.findIndex( + (card) => card.id === cardId, + ); if (cardIndex !== -1) { this.editCardFormByIndex(cardIndex); } @@ -445,8 +458,8 @@ export class DeckFormStore implements CardFormStoreInterface { } async onDeckBack(redirect: () => void) { - assert(this.form, "onDeckBack: form is empty"); - if (isFormEmpty(this.form) || !isFormDirty(this.form)) { + assert(this.deckForm, "onDeckBack: form is empty"); + if (isFormEmpty(this.deckForm) || !isFormDirty(this.deckForm)) { redirect(); return; } @@ -468,12 +481,12 @@ export class DeckFormStore implements CardFormStoreInterface { if (!selectedCard) { return; } - assert(this.form, "markCardAsRemoved: form is empty"); + assert(this.deckForm, "markCardAsRemoved: form is empty"); if (!selectedCard.id) { return; } - this.form.cardsToRemoveIds.push(selectedCard.id); + this.deckForm.cardsToRemoveIds.push(selectedCard.id); deckListStore.isAppLoading = true; }); @@ -491,10 +504,10 @@ export class DeckFormStore implements CardFormStoreInterface { } async onDeckSave(onSuccess?: () => void) { - assert(this.form, "onDeckSave: form is empty"); + assert(this.deckForm, "onDeckSave: form is empty"); - if (!isFormValid(this.form)) { - formTouchAll(this.form); + if (!isFormValid(this.deckForm)) { + formTouchAll(this.deckForm); return Promise.reject(); } @@ -506,21 +519,21 @@ export class DeckFormStore implements CardFormStoreInterface { // Avoid sending huge collections on every save // Only new and touched cards are sent to the server - const newCards = this.form.cards.filter((card) => !card.id); - const touchedCards = this.form.cards.filter( + const newCards = this.deckForm.cards.filter((card) => !card.id); + const touchedCards = this.deckForm.cards.filter( (card) => !!(card.id && (isFormTouched(card) || isFormDirty(card))), ); const cardsToSend = newCards.concat(touchedCards).map(cardFormToApi); const result = await this.upsertDeckRequest.execute({ - id: this.form.id, - title: this.form.title.value, - description: this.form.description.value, + id: this.deckForm.id, + title: this.deckForm.title.value, + description: this.deckForm.description.value, cards: cardsToSend, - speakLocale: this.form.speakingCardsLocale.value, - speakField: this.form.speakingCardsField.value, - folderId: this.form.folderId, - cardsToRemoveIds: this.form.cardsToRemoveIds, + speakLocale: this.deckForm.speakingCardsLocale.value, + speakField: this.deckForm.speakingCardsField.value, + folderId: this.deckForm.folderId, + cardsToRemoveIds: this.deckForm.cardsToRemoveIds, }); if (result.status === "error") { @@ -531,7 +544,7 @@ export class DeckFormStore implements CardFormStoreInterface { const { deck, folders, cardsToReview } = result.data; runInAction(() => { - this.form = createUpdateForm(deck.id, deck, () => this.cardForm); + this.deckForm = createUpdateForm(deck.id, deck, () => this.cardForm); deckListStore.replaceDeck(deck, true); deckListStore.updateFolders(folders); deckListStore.updateCardsToReview(cardsToReview); @@ -544,9 +557,9 @@ export class DeckFormStore implements CardFormStoreInterface { this.cardFormIndex !== undefined, "quitCardForm: cardFormIndex is empty", ); - assert(this.form, "quitCardForm: form is empty"); + assert(this.deckForm, "quitCardForm: form is empty"); if (this.cardFormType === "new") { - this.form.cards.splice(this.cardFormIndex, 1); + this.deckForm.cards.splice(this.cardFormIndex, 1); } this.cardFormIndex = undefined; this.cardFormType = undefined; diff --git a/src/screens/deck-list/main-screen.tsx b/src/screens/deck-list/main-screen.tsx index fcf85967..5324a801 100644 --- a/src/screens/deck-list/main-screen.tsx +++ b/src/screens/deck-list/main-screen.tsx @@ -9,7 +9,7 @@ import { Hint } from "../../ui/hint.tsx"; import { theme } from "../../ui/theme.tsx"; import { screenStore } from "../../store/screen-store.ts"; import { Button } from "../../ui/button.tsx"; -import { DeckLoading } from "../shared/deck-loading.tsx"; +import { CardRowLoading } from "../shared/card-row-loading.tsx"; import { ListHeader } from "../../ui/list-header.tsx"; import { range } from "../../lib/array/range.ts"; import { reset } from "../../ui/reset.ts"; @@ -42,7 +42,7 @@ export const MainScreen = observer(() => { {deckListStore.myInfoRequest.isLoading && range(deckListStore.skeletonLoaderData.myDecksCount).map((i) => ( - + ))} {deckListStore.myInfo ? deckListStore.myDeckItemsVisible.map((listItem) => { @@ -148,7 +148,7 @@ export const MainScreen = observer(() => { {deckListStore.myInfoRequest.isLoading && range(deckListStore.skeletonLoaderData.publicCount).map((i) => ( - + ))}
diff --git a/src/screens/deck-review/deck-finished.tsx b/src/screens/deck-review/deck-finished.tsx index 386218a4..87c1189c 100644 --- a/src/screens/deck-review/deck-finished.tsx +++ b/src/screens/deck-review/deck-finished.tsx @@ -5,7 +5,7 @@ import { useReviewStore } from "./store/review-store-context.tsx"; import { useMount } from "../../lib/react/use-mount.ts"; import { screenStore } from "../../store/screen-store.ts"; import { useMainButton } from "../../lib/platform/use-main-button.ts"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; import { t } from "../../translations/t.ts"; import { getEncouragingMessage } from "../../translations/get-encouraging-message.tsx"; import { WantMoreCardsButton } from "./want-more-cards-button.tsx"; @@ -29,7 +29,7 @@ export const DeckFinished = observer((props: Props) => { useMainButton(t("go_back"), () => { screenStore.go({ type: "main" }); }); - useMainButtonProgress(() => reviewStore.reviewCardsRequest.isLoading); + useProgress(() => reviewStore.reviewCardsRequest.isLoading); return ( diff --git a/src/screens/deck-review/deck-preview.tsx b/src/screens/deck-review/deck-preview.tsx index 85fe56e1..7de78294 100644 --- a/src/screens/deck-review/deck-preview.tsx +++ b/src/screens/deck-review/deck-preview.tsx @@ -10,7 +10,7 @@ import { useBackButton } from "../../lib/platform/use-back-button.ts"; import { useMainButton } from "../../lib/platform/use-main-button.ts"; import { showConfirm } from "../../lib/platform/show-confirm.ts"; import { ButtonSideAligned } from "../../ui/button-side-aligned.tsx"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; import { t } from "../../translations/t.ts"; import { ButtonGrid } from "../../ui/button-grid.tsx"; import { Button } from "../../ui/button.tsx"; @@ -27,7 +27,7 @@ export const DeckPreview = observer(() => { screenStore.back(); }); - useMainButtonProgress(() => deckListStore.deckWithCardsRequest.isLoading); + useProgress(() => deckListStore.deckWithCardsRequest.isLoading); useScrollToTopOnMount(); useMainButton( diff --git a/src/screens/deck-review/store/card-preview-store.ts b/src/screens/deck-review/store/card-preview-store.ts index 0d210e47..3b9ff656 100644 --- a/src/screens/deck-review/store/card-preview-store.ts +++ b/src/screens/deck-review/store/card-preview-store.ts @@ -13,7 +13,7 @@ import { SpeakLanguageEnum, } from "../../../lib/voice-playback/speak.ts"; import { removeAllTags } from "../../../lib/sanitize-html/remove-all-tags.ts"; -import { CardFormStoreInterface } from "../../deck-form/store/card-form-store-interface.ts"; +import { CardFormStoreInterface } from "../../deck-form/deck-form/store/card-form-store-interface.ts"; import { assert } from "../../../lib/typescript/assert.ts"; import { BooleanToggle } from "mobx-form-lite"; @@ -57,7 +57,7 @@ export class CardPreviewStore implements LimitedCardUnderReviewStore { isCorrect: answer.isCorrect.value, })); - const deckForm = cardFormStore.form; + const deckForm = cardFormStore.deckForm; if (!deckForm) { return; } @@ -114,7 +114,6 @@ export class CardPreviewStore implements LimitedCardUnderReviewStore { speak(removeAllTags(text), this.deckSpeakLocale); } - isCardSpeakerVisible(type: "front" | "back") { if (!userStore.isSpeakingCardsEnabled) { return false; diff --git a/src/screens/deck-review/store/review-store.test.ts b/src/screens/deck-review/store/review-store.test.ts index 3115128a..aa598021 100644 --- a/src/screens/deck-review/store/review-store.test.ts +++ b/src/screens/deck-review/store/review-store.test.ts @@ -41,6 +41,7 @@ const createDeckWithCards = (cards: DeckCardDbTypeWithType[]) => { available_in: null, deck_category: null, category_id: null, + card_input_mode_id: null, }; return deckMock; }; diff --git a/src/screens/folder-form/folder-form.tsx b/src/screens/folder-form/folder-form.tsx index b499fde9..5a0e8451 100644 --- a/src/screens/folder-form/folder-form.tsx +++ b/src/screens/folder-form/folder-form.tsx @@ -6,7 +6,7 @@ import { Input } from "../../ui/input.tsx"; import React from "react"; import { useBackButton } from "../../lib/platform/use-back-button.ts"; import { screenStore } from "../../store/screen-store.ts"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; import { useMount } from "../../lib/react/use-mount.ts"; import { useMainButton } from "../../lib/platform/use-main-button.ts"; import { assert } from "../../lib/typescript/assert.ts"; @@ -37,7 +37,7 @@ export const FolderForm = observer(() => { folderStore.onBack(); }); - useMainButtonProgress(() => folderStore.folderUpsertRequest.isLoading); + useProgress(() => folderStore.folderUpsertRequest.isLoading); if (!folderForm) { return null; diff --git a/src/screens/folder-review/folder-preview.tsx b/src/screens/folder-review/folder-preview.tsx index e70e951d..968a83c5 100644 --- a/src/screens/folder-review/folder-preview.tsx +++ b/src/screens/folder-review/folder-preview.tsx @@ -9,7 +9,7 @@ import { useBackButton } from "../../lib/platform/use-back-button.ts"; import { useMainButton } from "../../lib/platform/use-main-button.ts"; import { showConfirm } from "../../lib/platform/show-confirm.ts"; import { ButtonSideAligned } from "../../ui/button-side-aligned.tsx"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; import { t } from "../../translations/t.ts"; import { useReviewStore } from "../deck-review/store/review-store-context.tsx"; import { ListHeader } from "../../ui/list-header.tsx"; @@ -29,7 +29,7 @@ export const FolderPreview = observer(() => { screenStore.back(); }); - useMainButtonProgress(() => deckListStore.isCatalogItemLoading); + useProgress(() => deckListStore.isCatalogItemLoading); useScrollToTopOnMount(); useMainButton( diff --git a/src/screens/freeze-cards/freeze-cards-screen.tsx b/src/screens/freeze-cards/freeze-cards-screen.tsx index 3f2fc4fa..5559dadc 100644 --- a/src/screens/freeze-cards/freeze-cards-screen.tsx +++ b/src/screens/freeze-cards/freeze-cards-screen.tsx @@ -13,7 +13,7 @@ import { Chip } from "../../ui/chip.tsx"; import { FreezeCardsStore } from "./store/freeze-cards-store.ts"; import { FilledIcon } from "../../ui/filled-icon.tsx"; import { Accordion } from "../../ui/accordion.tsx"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; import { t } from "../../translations/t.ts"; import { formatDays } from "./translations.ts"; @@ -30,7 +30,7 @@ export const FreezeCardsScreen = observer(() => { store.freeze, () => store.isFreezeButtonVisible, ); - useMainButtonProgress(() => store.cardsFreezeRequest.isLoading); + useProgress(() => store.cardsFreezeRequest.isLoading); return ( diff --git a/src/screens/plans/plans-screen.tsx b/src/screens/plans/plans-screen.tsx index 63623b44..236fdb37 100644 --- a/src/screens/plans/plans-screen.tsx +++ b/src/screens/plans/plans-screen.tsx @@ -15,7 +15,7 @@ import { getPlanTitle, } from "./translations.ts"; import { PlansScreenStore } from "./store/plans-screen-store.ts"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; import { userStore } from "../../store/user-store.ts"; import { ExternalLink } from "../../ui/external-link.tsx"; import { t } from "../../translations/t.ts"; @@ -39,7 +39,7 @@ export const PlansScreen = observer(() => { () => store.isBuyButtonVisible, ); - useMainButtonProgress(() => store.createOrderRequest.isLoading); + useProgress(() => store.createOrderRequest.isLoading); if (store.plansRequest.result.status === "loading") { return ; diff --git a/src/screens/share-deck/share-deck-settings.tsx b/src/screens/share-deck/share-deck-settings.tsx index e8466c65..859cba2c 100644 --- a/src/screens/share-deck/share-deck-settings.tsx +++ b/src/screens/share-deck/share-deck-settings.tsx @@ -3,7 +3,7 @@ import { useBackButton } from "../../lib/platform/use-back-button.ts"; import { screenStore } from "../../store/screen-store.ts"; import { useMainButton } from "../../lib/platform/use-main-button.ts"; import { t } from "../../translations/t.ts"; -import { useMainButtonProgress } from "../../lib/platform/use-main-button-progress.tsx"; +import { useProgress } from "../../lib/platform/use-progress.tsx"; import { CardRow } from "../../ui/card-row.tsx"; import { RadioSwitcher } from "../../ui/radio-switcher.tsx"; import { HintTransparent } from "../../ui/hint-transparent.tsx"; @@ -32,7 +32,7 @@ export const ShareDeckSettings = observer(() => { () => store.isSaveButtonVisible, ); - useMainButtonProgress(() => store.addDeckAccessRequest.isLoading); + useProgress(() => store.addDeckAccessRequest.isLoading); return ( diff --git a/src/screens/shared/deck-loading.tsx b/src/screens/shared/card-row-loading.tsx similarity index 94% rename from src/screens/shared/deck-loading.tsx rename to src/screens/shared/card-row-loading.tsx index 9521b8c5..a3a64282 100644 --- a/src/screens/shared/deck-loading.tsx +++ b/src/screens/shared/card-row-loading.tsx @@ -7,7 +7,7 @@ type Props = { speed?: number; }; -export const DeckLoading = (props: Props) => { +export const CardRowLoading = (props: Props) => { const speed = props.speed || 2; return (
{ useBackButton(() => { screenStore.back(); }); - useMainButtonProgress(() => userSettingsStore.userSettingsRequest.isLoading); + useProgress(() => userSettingsStore.userSettingsRequest.isLoading); if (!deckListStore.myInfo || !userSettingsStore.form) { return null; diff --git a/src/screens/user-statistics/user-statistics-screen.tsx b/src/screens/user-statistics/user-statistics-screen.tsx index 8216fab8..3cc77e34 100644 --- a/src/screens/user-statistics/user-statistics-screen.tsx +++ b/src/screens/user-statistics/user-statistics-screen.tsx @@ -15,7 +15,7 @@ import { PieChartCanvas, } from "./pie-chart-canvas.tsx"; import { LegendItem } from "./legend-item.tsx"; -import { DeckLoading } from "../shared/deck-loading.tsx"; +import { CardRowLoading } from "../shared/card-row-loading.tsx"; import { EmptyStudyFrequencyChartText } from "./empty-study-frequency-chart-text.tsx"; import { Flex } from "../../ui/flex.tsx"; @@ -36,7 +36,7 @@ export const UserStatisticsScreen = observer(() => { return ( {userStatisticsStore.userStatisticsRequest.isLoading ? ( - + ) : ( {t("user_stats_remembered")} @@ -46,7 +46,7 @@ export const UserStatisticsScreen = observer(() => { {t("user_stats_remembered_hint")} {userStatisticsStore.userStatisticsRequest.isLoading ? ( - + ) : ( {t("user_stats_learning")} @@ -56,7 +56,7 @@ export const UserStatisticsScreen = observer(() => { {t("user_stats_learning_hint")} {userStatisticsStore.userStatisticsRequest.isLoading ? ( - + ) : ( {t("user_stats_total")} diff --git a/src/store/deck-list-store.test.ts b/src/store/deck-list-store.test.ts index 728b0d97..c7acf8dd 100644 --- a/src/store/deck-list-store.test.ts +++ b/src/store/deck-list-store.test.ts @@ -71,6 +71,7 @@ vi.mock("../api/api.ts", () => { speak_field: null, category_id: null, deck_category: null, + card_input_mode_id: null, deck_card: [ { id: 1, @@ -131,6 +132,7 @@ vi.mock("../api/api.ts", () => { speak_field: null, deck_category: null, category_id: null, + card_input_mode_id: null, deck_card: [ { id: 4, diff --git a/src/store/deck-list-store.ts b/src/store/deck-list-store.ts index 081e5b07..fca7abcd 100644 --- a/src/store/deck-list-store.ts +++ b/src/store/deck-list-store.ts @@ -145,6 +145,18 @@ export class DeckListStore { screenStore.go({ type: "folderPreview", folderId: folder.id }); } + addCardOptimistic(card: DeckCardDbType) { + const deck = this.searchDeckById(card.deck_id); + if (!deck || !this.myInfo) { + return; + } + deck.deck_card.push(card); + this.myInfo.cardsToReview.push({ + ...card, + type: "new", + }); + } + async openFolderFromCatalog(folderWithoutDecks: CatalogFolderDbType) { assert(this.myInfo); if ( @@ -447,6 +459,14 @@ export class DeckListStore { } } + updateDeckCardInputMode(deckId: number, cardInputModeId: string | null) { + const deck = this.searchDeckById(deckId); + if (!deck) { + return null; + } + deck.card_input_mode_id = cardInputModeId; + } + get publicDecks() { if (!this.myInfo) { return []; @@ -635,12 +655,16 @@ export class DeckListStore { } updateFolders(body: FolderWithDeckIdDbType[]) { - assert(this.myInfo, "myInfo is not loaded in updateFolders"); + if (!this.myInfo) { + return; + } this.myInfo.folders = body; } updateCardsToReview(body: CardToReviewDbType[]) { - assert(this.myInfo, "myInfo is not loaded in updateCardsToReview"); + if (!this.myInfo) { + return; + } this.myInfo.cardsToReview = body; } diff --git a/src/store/screen-store.ts b/src/store/screen-store.ts index 594dd9b1..877cefad 100644 --- a/src/store/screen-store.ts +++ b/src/store/screen-store.ts @@ -1,11 +1,17 @@ import { makeAutoObservable } from "mobx"; -import { CardFormType } from "../screens/deck-form/store/deck-form-store.ts"; +import { CardFormType } from "../screens/deck-form/deck-form/store/deck-form-store.ts"; +import { makeLoggable } from "mobx-log"; type Route = | { type: "main" } | { type: "deckMine"; deckId: number } | { type: "deckPublic"; deckId: number } - | { type: "deckForm"; deckId?: number; folder?: { id: number; name: string } } + | { + type: "deckForm"; + deckId?: number; + folder?: { id: number; name: string }; + cardId?: number; + } | { type: "cardPreview"; form: CardFormType } | { type: "folderForm"; folderId?: number } | { type: "folderPreview"; folderId: number } @@ -16,6 +22,11 @@ type Route = | { type: "shareDeck"; deckId: number; shareId: string } | { type: "shareFolder"; folderId: number; shareId: string } | { type: "aiMassCreation"; deckId: number; deckTitle: string | null } + | { + type: "cardInputMode"; + deckId: number; + cardInputModeId: string | null; + } | { type: "plans" } | { type: "debug" } | { type: "componentCatalog" } @@ -30,6 +41,7 @@ export class ScreenStore { constructor() { makeAutoObservable(this, {}, { autoBind: true }); + makeLoggable(this); } go(historyData: Route) { diff --git a/src/translations/t.ts b/src/translations/t.ts index e3f33918..874de937 100644 --- a/src/translations/t.ts +++ b/src/translations/t.ts @@ -2,6 +2,9 @@ import { Translator } from "../lib/translator/translator.ts"; import { platform } from "../lib/platform/platform.ts"; const en = { + card_input_mode_changed: "Card input mode has been changed", + card_input_mode_screen: "Card input mode", + generate: "Generate", folder_form_no_decks: "No decks in the folder", cards_add: "Add cards", card_next: "Next", @@ -260,6 +263,9 @@ const en = { type Translation = typeof en; const ru: Translation = { + card_input_mode_changed: "Режим ввода карточки изменен", + card_input_mode_screen: "Режим ввода карточки", + generate: "Сгенерировать", yes: "Да", ai_speech_title: "ИИ речь", ai_speech_empty: "Речь не создана. Выберите сторону карточки ниже.", @@ -514,6 +520,9 @@ const ru: Translation = { }; const es: Translation = { + card_input_mode_changed: "Modo de entrada de tarjeta cambiado", + generate: "Generar", + card_input_mode_screen: "Modo de entrada de tarjeta", yes: "Sí", ai_speech_validate: "Por favor, selecciona una cara o escribe el texto", ai_speech_type: "O escribe el texto necesario", @@ -771,6 +780,9 @@ const es: Translation = { }; const ptBr: Translation = { + card_input_mode_changed: "Modo de entrada de cartão alterado", + generate: "Gerar", + card_input_mode_screen: "Modo de entrada de cartão", yes: "Sim", ai_speech_generate: "Gerar", ai_speech_title: "Voz de IA", diff --git a/src/ui/radio-list/radio-box-empty.tsx b/src/ui/radio-list/radio-box-empty.tsx new file mode 100644 index 00000000..7a7f0358 --- /dev/null +++ b/src/ui/radio-list/radio-box-empty.tsx @@ -0,0 +1,17 @@ +import { theme } from "../theme.tsx"; + +export const RadioBoxEmpty = () => ( + + + +); diff --git a/src/ui/radio-list/radio-box-filled.tsx b/src/ui/radio-list/radio-box-filled.tsx new file mode 100644 index 00000000..4613bc95 --- /dev/null +++ b/src/ui/radio-list/radio-box-filled.tsx @@ -0,0 +1,17 @@ +import { theme } from "../theme.tsx"; + +export const RadioBoxFilled = () => ( + + + +); diff --git a/src/ui/radio-list/radio-list.tsx b/src/ui/radio-list/radio-list.tsx new file mode 100644 index 00000000..0cae0802 --- /dev/null +++ b/src/ui/radio-list/radio-list.tsx @@ -0,0 +1,47 @@ +import { theme } from "../theme.tsx"; +import { Flex } from "../flex.tsx"; +import { css } from "@emotion/css"; +import { ReactNode } from "react"; +import { RadioBoxFilled } from "./radio-box-filled.tsx"; +import { RadioBoxEmpty } from "./radio-box-empty.tsx"; + +type RadioItemId = string | null; + +type Props = { + selectedId: T; + options: Array<{ id: T; title: ReactNode }>; + onChange: (selectedId: T) => void; +}; + +export const RadioList = (props: Props) => { + const { selectedId, options, onChange } = props; + + return ( + + {options.map((option) => { + const isSelected = selectedId === option.id; + return ( +
onChange(option.id)} + > + {isSelected ? : } + {option.title} +
+ ); + })} +
+ ); +};