diff --git a/functions/upsert-deck.ts b/functions/upsert-deck.ts index 18de4a85..9634a6f5 100644 --- a/functions/upsert-deck.ts +++ b/functions/upsert-deck.ts @@ -9,7 +9,7 @@ import { DatabaseException } from "./db/database-exception.ts"; import { createJsonResponse } from "./lib/json-response/create-json-response.ts"; import { deckSchema, - deckWithCardsSchema, + DeckWithCardsDbType, } from "./db/deck/decks-with-cards-schema.ts"; import { addDeckToMineDb } from "./db/deck/add-deck-to-mine-db.ts"; import { createForbiddenRequestResponse } from "./lib/json-response/create-forbidden-request-response.ts"; @@ -17,6 +17,14 @@ import { getDeckByIdAndAuthorId } from "./db/deck/get-deck-by-id-and-author-id.t import { shortUniqueId } from "./lib/short-unique-id/short-unique-id.ts"; import { Database } from "./db/databaseTypes.ts"; import { getDeckWithCardsById } from "./db/deck/get-deck-with-cards-by-id-db.ts"; +import { + getFoldersWithDecksDb, + UserFoldersDbType, +} from "./db/folder/get-folders-with-decks-db.tsx"; +import { + CardToReviewDbType, + getCardsToReviewDb, +} from "./db/deck/get-cards-to-review-db.ts"; const requestSchema = z.object({ id: z.number().nullable().optional(), @@ -24,6 +32,7 @@ const requestSchema = z.object({ description: z.string().nullable().optional(), speakLocale: z.string().nullable().optional(), speakField: z.string().nullable().optional(), + folderId: z.number().nullable().optional(), cards: z.array( z.object({ front: z.string(), @@ -35,7 +44,11 @@ const requestSchema = z.object({ }); export type UpsertDeckRequest = z.infer; -export type UpsertDeckResponse = z.infer; +export type UpsertDeckResponse = { + deck: DeckWithCardsDbType; + folders: UserFoldersDbType[]; + cardsToReview: CardToReviewDbType[]; +}; type InsertDeckDatabaseType = Database["public"]["Tables"]["deck"]["Insert"]; type DeckRow = Database["public"]["Tables"]["deck"]["Row"]; @@ -116,15 +129,30 @@ export const onRequestPost = handleError(async ({ request, env }) => { throw new DatabaseException(createCardsResult.error); } + // If create deck if (!input.data.id) { await addDeckToMineDb(envSafe, { user_id: user.id, deck_id: upsertedDeck.id, }); + + // If folderId passed - add the new deck to folder + if (input.data.folderId) { + await db.from("deck_folder").upsert({ + deck_id: upsertedDeck.id, + folder_id: input.data.folderId, + }); + } } + const [deck, folders, cardsToReview] = await Promise.all([ + getDeckWithCardsById(envSafe, upsertedDeck.id), + getFoldersWithDecksDb(envSafe, user.id), + getCardsToReviewDb(envSafe, user.id), + ]); + return createJsonResponse( - await getDeckWithCardsById(envSafe, upsertedDeck.id), + { deck, folders, cardsToReview }, 200, ); }); diff --git a/src/screens/deck-form/deck-form.tsx b/src/screens/deck-form/deck-form.tsx index 42736ac9..5ae66223 100644 --- a/src/screens/deck-form/deck-form.tsx +++ b/src/screens/deck-form/deck-form.tsx @@ -52,7 +52,31 @@ export const DeckForm = observer(() => { } return ( - + + to folder{" "} + + + ) : undefined + } + > @@ -155,7 +179,7 @@ export const DeckForm = observer(() => { onClick={() => { assert(deckFormStore.form); assert(deckFormStore.form.id); - deckListStore.goDeckById(deckFormStore.form.id); + deckListStore.goDeckById(deckFormStore.form.id, "main"); }} > {t("deck_preview")} diff --git a/src/screens/deck-form/store/deck-form-store.test.ts b/src/screens/deck-form/store/deck-form-store.test.ts index 084a60f4..4b89516a 100644 --- a/src/screens/deck-form/store/deck-form-store.test.ts +++ b/src/screens/deck-form/store/deck-form-store.test.ts @@ -12,29 +12,33 @@ import { isFormValid } from "../../../lib/mobx-form/form-has-error.ts"; const mapUpsertDeckRequestToResponse = ( input: UpsertDeckRequest, ): UpsertDeckResponse => ({ - id: input.id || 9999, - available_in: null, - description: input.description ?? null, - created_at: new Date().toISOString(), - name: input.title, - author_id: 9999, - share_id: "share_id_mock", - is_public: false, - speak_locale: null, - speak_field: null, - deck_category: null, - category_id: null, - deck_card: input.cards.map((card) => { - assert(input.id); - return { - id: card.id || 9999, - deck_id: input.id, - created_at: new Date().toISOString(), - example: card.example ?? null, - front: card.front, - back: card.back, - }; - }), + folders: [], + cardsToReview: [], + deck: { + id: input.id || 9999, + available_in: null, + description: input.description ?? null, + created_at: new Date().toISOString(), + name: input.title, + author_id: 9999, + share_id: "share_id_mock", + is_public: false, + speak_locale: null, + speak_field: null, + deck_category: null, + category_id: null, + deck_card: input.cards.map((card) => { + assert(input.id); + return { + id: card.id || 9999, + deck_id: input.id, + created_at: new Date().toISOString(), + example: card.example ?? null, + front: card.front, + back: card.back, + }; + }), + } }); const mocks = vi.hoisted(() => { @@ -49,6 +53,7 @@ const mocks = vi.hoisted(() => { vi.mock("./../../../store/screen-store", () => { return { screenStore: { + go: () => {}, screen: { type: "deckForm", deckId: 1, @@ -98,6 +103,8 @@ vi.mock("./../../../store/deck-list-store.ts", () => { return { deckListStore: { replaceDeck: () => {}, + updateFolders: () => {}, + updateCardsToReview: () => {}, searchDeckById: (id: number) => { return myDecks.find((deck) => deck.id === id); }, diff --git a/src/screens/deck-form/store/deck-form-store.ts b/src/screens/deck-form/store/deck-form-store.ts index 41dfe896..afd97181 100644 --- a/src/screens/deck-form/store/deck-form-store.ts +++ b/src/screens/deck-form/store/deck-form-store.ts @@ -34,6 +34,7 @@ type DeckFormType = { cards: CardFormType[]; speakingCardsLocale: TextField; speakingCardsField: TextField; + folderId?: number; }; export const createDeckTitleField = (value: string) => { @@ -120,6 +121,7 @@ export class DeckFormStore { cards: [], speakingCardsLocale: new TextField(null), speakingCardsField: new TextField(null), + folderId: screen.folder?.id ?? undefined, }; } } @@ -330,11 +332,17 @@ export class DeckFormStore { cards: cardsToSend, speakLocale: this.form.speakingCardsLocale.value, speakField: this.form.speakingCardsField.value, + folderId: this.form.folderId, }) - .then((response) => { - this.form = createUpdateForm(response.id, response); - deckListStore.replaceDeck(response); - }) + .then( + action(({ deck, folders, cardsToReview }) => { + this.form = createUpdateForm(deck.id, deck); + deckListStore.replaceDeck(deck, true); + deckListStore.updateFolders(folders); + deckListStore.updateCardsToReview(cardsToReview); + screenStore.go({ type: "deckForm", deckId: deck.id }); + }), + ) .finally( action(() => { this.isSending = false; diff --git a/src/screens/deck-list/main-screen.tsx b/src/screens/deck-list/main-screen.tsx index 5267536d..85ca2d8b 100644 --- a/src/screens/deck-list/main-screen.tsx +++ b/src/screens/deck-list/main-screen.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { Fragment } from "react"; import { observer } from "mobx-react-lite"; import { css, cx } from "@emotion/css"; import { PublicDeck } from "./public-deck.tsx"; @@ -55,7 +55,7 @@ export const MainScreen = observer(() => { {deckListStore.myInfo ? deckListStore.myDeckItemsVisible.map((listItem) => { return ( - <> + { if (listItem.type === "deck") { @@ -71,7 +71,6 @@ export const MainScreen = observer(() => { }); } }} - key={listItem.id} item={listItem} /> {listItem.type === "folder" && @@ -93,7 +92,7 @@ export const MainScreen = observer(() => { ); }) : null} - + ); }) : null} diff --git a/src/screens/deck-review/deck-preview.tsx b/src/screens/deck-review/deck-preview.tsx index c0738038..ad4140e4 100644 --- a/src/screens/deck-review/deck-preview.tsx +++ b/src/screens/deck-review/deck-preview.tsx @@ -19,7 +19,14 @@ export const DeckPreview = observer(() => { const reviewStore = useReviewStore(); useBackButton(() => { - screenStore.back(); + const screen = screenStore.screen; + if ("backScreen" in screen && screen.backScreen) { + // backScreen is RouteType here + // @ts-ignore + screenStore.go({ type: screen.backScreen }); + } else { + screenStore.back(); + } }); useTelegramProgress(() => deckListStore.isDeckCardsLoading); @@ -64,7 +71,18 @@ export const DeckPreview = observer(() => { textAlign: "center", })} > -

{deck.name}

+

+ + {deck.name} +

{deck.description}
diff --git a/src/screens/folder-review/folder-preview.tsx b/src/screens/folder-review/folder-preview.tsx index 5bb5257a..68b1b8a6 100644 --- a/src/screens/folder-review/folder-preview.tsx +++ b/src/screens/folder-review/folder-preview.tsx @@ -30,7 +30,7 @@ export const FolderPreview = observer(() => { t("review_folder"), () => { const folder = deckListStore.selectedFolder; - assert(folder); + assert(folder, "Folder should be selected before review"); reviewStore.startFolderReview( folder.decks, userStore.isSpeakingCardsEnabled, @@ -70,7 +70,18 @@ export const FolderPreview = observer(() => { textAlign: "center", })} > -

{folder.name}

+

+ + {folder.name} +

{folder.description}
@@ -122,6 +133,23 @@ export const FolderPreview = observer(() => { gridTemplateColumns: "repeat(auto-fit, minmax(100px, 1fr))", })} > + {deckListStore.canEditFolder ? ( + { + screenStore.go({ + type: "deckForm", + folder: { + id: folder.id, + name: folder.name, + }, + }); + }} + > + {t("add_deck_short")} + + ) : null} {deckListStore.canEditFolder ? ( { })} > - {folder.decks.map((deck) => { + {folder.decks.map((deck, i) => { return ( { deckListStore.goDeckById(deck.id); }} diff --git a/src/screens/shared/screen.tsx b/src/screens/shared/screen.tsx index a0943e6e..dba1c2d2 100644 --- a/src/screens/shared/screen.tsx +++ b/src/screens/shared/screen.tsx @@ -5,10 +5,11 @@ import { css } from "@emotion/css"; type Props = { children: ReactNode; title: string; + subtitle?: ReactNode; }; export const Screen = observer((props: Props) => { - const { children, title } = props; + const { children, title, subtitle } = props; return (
{ marginBottom: 16, })} > -

{title}

+
+

{title}

+ {subtitle} +
{children}
); diff --git a/src/store/deck-list-store.ts b/src/store/deck-list-store.ts index 98edc1b6..bc3b2792 100644 --- a/src/store/deck-list-store.ts +++ b/src/store/deck-list-store.ts @@ -12,7 +12,7 @@ import { DeckCardDbType, DeckWithCardsDbType, } from "../../functions/db/deck/decks-with-cards-schema.ts"; -import { screenStore } from "./screen-store.ts"; +import { RouteType, screenStore } from "./screen-store.ts"; import { CardToReviewDbType } from "../../functions/db/deck/get-cards-to-review-db.ts"; import { assert } from "../lib/typescript/assert.ts"; import { ReviewStore } from "../screens/deck-review/store/review-store.ts"; @@ -232,18 +232,18 @@ export class DeckListStore { ); } - goDeckById(deckId: number) { + goDeckById(deckId: number, backScreen?: RouteType) { if (!this.myInfo) { return null; } const myDeck = this.myInfo.myDecks.find((deck) => deck.id === deckId); if (myDeck) { - screenStore.go({ type: "deckMine", deckId }); + screenStore.go({ type: "deckMine", deckId, backScreen }); return; } const publicDeck = this.publicDecks.find((deck) => deck.id === deckId); if (publicDeck) { - screenStore.go({ type: "deckPublic", deckId }); + screenStore.go({ type: "deckPublic", deckId, backScreen }); return; } } @@ -258,7 +258,7 @@ export class DeckListStore { get selectedFolder() { const screen = screenStore.screen; - assert(screen.type === "folderPreview"); + assert(screen.type === "folderPreview", "screen is not folder preview"); if (!this.myInfo) { return null; } @@ -269,7 +269,7 @@ export class DeckListStore { if (!folder) { return null; } - assert(folder.type === "folder"); + assert(folder.type === "folder", "folder is not folder type"); return folder; } @@ -311,7 +311,7 @@ export class DeckListStore { }; } - replaceDeck(deck: DeckWithCardsDbType) { + replaceDeck(deck: DeckWithCardsDbType, addToMine = false) { if (!this.myInfo) { return; } @@ -328,6 +328,11 @@ export class DeckListStore { ); if (deckPublicIndex !== -1) { this.myInfo.publicDecks[deckPublicIndex] = deck; + return; + } + + if (addToMine) { + this.myInfo.myDecks.push(deck); } } @@ -470,14 +475,11 @@ export class DeckListStore { this.isFullScreenLoaderVisible = true; deleteFolderRequest(folder.id) + .then(() => myInfoRequest()) .then( - action(() => { + action((result) => { + this.myInfo = result; screenStore.go({ type: "main" }); - myInfoRequest().then( - action((result) => { - this.myInfo = result; - }), - ); }), ) .catch((e) => { @@ -499,14 +501,11 @@ export class DeckListStore { this.isFullScreenLoaderVisible = true; removeDeckFromMineRequest({ deckId: deck.id }) + .then(() => myInfoRequest()) .then( - action(() => { + action((result) => { + this.myInfo = result; screenStore.go({ type: "main" }); - myInfoRequest().then( - action((result) => { - this.myInfo = result; - }), - ); }), ) .catch((e) => { @@ -520,8 +519,13 @@ export class DeckListStore { } updateFolders(body: UserFoldersDbType[]) { - assert(this.myInfo, "myInfo is not loaded in optimisticUpdateFolders"); - Object.assign(this.myInfo.folders, body); + assert(this.myInfo, "myInfo is not loaded in updateFolders"); + this.myInfo.folders = body; + } + + updateCardsToReview(body: CardToReviewDbType[]) { + assert(this.myInfo, "myInfo is not loaded in updateCardsToReview"); + this.myInfo.cardsToReview = body; } } diff --git a/src/store/screen-store.ts b/src/store/screen-store.ts index 6ff357b6..a08a2f30 100644 --- a/src/store/screen-store.ts +++ b/src/store/screen-store.ts @@ -2,9 +2,9 @@ import { makeAutoObservable } from "mobx"; type Route = | { type: "main" } - | { type: "deckMine"; deckId: number } - | { type: "deckPublic"; deckId: number } - | { type: "deckForm"; deckId?: number } + | { type: "deckMine"; deckId: number; backScreen?: RouteType } + | { type: "deckPublic"; deckId: number; backScreen?: RouteType } + | { type: "deckForm"; deckId?: number; folder?: { id: number; name: string } } | { type: "folderForm"; folderId?: number } | { type: "folderPreview"; folderId: number } | { type: "deckOrFolderChoose" } @@ -14,6 +14,8 @@ type Route = | { type: "shareDeck"; deckId: number; shareId: string } | { type: "userSettings" }; +export type RouteType = Route["type"]; + export class ScreenStore { history: Route[] = [{ type: "main" }]; diff --git a/src/translations/t.ts b/src/translations/t.ts index 9c62aa7c..94be4e73 100644 --- a/src/translations/t.ts +++ b/src/translations/t.ts @@ -45,6 +45,7 @@ const en = { edit_card: "Edit card", deck_preview: "Deck preview", add_card_short: "Add card", + add_deck_short: "Deck", card_front_title: "Front side", card_back_title: "Back side", card_front_side_hint: "The prompt or question", @@ -130,6 +131,7 @@ type Translation = typeof en; const ru: Translation = { choose_what_to_create: "Выберите что создать", deck: "Колода", + add_deck_short: "Колода", deck_description: "Коллекция карточек", folder: "Папка", folder_description: "Коллекция колод", @@ -253,6 +255,7 @@ const ru: Translation = { const es: Translation = { review_folder: "Repasar carpeta", folder_description: "Una colección de mazos", + add_deck_short: "Mazo", folder: "Carpeta", deck_description: "Una colección de tarjetas", deck: "Mazo", @@ -379,6 +382,7 @@ const es: Translation = { const ptBr: Translation = { review_folder: "Revisar pasta", + add_deck_short: "Baralho", choose_what_to_create: "Escolha o que criar", deck: "Baralho", deck_description: "Uma coleção de cartões",