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

Quick add card #2

Merged
merged 8 commits into from
Oct 31, 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
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ module.exports = {
rules: {
'react-refresh/only-export-components': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off'
},
}
1 change: 1 addition & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ jobs:
cache: 'npm'
- run: npm i
- run: npm run build --if-present
- run: npm run lint
- run: npm run test:api
- run: npm run test:frontend
56 changes: 56 additions & 0 deletions functions/add-card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { handleError } from "./lib/handle-error/handle-error.ts";
import { getUser } from "./services/get-user.ts";
import { createAuthFailedResponse } from "./lib/json-response/create-auth-failed-response.ts";
import { createBadRequestResponse } from "./lib/json-response/create-bad-request-response.ts";
import { z } from "zod";
import { canEditDeck } from "./db/deck/can-edit-deck.ts";
import { envSchema } from "./env/env-schema.ts";
import { getDatabase } from "./db/get-database.ts";
import { tables } from "./db/tables.ts";
import { DatabaseException } from "./db/database-exception.ts";
import { createForbiddenRequestResponse } from "./lib/json-response/create-forbidden-request-response.ts";
import { createJsonResponse } from "./lib/json-response/create-json-response.ts";

const requestSchema = z.object({
deckId: z.number(),
card: z.object({
front: z.string(),
back: z.string(),
id: z.number().nullable().optional(),
}),
});

export type AddCardRequest = z.infer<typeof requestSchema>;
export type AddCardResponse = null;

export const onRequestPost = handleError(async ({ request, env }) => {
const user = await getUser(request, env);
if (!user) return createAuthFailedResponse();

const input = requestSchema.safeParse(await request.json());
if (!input.success) {
return createBadRequestResponse();
}

const envSafe = envSchema.parse(env);

const canEdit = await canEditDeck(envSafe, input.data.deckId, user.id);
if (!canEdit) {
return createForbiddenRequestResponse();
}

const db = getDatabase(envSafe);
const { data } = input;

const createCardsResult = await db.from(tables.deckCard).insert({
deck_id: data.deckId,
front: data.card.front,
back: data.card.back,
});

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

return createJsonResponse<AddCardResponse>(null, 200);
});
24 changes: 24 additions & 0 deletions functions/db/deck/can-edit-deck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { EnvType } from "../../env/env-schema.ts";
import { getDatabase } from "../get-database.ts";
import { tables } from "../tables.ts";
import { DatabaseException } from "../database-exception.ts";

export const canEditDeck = async (
envSafe: EnvType,
deckId: number,
userId: number,
) => {
const db = getDatabase(envSafe);

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

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

return !!canEditDeckResult.data;
};
29 changes: 11 additions & 18 deletions functions/upsert-deck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createJsonResponse } from "./lib/json-response/create-json-response.ts"
import { deckSchema } from "./db/deck/decks-with-cards-schema.ts";
import { addDeckToMineDb } from "./db/deck/add-deck-to-mine-db.ts";
import { createForbiddenRequestResponse } from "./lib/json-response/create-forbidden-request-response.ts";
import { canEditDeck } from "./db/deck/can-edit-deck.ts";

const requestSchema = z.object({
id: z.number().nullable().optional(),
Expand Down Expand Up @@ -42,22 +43,13 @@ export const onRequestPost = handleError(async ({ request, env }) => {

// Check user can edit the deck
if (input.data.id) {
const canEditDeckResult = await db
.from(tables.deck)
.select()
.eq("author_id", user.id)
.eq("id", input.data.id);

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

if (!canEditDeckResult.data) {
const result = await canEditDeck(envSafe, input.data.id, user.id);
if (!result) {
return createForbiddenRequestResponse();
}
}

const createDeckResult = await db
const upsertDeckResult = await db
.from(tables.deck)
.upsert({
id: input.data.id ? input.data.id : undefined,
Expand All @@ -68,18 +60,19 @@ export const onRequestPost = handleError(async ({ request, env }) => {
})
.select();

if (createDeckResult.error) {
throw new DatabaseException(createDeckResult.error);
if (upsertDeckResult.error) {
throw new DatabaseException(upsertDeckResult.error);
}

const newDeckArray = z.array(deckSchema).parse(createDeckResult.data);
// Supabase returns an array as a result of upsert, that's why it gets validated against an array here
const upsertedDecks = z.array(deckSchema).parse(upsertDeckResult.data);

const updateCardsResult = await db.from(tables.deckCard).upsert(
input.data.cards
.filter((card) => card.id)
.map((card) => ({
id: card.id,
deck_id: newDeckArray[0].id,
deck_id: upsertedDecks[0].id,
front: card.front,
back: card.back,
})),
Expand All @@ -93,7 +86,7 @@ export const onRequestPost = handleError(async ({ request, env }) => {
input.data.cards
.filter((card) => !card.id)
.map((card) => ({
deck_id: newDeckArray[0].id,
deck_id: upsertedDecks[0].id,
front: card.front,
back: card.back,
})),
Expand All @@ -106,7 +99,7 @@ export const onRequestPost = handleError(async ({ request, env }) => {
if (!input.data.id) {
await addDeckToMineDb(envSafe, {
user_id: user.id,
deck_id: newDeckArray[0].id,
deck_id: upsertedDecks[0].id,
});
}

Expand Down
7 changes: 4 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<!-- Eruda is console for mobile browsers -->
<!-- <script src="https://cdn.jsdelivr.net/npm/eruda"></script>-->
<!-- <script>eruda.init();</script>-->
<!-- <script src="https://cdn.jsdelivr.net/npm/eruda"></script>-->
<!-- <script>-->
<!-- eruda.init();-->
<!-- </script>-->
</html>
5 changes: 5 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ShareDeckResponse,
} from "../../functions/share-deck.ts";
import { GetSharedDeckResponse } from "../../functions/get-shared-deck.ts";
import { AddCardRequest, AddCardResponse } from "../../functions/add-card.ts";

export const healthRequest = () => {
return request<HealthResponse>("/health");
Expand Down Expand Up @@ -55,6 +56,10 @@ export const upsertDeckRequest = (body: UpsertDeckRequest) => {
);
};

export const addCardRequest = (body: AddCardRequest) => {
return request<AddCardResponse, AddCardRequest>("/add-card", "POST", body);
};

export const shareDeckRequest = (body: ShareDeckRequest) => {
return request<ShareDeckResponse, ShareDeckRequest>(
"/share-deck",
Expand Down
1 change: 0 additions & 1 deletion src/lib/mobx-form/validator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
// https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/field-level-validation?file=/index.js

Expand Down
5 changes: 5 additions & 0 deletions src/lib/telegram/show-alert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import WebApp from "@twa-dev/sdk";

export const showAlert = (text: string) => {
WebApp.showAlert(text);
};
9 changes: 9 additions & 0 deletions src/lib/telegram/show-confirm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import WebApp from "@twa-dev/sdk";

export const showConfirm = (text: string): Promise<boolean> => {
return new Promise((resolve) => {
WebApp.showConfirm(text, (confirmed) => {
resolve(confirmed);
});
});
};
6 changes: 3 additions & 3 deletions src/lib/telegram/use-main-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import WebApp from "@twa-dev/sdk";
export const useMainButton = (
text: string,
onClick: () => void,
skipIf?: () => boolean,
condition?: () => boolean,
) => {
useMount(() => {
if (skipIf !== undefined) {
if (skipIf()) {
if (condition !== undefined) {
if (!condition()) {
return;
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/lib/telegram/use-telegram-progress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useMount } from "../react/use-mount.ts";
import { autorun } from "mobx";
import WebApp from "@twa-dev/sdk";

export const useTelegramProgress = (cb: () => boolean) => {
return useMount(() => {
return autorun(() => {
if (cb()) {
WebApp.MainButton.showProgress();
} else {
WebApp.MainButton.hideProgress();
}
});
});
};
4 changes: 3 additions & 1 deletion src/screens/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { ReviewStoreProvider } from "../store/review-store-context.tsx";
import { Screen, screenStore } from "../store/screen-store.ts";
import { DeckFormScreen } from "./deck-form/deck-form-screen.tsx";
import { DeckFormStoreProvider } from "../store/deck-form-store-context.tsx";
import { QuickAddCardForm } from "./deck-form/quick-add-card-form.tsx";

export const App = observer(() => {
return (
<div>
{screenStore.screen === Screen.Main && <MainScreen />}
{screenStore.isDeckScreen && (
{screenStore.isDeckPreviewScreen && (
<ReviewStoreProvider>
<DeckScreen />
</ReviewStoreProvider>
Expand All @@ -20,6 +21,7 @@ export const App = observer(() => {
<DeckFormScreen />
</DeckFormStoreProvider>
)}
{screenStore.screen === Screen.CardQuickAddForm && <QuickAddCardForm />}
</div>
);
});
34 changes: 34 additions & 0 deletions src/screens/deck-form/card-form-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { observer } from "mobx-react-lite";
import { css } from "@emotion/css";
import { Label } from "../../ui/label.tsx";
import { Input } from "../../ui/input.tsx";
import React from "react";
import { CardFormType } from "../../store/deck-form-store.ts";

type Props = {
cardForm: CardFormType;
};
export const CardFormView = observer((props: Props) => {
const { cardForm } = props;

return (
<div
className={css({
display: "flex",
flexDirection: "column",
gap: 6,
marginBottom: 16,
position: "relative",
})}
>
<h3 className={css({ textAlign: "center" })}>Add card</h3>
<Label text={"Front"}>
<Input {...cardForm.front.props} rows={7} type={"textarea"} />
</Label>

<Label text={"Back"}>
<Input {...cardForm.back.props} rows={7} type={"textarea"} />
</Label>
</div>
);
});
39 changes: 3 additions & 36 deletions src/screens/deck-form/card-form.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { observer } from "mobx-react-lite";
import { assert } from "../../lib/typescript/assert.ts";
import { css } from "@emotion/css";
import { Label } from "../../ui/label.tsx";
import { Input } from "../../ui/input.tsx";
import React from "react";
import { useMainButton } from "../../lib/telegram/use-main-button.tsx";
import { useDeckFormStore } from "../../store/deck-form-store-context.tsx";
import WebApp from "@twa-dev/sdk";
import { useBackButton } from "../../lib/telegram/use-back-button.tsx";
import { isFormEmpty } from "../../lib/mobx-form/form-has-error.ts";
import { CardFormView } from "./card-form-view.tsx";

export const CardForm = observer(() => {
const deckFormStore = useDeckFormStore();
Expand All @@ -18,38 +14,9 @@ export const CardForm = observer(() => {
useMainButton("Save", () => {
deckFormStore.saveCardForm();
});

useBackButton(() => {
if (isFormEmpty(cardForm)) {
deckFormStore.quitCardForm();
return;
}

WebApp.showConfirm("Quit editing card without saving?", (confirmed) => {
if (confirmed) {
deckFormStore.quitCardForm();
}
});
deckFormStore.onCardBack();
});

return (
<div
className={css({
display: "flex",
flexDirection: "column",
gap: 6,
marginBottom: 16,
position: "relative",
})}
>
<h3 className={css({ textAlign: "center" })}>Add card</h3>
<Label text={"Title"}>
<Input {...cardForm.front.props} rows={7} type={"textarea"} />
</Label>

<Label text={"Description"}>
<Input {...cardForm.back.props} rows={7} type={"textarea"} />
</Label>
</div>
);
return <CardFormView cardForm={cardForm} />;
});
Loading