diff --git a/functions/db/deck/add-deck-to-mine-db.ts b/functions/db/deck/add-deck-to-mine-db.ts index 9ff08385..951a6f5a 100644 --- a/functions/db/deck/add-deck-to-mine-db.ts +++ b/functions/db/deck/add-deck-to-mine-db.ts @@ -1,9 +1,9 @@ -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; export const addDeckToMineDb = async ( - env: EnvType, + env: EnvSafe, body: { user_id: number; deck_id: number }, ): Promise => { const db = getDatabase(env); diff --git a/functions/db/deck/decks-with-cards-schema.ts b/functions/db/deck/decks-with-cards-schema.ts index bc06f1c5..d683586e 100644 --- a/functions/db/deck/decks-with-cards-schema.ts +++ b/functions/db/deck/decks-with-cards-schema.ts @@ -27,6 +27,7 @@ export const deckWithCardsSchema = deckSchema.merge( z.object({ deck_card: z.array(deckCardSchema), available_in: z.string().nullable(), + category_id: z.string().nullable(), deck_category: z .object({ name: z.string(), diff --git a/functions/db/deck/get-all-categories-db.ts b/functions/db/deck/get-all-categories-db.ts index 385d1dce..c1aa5d87 100644 --- a/functions/db/deck/get-all-categories-db.ts +++ b/functions/db/deck/get-all-categories-db.ts @@ -1,15 +1,17 @@ -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; import { z } from "zod"; -const categorySchema = z.object({ +export const categorySchema = z.object({ id: z.string(), name: z.string(), logo: z.string().nullable(), }); -export const getAllCategoriesDb = async (env: EnvType) => { +export type DeckCategoryDb = z.infer; + +export const getAllCategoriesDb = async (env: EnvSafe) => { const db = getDatabase(env); const { data: categories, error: categoriesError } = await db diff --git a/functions/db/deck/get-cards-to-review-db.ts b/functions/db/deck/get-cards-to-review-db.ts index cac89c89..86920fb9 100644 --- a/functions/db/deck/get-cards-to-review-db.ts +++ b/functions/db/deck/get-cards-to-review-db.ts @@ -1,4 +1,4 @@ -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; import { z } from "zod"; @@ -14,7 +14,7 @@ const schema = z.array(cardToReviewSchema); export type CardToReviewDbType = z.infer; export const getCardsToReviewDb = async ( - env: EnvType, + env: EnvSafe, userId: number, ): Promise => { const db = getDatabase(env); diff --git a/functions/db/deck/get-catalog-decks-db.ts b/functions/db/deck/get-catalog-decks-db.ts index a9026e92..fd1396a8 100644 --- a/functions/db/deck/get-catalog-decks-db.ts +++ b/functions/db/deck/get-catalog-decks-db.ts @@ -1,4 +1,4 @@ -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; import { @@ -7,7 +7,7 @@ import { } from "./decks-with-cards-schema.ts"; export const getCatalogDecksDb = async ( - env: EnvType, + env: EnvSafe, ): Promise => { const db = getDatabase(env); diff --git a/functions/db/deck/get-deck-by-id-and-author-id.ts b/functions/db/deck/get-deck-by-id-and-author-id.ts index 15697381..1282f32d 100644 --- a/functions/db/deck/get-deck-by-id-and-author-id.ts +++ b/functions/db/deck/get-deck-by-id-and-author-id.ts @@ -1,9 +1,9 @@ -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; export const getDeckByIdAndAuthorId = async ( - envSafe: EnvType, + envSafe: EnvSafe, deckId: number, userId: number, isAdmin: boolean, diff --git a/functions/db/deck/get-deck-with-cards-by-id-db.ts b/functions/db/deck/get-deck-with-cards-by-id-db.ts index a1dc5cb6..41f6fc36 100644 --- a/functions/db/deck/get-deck-with-cards-by-id-db.ts +++ b/functions/db/deck/get-deck-with-cards-by-id-db.ts @@ -1,9 +1,9 @@ import { DatabaseException } from "../database-exception.ts"; import { deckWithCardsSchema } from "./decks-with-cards-schema.ts"; -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; -export const getDeckWithCardsById = async (env: EnvType, deckId: number) => { +export const getDeckWithCardsById = async (env: EnvSafe, deckId: number) => { const db = getDatabase(env); const { data, error } = await db diff --git a/functions/db/deck/get-my-decks-db.ts b/functions/db/deck/get-my-decks-db.ts index 10b561e8..cf6076b4 100644 --- a/functions/db/deck/get-my-decks-db.ts +++ b/functions/db/deck/get-my-decks-db.ts @@ -1,4 +1,4 @@ -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; import { @@ -8,7 +8,7 @@ import { import { z } from "zod"; export const getMyDecksDb = async ( - env: EnvType, + env: EnvSafe, userId: number, ): Promise => { const db = getDatabase(env); diff --git a/functions/db/deck/get-un-added-public-decks-db.ts b/functions/db/deck/get-un-added-public-decks-db.ts index af38f638..abbce253 100644 --- a/functions/db/deck/get-un-added-public-decks-db.ts +++ b/functions/db/deck/get-un-added-public-decks-db.ts @@ -1,9 +1,9 @@ -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; import { decksWithCardsSchema } from "./decks-with-cards-schema.ts"; -export const getUnAddedPublicDecksDb = async (env: EnvType, userId: number) => { +export const getUnAddedPublicDecksDb = async (env: EnvSafe, userId: number) => { const db = getDatabase(env); const { data, error } = await db.rpc("get_unadded_public_decks", { diff --git a/functions/db/deck/is-user-deck-exists.ts b/functions/db/deck/is-user-deck-exists.ts index a0d0c24a..b8693889 100644 --- a/functions/db/deck/is-user-deck-exists.ts +++ b/functions/db/deck/is-user-deck-exists.ts @@ -1,9 +1,9 @@ -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; export const isUserDeckExists = async ( - envSafe: EnvType, + envSafe: EnvSafe, body: { user_id: number; deck_id: number }, ) => { const db = getDatabase(envSafe); diff --git a/functions/db/deck/remove-deck-from-mine-db.ts b/functions/db/deck/remove-deck-from-mine-db.ts index 259a343b..207211ea 100644 --- a/functions/db/deck/remove-deck-from-mine-db.ts +++ b/functions/db/deck/remove-deck-from-mine-db.ts @@ -1,9 +1,9 @@ -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; export const removeDeckFromMineDb = async ( - env: EnvType, + env: EnvSafe, body: { user_id: number; deck_id: number }, ): Promise => { const db = getDatabase(env); diff --git a/functions/db/get-database.ts b/functions/db/get-database.ts index 401de1e7..36afc855 100644 --- a/functions/db/get-database.ts +++ b/functions/db/get-database.ts @@ -1,7 +1,7 @@ -import { EnvType } from "../env/env-schema.ts"; +import { EnvSafe } from "../env/env-schema.ts"; import { createClient } from "@supabase/supabase-js"; import { Database } from "./databaseTypes.ts"; -export const getDatabase = (env: EnvType) => { +export const getDatabase = (env: EnvSafe) => { return createClient(env.SUPABASE_URL, env.SUPABASE_KEY); }; diff --git a/functions/db/user/upsert-user-db.ts b/functions/db/user/upsert-user-db.ts index 8449a4e6..61a23c5c 100644 --- a/functions/db/user/upsert-user-db.ts +++ b/functions/db/user/upsert-user-db.ts @@ -1,6 +1,6 @@ import { getDatabase } from "../get-database.ts"; import { UserTelegramType } from "../../lib/telegram/validate-telegram-request.ts"; -import { EnvType } from "../../env/env-schema.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; import { DatabaseException } from "../database-exception.ts"; import { z } from "zod"; @@ -19,7 +19,7 @@ export const userDbSchema = z.object({ export type UserDbType = z.infer; export const upsertUserDb = async ( - env: EnvType, + env: EnvSafe, user: UserTelegramType, ): Promise => { const db = getDatabase(env); diff --git a/functions/deck-categories.ts b/functions/deck-categories.ts new file mode 100644 index 00000000..cb8a47eb --- /dev/null +++ b/functions/deck-categories.ts @@ -0,0 +1,25 @@ +import { createJsonResponse } from "./lib/json-response/create-json-response.ts"; +import { getUser } from "./services/get-user.ts"; +import { createAuthFailedResponse } from "./lib/json-response/create-auth-failed-response.ts"; +import { handleError } from "./lib/handle-error/handle-error.ts"; +import { envSchema } from "./env/env-schema.ts"; +import { + DeckCategoryDb, + getAllCategoriesDb, +} from "./db/deck/get-all-categories-db.ts"; + +export type DeckCategoryResponse = { + categories: DeckCategoryDb[]; +}; + +export const onRequest = handleError(async ({ request, env }) => { + const user = await getUser(request, env); + if (!user) return createAuthFailedResponse(); + const envSafe = envSchema.parse(env); + + const categories = await getAllCategoriesDb(envSafe); + + return createJsonResponse({ + categories: categories, + }); +}); diff --git a/functions/env/env-schema.ts b/functions/env/env-schema.ts index 3ca65c74..aa1c0080 100644 --- a/functions/env/env-schema.ts +++ b/functions/env/env-schema.ts @@ -8,4 +8,4 @@ export const envSchema = z.object({ BOT_ERROR_REPORTING_USER_ID: z.string().optional(), }); -export type EnvType = z.infer; +export type EnvSafe = z.infer; diff --git a/package-lock.json b/package-lock.json index 94fae2ad..98c0b3a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "luxon": "^3.4.3", "mobx": "^6.10.2", "mobx-log": "^2.2.3", + "mobx-persist-store": "^1.1.3", "mobx-react-lite": "^4.0.5", "mobx-utils": "^6.0.8", "react": "^18.2.0", @@ -3796,6 +3797,14 @@ "react": ">=16" } }, + "node_modules/mobx-persist-store": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/mobx-persist-store/-/mobx-persist-store-1.1.3.tgz", + "integrity": "sha512-P5B4CNCgAlKZ5dLqwcKYx9qmSc2b53iQTBKkH//7bXzgbsg0+bG4LJ0rPeIx/8/J5lduzqC7/oT+kuNaeHMMqQ==", + "peerDependencies": { + "mobx": "*" + } + }, "node_modules/mobx-react-lite": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.0.5.tgz", diff --git a/package.json b/package.json index 9ab148e2..149e7fef 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "luxon": "^3.4.3", "mobx": "^6.10.2", "mobx-log": "^2.2.3", + "mobx-persist-store": "^1.1.3", "mobx-react-lite": "^4.0.5", "mobx-utils": "^6.0.8", "react": "^18.2.0", diff --git a/src/api/api.ts b/src/api/api.ts index 2a0e9983..bb070c51 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -26,6 +26,7 @@ import { import { DeckCatalogResponse } from "../../functions/catalog-decks.ts"; import { DeckWithCardsResponse } from "../../functions/deck-with-cards.ts"; import { CopyDeckResponse } from "../../functions/duplicate-deck.ts"; +import { DeckCategoryResponse } from "../../functions/deck-categories.ts"; export const healthRequest = () => { return request("/health"); @@ -94,3 +95,7 @@ export const apiDeckCatalog = () => { export const apiDeckWithCards = (deckId: number) => { return request(`/deck-with-cards?deck_id=${deckId}`); }; + +export const apiDeckCategories = () => { + return request("/deck-categories"); +}; diff --git a/src/lib/cache/cache-promise.test.ts b/src/lib/cache/cache-promise.test.ts new file mode 100644 index 00000000..8b575339 --- /dev/null +++ b/src/lib/cache/cache-promise.test.ts @@ -0,0 +1,24 @@ +import { test, vi, expect } from "vitest"; +import { cachePromise } from "./cache-promise.ts"; + +test("should cache the resolved value of a promise", async () => { + const mockFunction = vi.fn(); + mockFunction.mockResolvedValueOnce("Cached value"); + + const promise = new Promise((resolve) => { + resolve(mockFunction()); + }); + + const cached = cachePromise(); + + // First call, should invoke the promise + const result1 = await cached(promise); + expect(result1).toBe("Cached value"); + expect(mockFunction).toHaveBeenCalledTimes(1); + + // Second call, should use cached value + const result2 = await cached(promise); + expect(result2).toBe("Cached value"); + // The mock function should not have been called again + expect(mockFunction).toHaveBeenCalledTimes(1); +}); diff --git a/src/lib/cache/cache-promise.ts b/src/lib/cache/cache-promise.ts new file mode 100644 index 00000000..59872c12 --- /dev/null +++ b/src/lib/cache/cache-promise.ts @@ -0,0 +1,14 @@ +export const cachePromise = () => { + let cache: T | null = null; + let isCacheSet = false; + + return async function (promise: Promise): Promise { + if (isCacheSet) { + return cache as T; + } + + cache = await promise; + isCacheSet = true; + return cache; + }; +}; diff --git a/src/lib/mobx-form/persistable-field.ts b/src/lib/mobx-form/persistable-field.ts new file mode 100644 index 00000000..980a816e --- /dev/null +++ b/src/lib/mobx-form/persistable-field.ts @@ -0,0 +1,16 @@ +import { TextField } from "./mobx-form.ts"; +import { makePersistable } from "mobx-persist-store"; + +export const persistableField = ( + field: TextField, + storageKey: string, +): TextField => { + makePersistable(field, { + name: storageKey, + properties: ["value"], + storage: window.localStorage, + expireIn: 86400000, // One day in milliseconds + }); + + return field; +}; diff --git a/src/screens/deck-catalog/deck-added-label.tsx b/src/screens/deck-catalog/deck-added-label.tsx index 812dfa63..30d2e2b1 100644 --- a/src/screens/deck-catalog/deck-added-label.tsx +++ b/src/screens/deck-catalog/deck-added-label.tsx @@ -1,4 +1,4 @@ -import { css } from "@emotion/css"; +import { css, cx } from "@emotion/css"; import { theme } from "../../ui/theme.tsx"; import React from "react"; @@ -9,16 +9,18 @@ export const DeckAddedLabel = () => { position: "absolute", right: 0, top: 0, - fontSize: 14, - fontStyle: "normal", - padding: "0 8px", borderRadius: theme.borderRadius, backgroundColor: theme.secondaryBgColor, - border: "1px solid " + theme.linkColor, - color: theme.linkColor, })} > - ADDED + ); }; diff --git a/src/screens/deck-catalog/deck-catalog.tsx b/src/screens/deck-catalog/deck-catalog.tsx index c6f4257a..4edfc358 100644 --- a/src/screens/deck-catalog/deck-catalog.tsx +++ b/src/screens/deck-catalog/deck-catalog.tsx @@ -39,7 +39,7 @@ export const DeckCatalog = observer(() => { >

Deck Catalog

-
Available in:
+
Available in
value={store.filters.language.value} onChange={store.filters.language.onChange} @@ -50,6 +50,25 @@ export const DeckCatalog = observer(() => { />
+
+
Category
+