Skip to content

Commit 9c159d0

Browse files
committed
Add deck from folder
1 parent d56838f commit 9c159d0

File tree

11 files changed

+193
-67
lines changed

11 files changed

+193
-67
lines changed

functions/upsert-deck.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,30 @@ import { DatabaseException } from "./db/database-exception.ts";
99
import { createJsonResponse } from "./lib/json-response/create-json-response.ts";
1010
import {
1111
deckSchema,
12-
deckWithCardsSchema,
12+
DeckWithCardsDbType,
1313
} from "./db/deck/decks-with-cards-schema.ts";
1414
import { addDeckToMineDb } from "./db/deck/add-deck-to-mine-db.ts";
1515
import { createForbiddenRequestResponse } from "./lib/json-response/create-forbidden-request-response.ts";
1616
import { getDeckByIdAndAuthorId } from "./db/deck/get-deck-by-id-and-author-id.ts";
1717
import { shortUniqueId } from "./lib/short-unique-id/short-unique-id.ts";
1818
import { Database } from "./db/databaseTypes.ts";
1919
import { getDeckWithCardsById } from "./db/deck/get-deck-with-cards-by-id-db.ts";
20+
import {
21+
getFoldersWithDecksDb,
22+
UserFoldersDbType,
23+
} from "./db/folder/get-folders-with-decks-db.tsx";
24+
import {
25+
CardToReviewDbType,
26+
getCardsToReviewDb,
27+
} from "./db/deck/get-cards-to-review-db.ts";
2028

2129
const requestSchema = z.object({
2230
id: z.number().nullable().optional(),
2331
title: z.string(),
2432
description: z.string().nullable().optional(),
2533
speakLocale: z.string().nullable().optional(),
2634
speakField: z.string().nullable().optional(),
35+
folderId: z.number().nullable().optional(),
2736
cards: z.array(
2837
z.object({
2938
front: z.string(),
@@ -35,7 +44,11 @@ const requestSchema = z.object({
3544
});
3645

3746
export type UpsertDeckRequest = z.infer<typeof requestSchema>;
38-
export type UpsertDeckResponse = z.infer<typeof deckWithCardsSchema>;
47+
export type UpsertDeckResponse = {
48+
deck: DeckWithCardsDbType;
49+
folders: UserFoldersDbType[];
50+
cardsToReview: CardToReviewDbType[];
51+
};
3952

4053
type InsertDeckDatabaseType = Database["public"]["Tables"]["deck"]["Insert"];
4154
type DeckRow = Database["public"]["Tables"]["deck"]["Row"];
@@ -116,15 +129,30 @@ export const onRequestPost = handleError(async ({ request, env }) => {
116129
throw new DatabaseException(createCardsResult.error);
117130
}
118131

132+
// If create deck
119133
if (!input.data.id) {
120134
await addDeckToMineDb(envSafe, {
121135
user_id: user.id,
122136
deck_id: upsertedDeck.id,
123137
});
138+
139+
// If folderId passed - add the new deck to folder
140+
if (input.data.folderId) {
141+
await db.from("deck_folder").upsert({
142+
deck_id: upsertedDeck.id,
143+
folder_id: input.data.folderId,
144+
});
145+
}
124146
}
125147

148+
const [deck, folders, cardsToReview] = await Promise.all([
149+
getDeckWithCardsById(envSafe, upsertedDeck.id),
150+
getFoldersWithDecksDb(envSafe, user.id),
151+
getCardsToReviewDb(envSafe, user.id),
152+
]);
153+
126154
return createJsonResponse<UpsertDeckResponse>(
127-
await getDeckWithCardsById(envSafe, upsertedDeck.id),
155+
{ deck, folders, cardsToReview },
128156
200,
129157
);
130158
});

src/screens/deck-form/deck-form.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,31 @@ export const DeckForm = observer(() => {
5252
}
5353

5454
return (
55-
<Screen title={screen.deckId ? t("edit_deck") : t("add_deck")}>
55+
<Screen
56+
title={screen.deckId ? t("edit_deck") : t("add_deck")}
57+
subtitle={
58+
screen.folder ? (
59+
<div className={css({ textAlign: "center", fontSize: 14 })}>
60+
to folder{" "}
61+
<button
62+
onClick={() => {
63+
assert(screen.folder, "Folder should be defined");
64+
screenStore.go({
65+
type: "folderPreview",
66+
folderId: screen.folder.id,
67+
});
68+
}}
69+
className={cx(
70+
reset.button,
71+
css({ fontSize: "inherit", color: theme.linkColor }),
72+
)}
73+
>
74+
{screen.folder.name}
75+
</button>
76+
</div>
77+
) : undefined
78+
}
79+
>
5680
<Label text={t("title")} isRequired>
5781
<Input field={deckFormStore.form.title} />
5882
</Label>
@@ -155,7 +179,7 @@ export const DeckForm = observer(() => {
155179
onClick={() => {
156180
assert(deckFormStore.form);
157181
assert(deckFormStore.form.id);
158-
deckListStore.goDeckById(deckFormStore.form.id);
182+
deckListStore.goDeckById(deckFormStore.form.id, "main");
159183
}}
160184
>
161185
{t("deck_preview")}

src/screens/deck-form/store/deck-form-store.test.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,33 @@ import { isFormValid } from "../../../lib/mobx-form/form-has-error.ts";
1212
const mapUpsertDeckRequestToResponse = (
1313
input: UpsertDeckRequest,
1414
): UpsertDeckResponse => ({
15-
id: input.id || 9999,
16-
available_in: null,
17-
description: input.description ?? null,
18-
created_at: new Date().toISOString(),
19-
name: input.title,
20-
author_id: 9999,
21-
share_id: "share_id_mock",
22-
is_public: false,
23-
speak_locale: null,
24-
speak_field: null,
25-
deck_category: null,
26-
category_id: null,
27-
deck_card: input.cards.map((card) => {
28-
assert(input.id);
29-
return {
30-
id: card.id || 9999,
31-
deck_id: input.id,
32-
created_at: new Date().toISOString(),
33-
example: card.example ?? null,
34-
front: card.front,
35-
back: card.back,
36-
};
37-
}),
15+
folders: [],
16+
cardsToReview: [],
17+
deck: {
18+
id: input.id || 9999,
19+
available_in: null,
20+
description: input.description ?? null,
21+
created_at: new Date().toISOString(),
22+
name: input.title,
23+
author_id: 9999,
24+
share_id: "share_id_mock",
25+
is_public: false,
26+
speak_locale: null,
27+
speak_field: null,
28+
deck_category: null,
29+
category_id: null,
30+
deck_card: input.cards.map((card) => {
31+
assert(input.id);
32+
return {
33+
id: card.id || 9999,
34+
deck_id: input.id,
35+
created_at: new Date().toISOString(),
36+
example: card.example ?? null,
37+
front: card.front,
38+
back: card.back,
39+
};
40+
}),
41+
}
3842
});
3943

4044
const mocks = vi.hoisted(() => {
@@ -98,6 +102,8 @@ vi.mock("./../../../store/deck-list-store.ts", () => {
98102
return {
99103
deckListStore: {
100104
replaceDeck: () => {},
105+
updateFolders: () => {},
106+
updateCardsToReview: () => {},
101107
searchDeckById: (id: number) => {
102108
return myDecks.find((deck) => deck.id === id);
103109
},

src/screens/deck-form/store/deck-form-store.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type DeckFormType = {
3434
cards: CardFormType[];
3535
speakingCardsLocale: TextField<string | null>;
3636
speakingCardsField: TextField<DeckSpeakFieldEnum | null>;
37+
folderId?: number;
3738
};
3839

3940
export const createDeckTitleField = (value: string) => {
@@ -120,6 +121,7 @@ export class DeckFormStore {
120121
cards: [],
121122
speakingCardsLocale: new TextField(null),
122123
speakingCardsField: new TextField(null),
124+
folderId: screen.folder?.id ?? undefined,
123125
};
124126
}
125127
}
@@ -330,11 +332,17 @@ export class DeckFormStore {
330332
cards: cardsToSend,
331333
speakLocale: this.form.speakingCardsLocale.value,
332334
speakField: this.form.speakingCardsField.value,
335+
folderId: this.form.folderId,
333336
})
334-
.then((response) => {
335-
this.form = createUpdateForm(response.id, response);
336-
deckListStore.replaceDeck(response);
337-
})
337+
.then(
338+
action(({ deck, folders, cardsToReview }) => {
339+
this.form = createUpdateForm(deck.id, deck);
340+
deckListStore.replaceDeck(deck, true);
341+
deckListStore.updateFolders(folders);
342+
deckListStore.updateCardsToReview(cardsToReview);
343+
screenStore.go({ type: "deckForm", deckId: deck.id });
344+
}),
345+
)
338346
.finally(
339347
action(() => {
340348
this.isSending = false;

src/screens/deck-list/main-screen.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { Fragment } from "react";
22
import { observer } from "mobx-react-lite";
33
import { css, cx } from "@emotion/css";
44
import { PublicDeck } from "./public-deck.tsx";
@@ -55,7 +55,7 @@ export const MainScreen = observer(() => {
5555
{deckListStore.myInfo
5656
? deckListStore.myDeckItemsVisible.map((listItem) => {
5757
return (
58-
<>
58+
<Fragment key={listItem.id}>
5959
<MyDeckRow
6060
onClick={() => {
6161
if (listItem.type === "deck") {
@@ -71,7 +71,6 @@ export const MainScreen = observer(() => {
7171
});
7272
}
7373
}}
74-
key={listItem.id}
7574
item={listItem}
7675
/>
7776
{listItem.type === "folder" &&
@@ -93,7 +92,7 @@ export const MainScreen = observer(() => {
9392
);
9493
})
9594
: null}
96-
</>
95+
</Fragment>
9796
);
9897
})
9998
: null}

src/screens/deck-review/deck-preview.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ export const DeckPreview = observer(() => {
1919
const reviewStore = useReviewStore();
2020

2121
useBackButton(() => {
22-
screenStore.back();
22+
const screen = screenStore.screen;
23+
if ("backScreen" in screen && screen.backScreen) {
24+
// backScreen is RouteType here
25+
// @ts-ignore
26+
screenStore.go({ type: screen.backScreen });
27+
} else {
28+
screenStore.back();
29+
}
2330
});
2431

2532
useTelegramProgress(() => deckListStore.isDeckCardsLoading);
@@ -64,7 +71,18 @@ export const DeckPreview = observer(() => {
6471
textAlign: "center",
6572
})}
6673
>
67-
<h3 className={css({ paddingTop: 12 })}>{deck.name}</h3>
74+
<h3
75+
className={css({
76+
paddingTop: 12,
77+
display: "flex",
78+
justifyContent: "center",
79+
alignItems: "center",
80+
gap: 6,
81+
})}
82+
>
83+
<i className={"mdi mdi-cards-outline"} title={t("deck")} />
84+
{deck.name}
85+
</h3>
6886
</div>
6987
<div>
7088
<div>{deck.description}</div>

src/screens/folder-review/folder-preview.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const FolderPreview = observer(() => {
3030
t("review_folder"),
3131
() => {
3232
const folder = deckListStore.selectedFolder;
33-
assert(folder);
33+
assert(folder, "Folder should be selected before review");
3434
reviewStore.startFolderReview(
3535
folder.decks,
3636
userStore.isSpeakingCardsEnabled,
@@ -70,7 +70,18 @@ export const FolderPreview = observer(() => {
7070
textAlign: "center",
7171
})}
7272
>
73-
<h3 className={css({ paddingTop: 12 })}>{folder.name}</h3>
73+
<h3
74+
className={css({
75+
paddingTop: 12,
76+
display: "flex",
77+
justifyContent: "center",
78+
alignItems: "center",
79+
gap: 6,
80+
})}
81+
>
82+
<i className={"mdi mdi-folder-open-outline"} title={t("folder")} />
83+
{folder.name}
84+
</h3>
7485
</div>
7586
<div>
7687
<div>{folder.description}</div>
@@ -122,6 +133,23 @@ export const FolderPreview = observer(() => {
122133
gridTemplateColumns: "repeat(auto-fit, minmax(100px, 1fr))",
123134
})}
124135
>
136+
{deckListStore.canEditFolder ? (
137+
<ButtonSideAligned
138+
icon={"mdi-plus-circle mdi-24px"}
139+
outline
140+
onClick={() => {
141+
screenStore.go({
142+
type: "deckForm",
143+
folder: {
144+
id: folder.id,
145+
name: folder.name,
146+
},
147+
});
148+
}}
149+
>
150+
{t("add_deck_short")}
151+
</ButtonSideAligned>
152+
) : null}
125153
{deckListStore.canEditFolder ? (
126154
<ButtonSideAligned
127155
icon={"mdi-pencil-circle mdi-24px"}
@@ -157,9 +185,10 @@ export const FolderPreview = observer(() => {
157185
})}
158186
>
159187
<ListHeader text={t("decks")} />
160-
{folder.decks.map((deck) => {
188+
{folder.decks.map((deck, i) => {
161189
return (
162190
<SettingsRow
191+
key={i}
163192
onClick={() => {
164193
deckListStore.goDeckById(deck.id);
165194
}}

src/screens/shared/screen.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { css } from "@emotion/css";
55
type Props = {
66
children: ReactNode;
77
title: string;
8+
subtitle?: ReactNode;
89
};
910

1011
export const Screen = observer((props: Props) => {
11-
const { children, title } = props;
12+
const { children, title, subtitle } = props;
1213
return (
1314
<div
1415
className={css({
@@ -19,7 +20,10 @@ export const Screen = observer((props: Props) => {
1920
marginBottom: 16,
2021
})}
2122
>
22-
<h3 className={css({ textAlign: "center" })}>{title}</h3>
23+
<div>
24+
<h3 className={css({ textAlign: "center"})}>{title}</h3>
25+
{subtitle}
26+
</div>
2327
{children}
2428
</div>
2529
);

0 commit comments

Comments
 (0)