Skip to content

Commit

Permalink
Deck categories (#25)
Browse files Browse the repository at this point in the history
* Duplicate deck

* Deck category
  • Loading branch information
kubk authored Dec 14, 2023
1 parent b7b674e commit 408fce2
Show file tree
Hide file tree
Showing 18 changed files with 270 additions and 25 deletions.
1 change: 1 addition & 0 deletions functions/add-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const onRequestPost = handleError(async ({ request, env }) => {
envSafe,
input.data.deckId,
user.id,
user.is_admin,
);
if (!canEdit) {
return createForbiddenRequestResponse();
Expand Down
31 changes: 31 additions & 0 deletions functions/db/databaseTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface Database {
Row: {
author_id: number | null
available_in: string | null
category_id: string | null
created_at: string
description: string | null
id: number
Expand All @@ -65,6 +66,7 @@ export interface Database {
Insert: {
author_id?: number | null
available_in?: string | null
category_id?: string | null
created_at?: string
description?: string | null
id?: number
Expand All @@ -77,6 +79,7 @@ export interface Database {
Update: {
author_id?: number | null
available_in?: string | null
category_id?: string | null
created_at?: string
description?: string | null
id?: number
Expand All @@ -92,6 +95,12 @@ export interface Database {
columns: ["author_id"]
referencedRelation: "user"
referencedColumns: ["id"]
},
{
foreignKeyName: "deck_category_id_fkey"
columns: ["category_id"]
referencedRelation: "deck_category"
referencedColumns: ["id"]
}
]
}
Expand Down Expand Up @@ -132,6 +141,27 @@ export interface Database {
}
]
}
deck_category: {
Row: {
created_at: string
id: string
logo: string | null
name: string
}
Insert: {
created_at?: string
id?: string
logo?: string | null
name: string
}
Update: {
created_at?: string
id?: string
logo?: string | null
name?: string
}
Relationships: []
}
notification: {
Row: {
created_at: string
Expand Down Expand Up @@ -248,6 +278,7 @@ export interface Database {
Returns: {
author_id: number | null
available_in: string | null
category_id: string | null
created_at: string
description: string | null
id: number
Expand Down
7 changes: 7 additions & 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,13 @@ export const deckWithCardsSchema = deckSchema.merge(
z.object({
deck_card: z.array(deckCardSchema),
available_in: z.string().nullable(),
deck_category: z
.object({
name: z.string(),
logo: z.string().nullable(),
})
.nullable()
.optional(),
}),
);

Expand Down
25 changes: 25 additions & 0 deletions functions/db/deck/get-all-categories-db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EnvType } 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({
id: z.string(),
name: z.string(),
logo: z.string().nullable(),
});

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

const { data: categories, error: categoriesError } = await db
.from("deck_category")
.select("*")
.limit(100);

if (categoriesError) {
throw new DatabaseException(categoriesError);
}

return z.array(categorySchema).parse(categories);
};
7 changes: 5 additions & 2 deletions functions/db/deck/get-catalog-decks-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const getCatalogDecksDb = async (

const { data, error } = await db
.from("deck")
.select("*")
.select("*, deck_category:category_id(name, logo)")
.eq("is_public", true)
.order("id", { ascending: false })
.limit(100);
Expand All @@ -23,6 +23,9 @@ export const getCatalogDecksDb = async (
}

return decksWithCardsSchema.parse(
data.map((item) => ({ ...item, deck_card: [] })),
data.map((deck) => {
// @ts-ignore
return { ...deck, deck_card: [] };
}),
);
};
14 changes: 8 additions & 6 deletions functions/db/deck/get-deck-by-id-and-author-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ export const getDeckByIdAndAuthorId = async (
envSafe: EnvType,
deckId: number,
userId: number,
isAdmin: boolean,
) => {
const db = getDatabase(envSafe);

const canEditDeckResult = await db
.from("deck")
.select()
.eq("author_id", userId)
.eq("id", deckId)
.single();
let query = db.from("deck").select().eq("id", deckId);

if (!isAdmin) {
query = query.eq("author_id", userId);
}

const canEditDeckResult = await query.single();

if (canEditDeckResult.error) {
throw new DatabaseException(canEditDeckResult.error);
Expand Down
1 change: 1 addition & 0 deletions functions/db/user/upsert-user-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const userDbSchema = z.object({
is_remind_enabled: z.boolean(),
is_speaking_card_enabled: z.boolean().nullable(),
last_reminded_date: z.string().nullable(),
is_admin: z.boolean(),
});

export type UserDbType = z.infer<typeof userDbSchema>;
Expand Down
71 changes: 71 additions & 0 deletions functions/duplicate-deck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 { getDeckWithCardsById } from "./db/deck/get-deck-with-cards-by-id-db.ts";
import { createBadRequestResponse } from "./lib/json-response/create-bad-request-response.ts";
import { shortUniqueId } from "./lib/short-unique-id/short-unique-id.ts";
import { getDatabase } from "./db/get-database.ts";
import { DatabaseException } from "./db/database-exception.ts";
import { addDeckToMineDb } from "./db/deck/add-deck-to-mine-db.ts";

export type CopyDeckResponse = null;

export const onRequestPost = handleError(async ({ request, env }) => {
const user = await getUser(request, env);
if (!user || !user.is_admin) {
return createAuthFailedResponse();
}
const envSafe = envSchema.parse(env);

const url = new URL(request.url);
const deckId = url.searchParams.get("deck_id");
if (!deckId) {
return createBadRequestResponse();
}

const db = getDatabase(envSafe);
const deck = await getDeckWithCardsById(envSafe, parseInt(deckId));

// prettier-ignore
const insertData = {
author_id: user.id,
name: `${deck.name} (copy)`,
description: deck.description,
share_id: shortUniqueId(),
is_public: false,
speak_field: deck.speak_field,
speak_locale: deck.speak_locale,
};

const insertDeckResult = await db
.from("deck")
.insert(insertData)
.select()
.single();

if (insertDeckResult.error) {
throw new DatabaseException(insertDeckResult.error);
}

const createCardsResult = await db.from("deck_card").insert(
deck.deck_card.map((card) => ({
deck_id: insertDeckResult.data.id,
example: card.example,
front: card.front,
back: card.back,
})),
);

await addDeckToMineDb(envSafe, {
user_id: user.id,
deck_id: insertDeckResult.data.id,
});

if (createCardsResult.error) {
throw new DatabaseException(createCardsResult.error);
}

return createJsonResponse<CopyDeckResponse>(null);
});
1 change: 1 addition & 0 deletions functions/upsert-deck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const onRequestPost = handleError(async ({ request, env }) => {
envSafe,
input.data.id,
user.id,
user.is_admin,
);
if (!databaseDeck) {
return createForbiddenRequestResponse();
Expand Down
5 changes: 5 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from "../../functions/remove-deck-from-mine.ts";
import { DeckCatalogResponse } from "../../functions/catalog-decks.ts";
import { DeckWithCardsResponse } from "../../functions/deck-with-cards.ts";
import { CopyDeckResponse } from "../../functions/duplicate-deck.ts";

export const healthRequest = () => {
return request<HealthResponse>("/health");
Expand All @@ -46,6 +47,10 @@ export const addDeckToMineRequest = (body: AddDeckToMineRequest) => {
);
};

export const apiDuplicateDeckRequest = (deckId: number) => {
return request<CopyDeckResponse>(`/duplicate-deck?deck_id=${deckId}`, "POST");
};

export const userSettingsRequest = (body: UserSettingsRequest) => {
return request<UserSettingsResponse, UserSettingsRequest>(
"/user-settings",
Expand Down
1 change: 1 addition & 0 deletions src/screens/deck-catalog/deck-added-label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const DeckAddedLabel = () => {
fontStyle: "normal",
padding: "0 8px",
borderRadius: theme.borderRadius,
backgroundColor: theme.secondaryBgColor,
border: "1px solid " + theme.linkColor,
color: theme.linkColor,
})}
Expand Down
20 changes: 18 additions & 2 deletions src/screens/deck-review/deck-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useMainButton } from "../../lib/telegram/use-main-button.tsx";
import { showConfirm } from "../../lib/telegram/show-confirm.ts";
import { ButtonSideAligned } from "../../ui/button-side-aligned.tsx";
import { useTelegramProgress } from "../../lib/telegram/use-telegram-progress.tsx";
import { apiDuplicateDeckRequest } from "../../api/api.ts";

export const DeckPreview = observer(() => {
const reviewStore = useReviewStore();
Expand Down Expand Up @@ -110,7 +111,7 @@ export const DeckPreview = observer(() => {
gridTemplateColumns: "repeat(auto-fit, minmax(100px, 1fr))",
})}
>
{deckListStore.myId && deck.author_id === deckListStore.myId ? (
{deckListStore.canEditDeck(deck) ? (
<ButtonSideAligned
icon={"mdi-plus-circle mdi-24px"}
outline
Expand All @@ -124,7 +125,22 @@ export const DeckPreview = observer(() => {
Add card
</ButtonSideAligned>
) : null}
{deckListStore.myId && deck.author_id === deckListStore.myId ? (
{deckListStore.user?.is_admin && (
<ButtonSideAligned
icon={"mdi-content-duplicate mdi-24px"}
outline
onClick={() => {
showConfirm("Are you sure to duplicate this deck?").then(() => {
apiDuplicateDeckRequest(deck.id).then(() => {
screenStore.go({ type: "main" });
});
});
}}
>
Duplicate
</ButtonSideAligned>
)}
{deckListStore.canEditDeck(deck) ? (
<ButtonSideAligned
icon={"mdi-pencil-circle mdi-24px"}
outline
Expand Down
23 changes: 14 additions & 9 deletions src/store/deck-form-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,23 @@ vi.mock("./deck-list-store.ts", () => {
},
];

const myDecks = [
{
id: 1,
cardsToReview: deckCardsMock.slice(0, 2),
share_id: null,
deck_card: deckCardsMock,
name: "Test",
},
] as DeckWithCardsWithReviewType[];

return {
deckListStore: {
replaceDeck: () => {},
myDecks: [
{
id: 1,
cardsToReview: deckCardsMock.slice(0, 2),
share_id: null,
deck_card: deckCardsMock,
name: "Test",
},
] as DeckWithCardsWithReviewType[],
searchDeckById: (id: number) => {
return myDecks.find((deck) => deck.id === id);
},
myDecks: myDecks
},
};
});
Expand Down
4 changes: 1 addition & 3 deletions src/store/deck-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,7 @@ export class DeckFormStore {
assert(screen.type === "deckForm");

if (screen.deckId) {
const deck = deckListStore.myDecks.find(
(myDeck) => myDeck.id === screen.deckId,
);
const deck = deckListStore.searchDeckById(screen.deckId);
assert(deck, "Deck not found in deckListStore");
this.form = createUpdateForm(screen.deckId, deck);
} else {
Expand Down
1 change: 1 addition & 0 deletions src/store/deck-list-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ vi.mock("../api/api.ts", () => {
last_name: "Testov",
last_reminded_date: null,
is_speaking_card_enabled: false,
is_admin: false,
username: "test",
},
cardsToReview: [
Expand Down
Loading

0 comments on commit 408fce2

Please sign in to comment.