Skip to content

Commit

Permalink
Formatting field switcher + CloudStorage adapter (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubk authored Jan 29, 2024
1 parent 4623513 commit f297a43
Show file tree
Hide file tree
Showing 24 changed files with 244 additions and 55 deletions.
2 changes: 1 addition & 1 deletion src/lib/cache/cache-promise.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, vi, expect } from "vitest";
import { expect, test, vi } from "vitest";
import { cachePromise } from "./cache-promise.ts";

test("should cache the resolved value of a promise", async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/mobx-form/persistable-field.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { makePersistable } from "mobx-persist-store";
import { FieldWithValue } from "./field-with-value.ts";
import { storageAdapter } from "../telegram/storage-adapter.ts";

export const persistableField = <T extends FieldWithValue<any>>(
field: T,
Expand All @@ -9,7 +10,7 @@ export const persistableField = <T extends FieldWithValue<any>>(
makePersistable(field, {
name: storageKey,
properties: ["value"],
storage: window.localStorage,
storage: storageAdapter,
expireIn: expireIn,
});

Expand Down
5 changes: 4 additions & 1 deletion src/lib/sanitize-html/remove-all-tags.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import sanitizeHtml from "sanitize-html";

export const removeAllTags = (text: string) => {
return sanitizeHtml(text);
return sanitizeHtml(text, {
allowedTags: [],
allowedAttributes: {},
});
};
42 changes: 42 additions & 0 deletions src/lib/telegram/cloud-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import WebApp from "@twa-dev/sdk";
import { type StorageController } from "mobx-persist-store";

// An adapter of the Telegram cloud storage to the mobx-persist-store interface
export const cloudStorageAdapter: StorageController = {
getItem(key: string) {
return new Promise((resolve, reject) => {
WebApp.CloudStorage.getItem(key, (err, value) => {
if (err != null) {
return reject(err);
} else {
// @ts-ignore
return resolve(value);
}
});
});
},
removeItem(key: string) {
return new Promise((resolve, reject) => {
WebApp.CloudStorage.removeItem(key, (err, result) => {
if (err != null) {
return reject(err);
} else {
// @ts-ignore
return resolve(result);
}
});
});
},
setItem(key: string, value: any) {
return new Promise((resolve, reject) => {
WebApp.CloudStorage.setItem(key, value, (err, result) => {
if (err != null) {
return reject(err);
} else {
// @ts-ignore
return resolve(result);
}
});
});
},
};
File renamed without changes.
6 changes: 6 additions & 0 deletions src/lib/telegram/storage-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import WebApp from "@twa-dev/sdk";
import { cloudStorageAdapter } from "./cloud-storage.ts";

export const storageAdapter = WebApp.isVersionAtLeast("6.9")
? cloudStorageAdapter
: window.localStorage;
63 changes: 51 additions & 12 deletions src/screens/deck-form/card-form-view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { observer, useLocalObservable } from "mobx-react-lite";
import { CardFormStore } from "./store/card-form-store.ts";
import { CardFormStoreInterface } from "./store/card-form-store-interface.ts";
import { assert } from "../../lib/typescript/assert.ts";
import { useMainButton } from "../../lib/telegram/use-main-button.tsx";
import { t } from "../../translations/t.ts";
Expand All @@ -24,9 +24,12 @@ import React from "react";
import { ValidationError } from "../../ui/validation-error.tsx";
import { showAlert } from "../../lib/telegram/show-alert.ts";
import { WysiwygField } from "../../ui/wysiwyg-field/wysiwig-field.tsx";
import { userStore } from "../../store/user-store.ts";
import { Input } from "../../ui/input.tsx";
import { FormattingSwitcher } from "./formatting-switcher.tsx";

type Props = {
cardFormStore: CardFormStore;
cardFormStore: CardFormStoreInterface;
};

export const CardFormView = observer((props: Props) => {
Expand Down Expand Up @@ -54,17 +57,45 @@ export const CardFormView = observer((props: Props) => {
),
}));

const isCardFormattingOn = userStore.isCardFormattingOn.value;

return (
<Screen title={cardForm.id ? t("edit_card") : t("add_card")}>
<Label text={t("card_front_title")} isRequired>
<WysiwygField field={cardForm.front} />
<HintTransparent>{t("card_front_side_hint")}</HintTransparent>
</Label>
<div
className={css({
display: "flex",
flexDirection: "column",
gap: 16,
})}
>
<Label
text={t("card_front_title")}
isPlain
isRequired
slotRight={<FormattingSwitcher />}
>
{isCardFormattingOn ? (
<WysiwygField field={cardForm.front} />
) : (
<Input field={cardForm.front} type={"textarea"} rows={2} />
)}
<HintTransparent>{t("card_front_side_hint")}</HintTransparent>
</Label>

<Label text={t("card_back_title")} isRequired>
<WysiwygField field={cardForm.back} />
<HintTransparent>{t("card_back_side_hint")}</HintTransparent>
</Label>
<Label
text={t("card_back_title")}
isPlain
isRequired
slotRight={<FormattingSwitcher />}
>
{isCardFormattingOn ? (
<WysiwygField field={cardForm.back} />
) : (
<Input field={cardForm.back} type={"textarea"} rows={2} />
)}
<HintTransparent>{t("card_back_side_hint")}</HintTransparent>
</Label>
</div>

<CardRow>
<span>{t("card_advanced")}</span>
Expand Down Expand Up @@ -160,8 +191,16 @@ export const CardFormView = observer((props: Props) => {
</>
)}

<Label text={t("card_field_example_title")}>
<WysiwygField field={cardForm.example} />
<Label
isPlain
text={t("card_field_example_title")}
slotRight={<FormattingSwitcher />}
>
{isCardFormattingOn ? (
<WysiwygField field={cardForm.example} />
) : (
<Input field={cardForm.example} type={"textarea"} rows={2} />
)}
<HintTransparent>{t("card_field_example_hint")}</HintTransparent>
</Label>
</>
Expand Down
11 changes: 4 additions & 7 deletions src/screens/deck-form/card-form-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ import { observer } from "mobx-react-lite";
import React from "react";
import { AnswerFormView } from "./answer-form-view.tsx";
import { assert } from "../../lib/typescript/assert.ts";
import { CardFormStore } from "./store/card-form-store.ts";
import { CardFormStoreInterface } from "./store/card-form-store-interface.ts";
import { CardPreview } from "./card-preview.tsx";
import { CardFormView } from "./card-form-view.tsx";
import { DeckFormStore } from "./store/deck-form-store.ts";

type Props = {
cardFormStore: CardFormStore;
deckFormStore?: DeckFormStore;
cardFormStore: CardFormStoreInterface;
};

export const CardFormWrapper = observer((props: Props) => {
const { cardFormStore, deckFormStore } = props;
const { cardFormStore } = props;
const { cardForm } = cardFormStore;
assert(cardForm, "Card should not be empty before editing");

Expand All @@ -24,8 +22,7 @@ export const CardFormWrapper = observer((props: Props) => {
if (cardFormStore.isCardPreviewSelected.value) {
return (
<CardPreview
form={cardForm}
deckFormStore={deckFormStore}
form={cardFormStore}
onBack={cardFormStore.isCardPreviewSelected.setFalse}
/>
);
Expand Down
11 changes: 4 additions & 7 deletions src/screens/deck-form/card-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { observer } from "mobx-react-lite";
import { useBackButton } from "../../lib/telegram/use-back-button.tsx";
import { css } from "@emotion/css";
import { CardFormType, DeckFormStore } from "./store/deck-form-store.ts";
import { CardReviewWithControls } from "../deck-review/card-review-with-controls.tsx";
import { useState } from "react";
import { CardPreviewStore } from "../deck-review/store/card-preview-store.ts";
import { CardFormStoreInterface } from "./store/card-form-store-interface.ts";

type Props = {
form: CardFormType;
form: CardFormStoreInterface;
onBack: () => void;
deckFormStore?: DeckFormStore;
};

export const CardPreview = observer((props: Props) => {
const { form, onBack, deckFormStore } = props;
const [cardPreviewStore] = useState(
() => new CardPreviewStore(form, deckFormStore),
);
const { form, onBack } = props;
const [cardPreviewStore] = useState(() => new CardPreviewStore(form));

useBackButton(() => {
onBack();
Expand Down
7 changes: 1 addition & 6 deletions src/screens/deck-form/deck-form-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ export const DeckFormScreen = observer(() => {
}

if (deckFormStore.deckFormScreen === "cardForm") {
return (
<CardFormWrapper
cardFormStore={deckFormStore}
deckFormStore={deckFormStore}
/>
);
return <CardFormWrapper cardFormStore={deckFormStore} />;
}

return <DeckForm />;
Expand Down
39 changes: 39 additions & 0 deletions src/screens/deck-form/formatting-switcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { observer } from "mobx-react-lite";
import { userStore } from "../../store/user-store.ts";
import { css, cx } from "@emotion/css";
import { reset } from "../../ui/reset.ts";
import { theme } from "../../ui/theme.tsx";
import { ChevronIcon } from "../../ui/chevron-icon.tsx";
import { t } from "../../translations/t.ts";
import React from "react";

export const FormattingSwitcher = observer(() => {
return (
<button
onClick={() => {
userStore.isCardFormattingOn.toggle();
}}
className={cx(
reset.button,
css({
fontSize: 16,
color: theme.linkColor,
cursor: "pointer",
}),
)}
>
<span
className={css({
transform: "translateY(2px)",
display: "inline-block",
})}
>
{" "}
<ChevronIcon
direction={userStore.isCardFormattingOn.value ? "top" : "bottom"}
/>
</span>{" "}
{t("formatting")}
</button>
);
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { BooleanToggle } from "../../../lib/mobx-form/boolean-toggle.ts";
import { CardFormType } from "./deck-form-store.ts";
import { TextField } from "../../../lib/mobx-form/text-field.ts";
import { DeckSpeakFieldEnum } from "../../../../functions/db/deck/decks-with-cards-schema.ts";

export type CardFormStore = {
export type CardFormStoreInterface = {
cardForm?: CardFormType | null;
onSaveCard: () => void;
onBackCard: () => void;
isSaveCardButtonActive: boolean;
isCardPreviewSelected: BooleanToggle;
isSending: boolean;
markCardAsRemoved?: () => void;

deckForm?: {
speakingCardsLocale: TextField<string | null>;
speakingCardsField: TextField<DeckSpeakFieldEnum | null>;
};
};
4 changes: 2 additions & 2 deletions src/screens/deck-form/store/deck-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { CardAnswerType } from "../../../../functions/db/custom-types.ts";
import { ListField } from "../../../lib/mobx-form/list-field.ts";
import { BooleanField } from "../../../lib/mobx-form/boolean-field.ts";
import { v4 } from "uuid";
import { CardFormStore } from "./card-form-store.ts";
import { CardFormStoreInterface } from "./card-form-store-interface.ts";
import { UpsertDeckRequest } from "../../../../functions/upsert-deck.ts";
import { UnwrapArray } from "../../../lib/typescript/unwrap-array.ts";

Expand Down Expand Up @@ -147,7 +147,7 @@ const cardFormToApi = (
export type CardFilterSortBy = "createdAt" | "frontAlpha" | "backAlpha";
export type CardFilterDirection = "desc" | "asc";

export class DeckFormStore implements CardFormStore {
export class DeckFormStore implements CardFormStoreInterface {
cardFormIndex?: number;
cardFormType?: "new" | "edit";
isCardPreviewSelected = new BooleanToggle(false);
Expand Down
4 changes: 2 additions & 2 deletions src/screens/deck-form/store/quick-add-card-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import { AddCardRequest } from "../../../../functions/add-card.ts";
import { deckListStore } from "../../../store/deck-list-store.ts";
import { t } from "../../../translations/t.ts";
import { BooleanToggle } from "../../../lib/mobx-form/boolean-toggle.ts";
import { CardFormStore } from "./card-form-store.ts";
import { CardFormStoreInterface } from "./card-form-store-interface.ts";

export class QuickAddCardFormStore implements CardFormStore {
export class QuickAddCardFormStore implements CardFormStoreInterface {
cardForm: CardFormType = {
back: createCardSideField(""),
front: createCardSideField(""),
Expand Down
20 changes: 10 additions & 10 deletions src/screens/deck-review/store/card-preview-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import {
DeckSpeakFieldEnum,
} from "../../../../functions/db/deck/decks-with-cards-schema.ts";
import { CardAnswerType } from "../../../../functions/db/custom-types.ts";
import {
CardFormType,
DeckFormStore,
} from "../../deck-form/store/deck-form-store.ts";
import { makeAutoObservable } from "mobx";
import { userStore } from "../../../store/user-store.ts";
import { isEnumValid } from "../../../lib/typescript/is-enum-valid.ts";
import { SpeakLanguageEnum, speak } from "../../../lib/voice-playback/speak.ts";
import { speak, SpeakLanguageEnum } from "../../../lib/voice-playback/speak.ts";
import { removeAllTags } from "../../../lib/sanitize-html/remove-all-tags.ts";
import { CardFormStoreInterface } from "../../deck-form/store/card-form-store-interface.ts";
import { assert } from "../../../lib/typescript/assert.ts";

export class CardPreviewStore implements LimitedCardUnderReviewStore {
id: number;
Expand All @@ -28,8 +26,10 @@ export class CardPreviewStore implements LimitedCardUnderReviewStore {

isOpened = false;

constructor(form: CardFormType, deckFormStore?: DeckFormStore) {
constructor(cardFormStore: CardFormStoreInterface) {
makeAutoObservable(this, {}, { autoBind: true });
const form = cardFormStore.cardForm;
assert(form, "form is not defined");
this.id = 9999;
this.front = form.front.value;
this.back = form.back.value;
Expand All @@ -41,13 +41,13 @@ export class CardPreviewStore implements LimitedCardUnderReviewStore {
isCorrect: answer.isCorrect.value,
}));

if (!deckFormStore) {
const deckForm = cardFormStore.deckForm;
if (!deckForm) {
return;
}

this.deckSpeakLocale =
deckFormStore.form?.speakingCardsLocale.value ?? null;
this.deckSpeakField = deckFormStore.form?.speakingCardsField.value ?? null;
this.deckSpeakLocale = deckForm.speakingCardsLocale.value ?? null;
this.deckSpeakField = deckForm.speakingCardsField.value ?? null;
}

speak() {
Expand Down
Loading

0 comments on commit f297a43

Please sign in to comment.