Skip to content

Commit 805b2f6

Browse files
committed
Deck catalog: category filter
1 parent 408fce2 commit 805b2f6

28 files changed

+184
-42
lines changed

functions/db/deck/add-deck-to-mine-db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { EnvType } from "../../env/env-schema.ts";
1+
import { EnvSafe } from "../../env/env-schema.ts";
22
import { getDatabase } from "../get-database.ts";
33
import { DatabaseException } from "../database-exception.ts";
44

55
export const addDeckToMineDb = async (
6-
env: EnvType,
6+
env: EnvSafe,
77
body: { user_id: number; deck_id: number },
88
): Promise<null> => {
99
const db = getDatabase(env);

functions/db/deck/decks-with-cards-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const deckWithCardsSchema = deckSchema.merge(
2727
z.object({
2828
deck_card: z.array(deckCardSchema),
2929
available_in: z.string().nullable(),
30+
category_id: z.string().nullable(),
3031
deck_category: z
3132
.object({
3233
name: z.string(),

functions/db/deck/get-all-categories-db.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
import { EnvType } from "../../env/env-schema.ts";
1+
import { EnvSafe } from "../../env/env-schema.ts";
22
import { getDatabase } from "../get-database.ts";
33
import { DatabaseException } from "../database-exception.ts";
44
import { z } from "zod";
55

6-
const categorySchema = z.object({
6+
export const categorySchema = z.object({
77
id: z.string(),
88
name: z.string(),
99
logo: z.string().nullable(),
1010
});
1111

12-
export const getAllCategoriesDb = async (env: EnvType) => {
12+
export type DeckCategoryDb = z.infer<typeof categorySchema>;
13+
14+
export const getAllCategoriesDb = async (env: EnvSafe) => {
1315
const db = getDatabase(env);
1416

1517
const { data: categories, error: categoriesError } = await db

functions/db/deck/get-cards-to-review-db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EnvType } from "../../env/env-schema.ts";
1+
import { EnvSafe } from "../../env/env-schema.ts";
22
import { getDatabase } from "../get-database.ts";
33
import { DatabaseException } from "../database-exception.ts";
44
import { z } from "zod";
@@ -14,7 +14,7 @@ const schema = z.array(cardToReviewSchema);
1414
export type CardToReviewDbType = z.infer<typeof cardToReviewSchema>;
1515

1616
export const getCardsToReviewDb = async (
17-
env: EnvType,
17+
env: EnvSafe,
1818
userId: number,
1919
): Promise<CardToReviewDbType[]> => {
2020
const db = getDatabase(env);

functions/db/deck/get-catalog-decks-db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EnvType } from "../../env/env-schema.ts";
1+
import { EnvSafe } from "../../env/env-schema.ts";
22
import { getDatabase } from "../get-database.ts";
33
import { DatabaseException } from "../database-exception.ts";
44
import {
@@ -7,7 +7,7 @@ import {
77
} from "./decks-with-cards-schema.ts";
88

99
export const getCatalogDecksDb = async (
10-
env: EnvType,
10+
env: EnvSafe,
1111
): Promise<DeckWithCardsDbType[]> => {
1212
const db = getDatabase(env);
1313

functions/db/deck/get-deck-by-id-and-author-id.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { EnvType } from "../../env/env-schema.ts";
1+
import { EnvSafe } from "../../env/env-schema.ts";
22
import { getDatabase } from "../get-database.ts";
33
import { DatabaseException } from "../database-exception.ts";
44

55
export const getDeckByIdAndAuthorId = async (
6-
envSafe: EnvType,
6+
envSafe: EnvSafe,
77
deckId: number,
88
userId: number,
99
isAdmin: boolean,

functions/db/deck/get-deck-with-cards-by-id-db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { DatabaseException } from "../database-exception.ts";
22
import { deckWithCardsSchema } from "./decks-with-cards-schema.ts";
3-
import { EnvType } from "../../env/env-schema.ts";
3+
import { EnvSafe } from "../../env/env-schema.ts";
44
import { getDatabase } from "../get-database.ts";
55

6-
export const getDeckWithCardsById = async (env: EnvType, deckId: number) => {
6+
export const getDeckWithCardsById = async (env: EnvSafe, deckId: number) => {
77
const db = getDatabase(env);
88

99
const { data, error } = await db

functions/db/deck/get-my-decks-db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EnvType } from "../../env/env-schema.ts";
1+
import { EnvSafe } from "../../env/env-schema.ts";
22
import { getDatabase } from "../get-database.ts";
33
import { DatabaseException } from "../database-exception.ts";
44
import {
@@ -8,7 +8,7 @@ import {
88
import { z } from "zod";
99

1010
export const getMyDecksDb = async (
11-
env: EnvType,
11+
env: EnvSafe,
1212
userId: number,
1313
): Promise<DeckWithCardsDbType[]> => {
1414
const db = getDatabase(env);

functions/db/deck/get-un-added-public-decks-db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { EnvType } from "../../env/env-schema.ts";
1+
import { EnvSafe } from "../../env/env-schema.ts";
22
import { getDatabase } from "../get-database.ts";
33
import { DatabaseException } from "../database-exception.ts";
44
import { decksWithCardsSchema } from "./decks-with-cards-schema.ts";
55

6-
export const getUnAddedPublicDecksDb = async (env: EnvType, userId: number) => {
6+
export const getUnAddedPublicDecksDb = async (env: EnvSafe, userId: number) => {
77
const db = getDatabase(env);
88

99
const { data, error } = await db.rpc("get_unadded_public_decks", {

functions/db/deck/is-user-deck-exists.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { EnvType } from "../../env/env-schema.ts";
1+
import { EnvSafe } from "../../env/env-schema.ts";
22
import { getDatabase } from "../get-database.ts";
33
import { DatabaseException } from "../database-exception.ts";
44

55
export const isUserDeckExists = async (
6-
envSafe: EnvType,
6+
envSafe: EnvSafe,
77
body: { user_id: number; deck_id: number },
88
) => {
99
const db = getDatabase(envSafe);

functions/db/deck/remove-deck-from-mine-db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { EnvType } from "../../env/env-schema.ts";
1+
import { EnvSafe } from "../../env/env-schema.ts";
22
import { getDatabase } from "../get-database.ts";
33
import { DatabaseException } from "../database-exception.ts";
44

55
export const removeDeckFromMineDb = async (
6-
env: EnvType,
6+
env: EnvSafe,
77
body: { user_id: number; deck_id: number },
88
): Promise<null> => {
99
const db = getDatabase(env);

functions/db/get-database.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { EnvType } from "../env/env-schema.ts";
1+
import { EnvSafe } from "../env/env-schema.ts";
22
import { createClient } from "@supabase/supabase-js";
33
import { Database } from "./databaseTypes.ts";
44

5-
export const getDatabase = (env: EnvType) => {
5+
export const getDatabase = (env: EnvSafe) => {
66
return createClient<Database>(env.SUPABASE_URL, env.SUPABASE_KEY);
77
};

functions/db/user/upsert-user-db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getDatabase } from "../get-database.ts";
22
import { UserTelegramType } from "../../lib/telegram/validate-telegram-request.ts";
3-
import { EnvType } from "../../env/env-schema.ts";
3+
import { EnvSafe } from "../../env/env-schema.ts";
44
import { DatabaseException } from "../database-exception.ts";
55
import { z } from "zod";
66

@@ -19,7 +19,7 @@ export const userDbSchema = z.object({
1919
export type UserDbType = z.infer<typeof userDbSchema>;
2020

2121
export const upsertUserDb = async (
22-
env: EnvType,
22+
env: EnvSafe,
2323
user: UserTelegramType,
2424
): Promise<UserDbType> => {
2525
const db = getDatabase(env);

functions/deck-categories.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createJsonResponse } from "./lib/json-response/create-json-response.ts";
2+
import { getUser } from "./services/get-user.ts";
3+
import { createAuthFailedResponse } from "./lib/json-response/create-auth-failed-response.ts";
4+
import { handleError } from "./lib/handle-error/handle-error.ts";
5+
import { envSchema } from "./env/env-schema.ts";
6+
import {
7+
DeckCategoryDb,
8+
getAllCategoriesDb,
9+
} from "./db/deck/get-all-categories-db.ts";
10+
11+
export type DeckCategoryResponse = {
12+
categories: DeckCategoryDb[];
13+
};
14+
15+
export const onRequest = handleError(async ({ request, env }) => {
16+
const user = await getUser(request, env);
17+
if (!user) return createAuthFailedResponse();
18+
const envSafe = envSchema.parse(env);
19+
20+
const categories = await getAllCategoriesDb(envSafe);
21+
22+
return createJsonResponse<DeckCategoryResponse>({
23+
categories: categories,
24+
});
25+
});

functions/env/env-schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ export const envSchema = z.object({
88
BOT_ERROR_REPORTING_USER_ID: z.string().optional(),
99
});
1010

11-
export type EnvType = z.infer<typeof envSchema>;
11+
export type EnvSafe = z.infer<typeof envSchema>;

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"luxon": "^3.4.3",
3737
"mobx": "^6.10.2",
3838
"mobx-log": "^2.2.3",
39+
"mobx-persist-store": "^1.1.3",
3940
"mobx-react-lite": "^4.0.5",
4041
"mobx-utils": "^6.0.8",
4142
"react": "^18.2.0",

src/api/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import { DeckCatalogResponse } from "../../functions/catalog-decks.ts";
2727
import { DeckWithCardsResponse } from "../../functions/deck-with-cards.ts";
2828
import { CopyDeckResponse } from "../../functions/duplicate-deck.ts";
29+
import { DeckCategoryResponse } from "../../functions/deck-categories.ts";
2930

3031
export const healthRequest = () => {
3132
return request<HealthResponse>("/health");
@@ -94,3 +95,7 @@ export const apiDeckCatalog = () => {
9495
export const apiDeckWithCards = (deckId: number) => {
9596
return request<DeckWithCardsResponse>(`/deck-with-cards?deck_id=${deckId}`);
9697
};
98+
99+
export const apiDeckCategories = () => {
100+
return request<DeckCategoryResponse>("/deck-categories");
101+
};

src/lib/cache/cache-promise.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { test, vi, expect } from "vitest";
2+
import { cachePromise } from "./cache-promise.ts";
3+
4+
test("should cache the resolved value of a promise", async () => {
5+
const mockFunction = vi.fn();
6+
mockFunction.mockResolvedValueOnce("Cached value");
7+
8+
const promise = new Promise<string>((resolve) => {
9+
resolve(mockFunction());
10+
});
11+
12+
const cached = cachePromise<string>();
13+
14+
// First call, should invoke the promise
15+
const result1 = await cached(promise);
16+
expect(result1).toBe("Cached value");
17+
expect(mockFunction).toHaveBeenCalledTimes(1);
18+
19+
// Second call, should use cached value
20+
const result2 = await cached(promise);
21+
expect(result2).toBe("Cached value");
22+
// The mock function should not have been called again
23+
expect(mockFunction).toHaveBeenCalledTimes(1);
24+
});

src/lib/cache/cache-promise.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const cachePromise = <T>() => {
2+
let cache: T | null = null;
3+
let isCacheSet = false;
4+
5+
return async function (promise: Promise<T>): Promise<T> {
6+
if (isCacheSet) {
7+
return cache as T;
8+
}
9+
10+
cache = await promise;
11+
isCacheSet = true;
12+
return cache;
13+
};
14+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TextField } from "./mobx-form.ts";
2+
import { makePersistable } from "mobx-persist-store";
3+
4+
export const persistableField = <T>(
5+
field: TextField<T>,
6+
storageKey: string,
7+
): TextField<T> => {
8+
makePersistable(field, {
9+
name: storageKey,
10+
properties: ["value"],
11+
storage: window.localStorage,
12+
expireIn: 86400000, // One day in milliseconds
13+
});
14+
15+
return field;
16+
};
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { css } from "@emotion/css";
1+
import { css, cx } from "@emotion/css";
22
import { theme } from "../../ui/theme.tsx";
33
import React from "react";
44

@@ -9,16 +9,18 @@ export const DeckAddedLabel = () => {
99
position: "absolute",
1010
right: 0,
1111
top: 0,
12-
fontSize: 14,
13-
fontStyle: "normal",
14-
padding: "0 8px",
1512
borderRadius: theme.borderRadius,
1613
backgroundColor: theme.secondaryBgColor,
17-
border: "1px solid " + theme.linkColor,
18-
color: theme.linkColor,
1914
})}
2015
>
21-
ADDED
16+
<i
17+
className={cx(
18+
"mdi mdi-check-circle",
19+
css({
20+
color: theme.linkColor,
21+
}),
22+
)}
23+
/>
2224
</div>
2325
);
2426
};

src/screens/deck-catalog/deck-catalog.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const DeckCatalog = observer(() => {
3939
>
4040
<h3 className={css({ textAlign: "center" })}>Deck Catalog</h3>
4141
<div className={css({ display: "flex", gap: 4 })}>
42-
<div className={css({ color: theme.hintColor })}>Available in:</div>
42+
<div className={css({ color: theme.hintColor })}>Available in</div>
4343
<Select<LanguageFilter>
4444
value={store.filters.language.value}
4545
onChange={store.filters.language.onChange}
@@ -50,6 +50,25 @@ export const DeckCatalog = observer(() => {
5050
/>
5151
</div>
5252

53+
<div className={css({ display: "flex", gap: 4 })}>
54+
<div className={css({ color: theme.hintColor })}>Category</div>
55+
<Select
56+
value={store.filters.categoryId.value}
57+
onChange={store.filters.categoryId.onChange}
58+
isLoading={store.categories?.state === "pending"}
59+
options={
60+
store.categories?.state === "fulfilled"
61+
? [{ value: "", label: "Any" }].concat(
62+
store.categories.value.categories.map((category) => ({
63+
value: category.id,
64+
label: category.name,
65+
})),
66+
)
67+
: []
68+
}
69+
/>
70+
</div>
71+
5372
{(() => {
5473
if (store.decks?.state === "pending") {
5574
return range(5).map((i) => <DeckLoading key={i} />);

0 commit comments

Comments
 (0)