Skip to content

Commit

Permalink
Deck catalog: category filter (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubk authored Dec 14, 2023
1 parent 408fce2 commit 3f37cd0
Show file tree
Hide file tree
Showing 28 changed files with 184 additions and 42 deletions.
4 changes: 2 additions & 2 deletions functions/db/deck/add-deck-to-mine-db.ts
Original file line number Diff line number Diff line change
@@ -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<null> => {
const db = getDatabase(env);
Expand Down
1 change: 1 addition & 0 deletions functions/db/deck/decks-with-cards-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
8 changes: 5 additions & 3 deletions functions/db/deck/get-all-categories-db.ts
Original file line number Diff line number Diff line change
@@ -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<typeof categorySchema>;

export const getAllCategoriesDb = async (env: EnvSafe) => {
const db = getDatabase(env);

const { data: categories, error: categoriesError } = await db
Expand Down
4 changes: 2 additions & 2 deletions functions/db/deck/get-cards-to-review-db.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -14,7 +14,7 @@ const schema = z.array(cardToReviewSchema);
export type CardToReviewDbType = z.infer<typeof cardToReviewSchema>;

export const getCardsToReviewDb = async (
env: EnvType,
env: EnvSafe,
userId: number,
): Promise<CardToReviewDbType[]> => {
const db = getDatabase(env);
Expand Down
4 changes: 2 additions & 2 deletions functions/db/deck/get-catalog-decks-db.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -7,7 +7,7 @@ import {
} from "./decks-with-cards-schema.ts";

export const getCatalogDecksDb = async (
env: EnvType,
env: EnvSafe,
): Promise<DeckWithCardsDbType[]> => {
const db = getDatabase(env);

Expand Down
4 changes: 2 additions & 2 deletions functions/db/deck/get-deck-by-id-and-author-id.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
4 changes: 2 additions & 2 deletions functions/db/deck/get-deck-with-cards-by-id-db.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions functions/db/deck/get-my-decks-db.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -8,7 +8,7 @@ import {
import { z } from "zod";

export const getMyDecksDb = async (
env: EnvType,
env: EnvSafe,
userId: number,
): Promise<DeckWithCardsDbType[]> => {
const db = getDatabase(env);
Expand Down
4 changes: 2 additions & 2 deletions functions/db/deck/get-un-added-public-decks-db.ts
Original file line number Diff line number Diff line change
@@ -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", {
Expand Down
4 changes: 2 additions & 2 deletions functions/db/deck/is-user-deck-exists.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
4 changes: 2 additions & 2 deletions functions/db/deck/remove-deck-from-mine-db.ts
Original file line number Diff line number Diff line change
@@ -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<null> => {
const db = getDatabase(env);
Expand Down
4 changes: 2 additions & 2 deletions functions/db/get-database.ts
Original file line number Diff line number Diff line change
@@ -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<Database>(env.SUPABASE_URL, env.SUPABASE_KEY);
};
4 changes: 2 additions & 2 deletions functions/db/user/upsert-user-db.ts
Original file line number Diff line number Diff line change
@@ -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";

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

export const upsertUserDb = async (
env: EnvType,
env: EnvSafe,
user: UserTelegramType,
): Promise<UserDbType> => {
const db = getDatabase(env);
Expand Down
25 changes: 25 additions & 0 deletions functions/deck-categories.ts
Original file line number Diff line number Diff line change
@@ -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<DeckCategoryResponse>({
categories: categories,
});
});
2 changes: 1 addition & 1 deletion functions/env/env-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export const envSchema = z.object({
BOT_ERROR_REPORTING_USER_ID: z.string().optional(),
});

export type EnvType = z.infer<typeof envSchema>;
export type EnvSafe = z.infer<typeof envSchema>;
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HealthResponse>("/health");
Expand Down Expand Up @@ -94,3 +95,7 @@ export const apiDeckCatalog = () => {
export const apiDeckWithCards = (deckId: number) => {
return request<DeckWithCardsResponse>(`/deck-with-cards?deck_id=${deckId}`);
};

export const apiDeckCategories = () => {
return request<DeckCategoryResponse>("/deck-categories");
};
24 changes: 24 additions & 0 deletions src/lib/cache/cache-promise.test.ts
Original file line number Diff line number Diff line change
@@ -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<string>((resolve) => {
resolve(mockFunction());
});

const cached = cachePromise<string>();

// 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);
});
14 changes: 14 additions & 0 deletions src/lib/cache/cache-promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const cachePromise = <T>() => {
let cache: T | null = null;
let isCacheSet = false;

return async function (promise: Promise<T>): Promise<T> {
if (isCacheSet) {
return cache as T;
}

cache = await promise;
isCacheSet = true;
return cache;
};
};
16 changes: 16 additions & 0 deletions src/lib/mobx-form/persistable-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TextField } from "./mobx-form.ts";
import { makePersistable } from "mobx-persist-store";

export const persistableField = <T>(
field: TextField<T>,
storageKey: string,
): TextField<T> => {
makePersistable(field, {
name: storageKey,
properties: ["value"],
storage: window.localStorage,
expireIn: 86400000, // One day in milliseconds
});

return field;
};
16 changes: 9 additions & 7 deletions src/screens/deck-catalog/deck-added-label.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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
<i
className={cx(
"mdi mdi-check-circle",
css({
color: theme.linkColor,
}),
)}
/>
</div>
);
};
21 changes: 20 additions & 1 deletion src/screens/deck-catalog/deck-catalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const DeckCatalog = observer(() => {
>
<h3 className={css({ textAlign: "center" })}>Deck Catalog</h3>
<div className={css({ display: "flex", gap: 4 })}>
<div className={css({ color: theme.hintColor })}>Available in:</div>
<div className={css({ color: theme.hintColor })}>Available in</div>
<Select<LanguageFilter>
value={store.filters.language.value}
onChange={store.filters.language.onChange}
Expand All @@ -50,6 +50,25 @@ export const DeckCatalog = observer(() => {
/>
</div>

<div className={css({ display: "flex", gap: 4 })}>
<div className={css({ color: theme.hintColor })}>Category</div>
<Select
value={store.filters.categoryId.value}
onChange={store.filters.categoryId.onChange}
isLoading={store.categories?.state === "pending"}
options={
store.categories?.state === "fulfilled"
? [{ value: "", label: "Any" }].concat(
store.categories.value.categories.map((category) => ({
value: category.id,
label: category.name,
})),
)
: []
}
/>
</div>

{(() => {
if (store.decks?.state === "pending") {
return range(5).map((i) => <DeckLoading key={i} />);
Expand Down
Loading

0 comments on commit 3f37cd0

Please sign in to comment.