Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deck catalog: category filter #26

Merged
merged 1 commit into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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