Skip to content

Commit

Permalink
Deck catalog (#24)
Browse files Browse the repository at this point in the history
Deck catalog
  • Loading branch information
kubk authored Dec 13, 2023
1 parent 572a448 commit 34d8fd3
Show file tree
Hide file tree
Showing 26 changed files with 473 additions and 211 deletions.
10 changes: 10 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
RemoveDeckFromMineRequest,
RemoveDeckFromMineResponse,
} from "../../functions/remove-deck-from-mine.ts";
import { DeckCatalogResponse } from "../../functions/catalog-decks.ts";
import { DeckWithCardsResponse } from "../../functions/deck-with-cards.ts";

export const healthRequest = () => {
return request<HealthResponse>("/health");
Expand Down Expand Up @@ -79,3 +81,11 @@ export const removeDeckFromMine = (body: RemoveDeckFromMineRequest) => {
body,
);
};

export const apiDeckCatalog = () => {
return request<DeckCatalogResponse>("/catalog-decks");
};

export const apiDeckWithCards = (deckId: number) => {
return request<DeckWithCardsResponse>(`/deck-with-cards?deck_id=${deckId}`);
};
6 changes: 6 additions & 0 deletions src/lib/language-code/language-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// ISO 639-1 two-letter language codes
export enum LanguageCode {
Russian = "ru",
English = "en",
Spanish = "es",
}
3 changes: 3 additions & 0 deletions src/lib/string/camel-case-to-human.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const camelCaseToHuman = (str: string) => {
return str.replace(/([A-Z])/g, " $1");
};
25 changes: 0 additions & 25 deletions src/lib/telegram/cloud-storage.ts

This file was deleted.

4 changes: 3 additions & 1 deletion src/lib/voice-playback/speak.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { camelCaseToHuman } from "../string/camel-case-to-human.ts";

export enum SpeakLanguageEnum {
USEnglish = "en-US",
Italian = "it-IT",
Expand Down Expand Up @@ -41,7 +43,7 @@ export const languageKeyToHuman = (str: string): string => {
if (str === "USEnglish") {
return "US English";
}
return str.replace(/([A-Z])/g, " $1").trim();
return camelCaseToHuman(str);
};

export const isSpeechSynthesisSupported =
Expand Down
9 changes: 9 additions & 0 deletions src/screens/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
useRestoreFullScreenExpand,
} from "../lib/telegram/prevent-telegram-swipe-down-closing.tsx";
import { RepeatAllScreen } from "./deck-review/repeat-all-screen.tsx";
import { DeckCatalog } from "./deck-catalog/deck-catalog.tsx";
import { DeckCatalogStoreContextProvider } from "../store/deck-catalog-store-context.tsx";

export const App = observer(() => {
useRestoreFullScreenExpand();
Expand Down Expand Up @@ -70,6 +72,13 @@ export const App = observer(() => {
</UserSettingsStoreProvider>
</PreventTelegramSwipeDownClosingIos>
)}
{screenStore.screen.type === "deckCatalog" && (
<PreventTelegramSwipeDownClosingIos>
<DeckCatalogStoreContextProvider>
<DeckCatalog />
</DeckCatalogStoreContextProvider>
</PreventTelegramSwipeDownClosingIos>
)}
</div>
);
});
23 changes: 23 additions & 0 deletions src/screens/deck-catalog/deck-added-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { css } from "@emotion/css";
import { theme } from "../../ui/theme.tsx";
import React from "react";

export const DeckAddedLabel = () => {
return (
<div
className={css({
position: "absolute",
right: 0,
top: 0,
fontSize: 14,
fontStyle: "normal",
padding: "0 8px",
borderRadius: theme.borderRadius,
border: "1px solid " + theme.linkColor,
color: theme.linkColor,
})}
>
ADDED
</div>
);
};
87 changes: 87 additions & 0 deletions src/screens/deck-catalog/deck-catalog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { observer } from "mobx-react-lite";
import { useBackButton } from "../../lib/telegram/use-back-button.tsx";
import { screenStore } from "../../store/screen-store.ts";
import { css } from "@emotion/css";
import React from "react";
import { useDeckCatalogStore } from "../../store/deck-catalog-store-context.tsx";
import { useMount } from "../../lib/react/use-mount.ts";
import { theme } from "../../ui/theme.tsx";
import { Select } from "../../ui/select.tsx";
import { enumEntries } from "../../lib/typescript/enum-values.ts";
import { LanguageFilter } from "../../store/deck-catalog-store.ts";
import { camelCaseToHuman } from "../../lib/string/camel-case-to-human.ts";
import { DeckListItemWithDescription } from "../../ui/deck-list-item-with-description.tsx";
import { range } from "../../lib/array/range.ts";
import { DeckLoading } from "../deck-list/deck-loading.tsx";
import { NoDecksMatchingFilters } from "./no-decks-matching-filters.tsx";
import { deckListStore } from "../../store/deck-list-store.ts";
import { DeckAddedLabel } from "./deck-added-label.tsx";

export const DeckCatalog = observer(() => {
const store = useDeckCatalogStore();

useMount(() => {
store.load();
});

useBackButton(() => {
screenStore.go({ type: "main" });
});

return (
<div
className={css({
display: "flex",
flexDirection: "column",
gap: 6,
marginBottom: 16,
})}
>
<h3 className={css({ textAlign: "center" })}>Deck Catalog</h3>
<div className={css({ display: "flex", gap: 4 })}>
<div className={css({ color: theme.hintColor })}>Available in:</div>
<Select<LanguageFilter>
value={store.filters.language.value}
onChange={store.filters.language.onChange}
options={enumEntries(LanguageFilter).map(([name, key]) => ({
value: key,
label: name === "Any" ? "Any language" : camelCaseToHuman(name),
}))}
/>
</div>

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

if (store.decks?.state === "fulfilled") {
const filteredDecks = store.filteredDecks;

if (filteredDecks.length === 0) {
return <NoDecksMatchingFilters />;
}

const myDeckIds = deckListStore.myDecks.map((deck) => deck.id);

return filteredDecks.map((deck) => {
const isMine = myDeckIds.includes(deck.id);

return (
<DeckListItemWithDescription
key={deck.id}
titleRightSlot={isMine ? <DeckAddedLabel /> : undefined}
deck={deck}
onClick={() => {
deckListStore.openDeckFromCatalog(deck, isMine);
}}
/>
);
});
}

return null;
})()}
</div>
);
});
20 changes: 20 additions & 0 deletions src/screens/deck-catalog/no-decks-matching-filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { css } from "@emotion/css";
import { theme } from "../../ui/theme.tsx";
import React from "react";

export const NoDecksMatchingFilters = () => {
return (
<div
className={css({
marginTop: 150,
alignSelf: "center",
textAlign: "center",
})}
>
<div className={css({ fontWeight: 500 })}>No decks found</div>
<div className={css({ fontSize: 14, color: theme.hintColor })}>
Try updating filters to see more decks
</div>
</div>
);
};
48 changes: 28 additions & 20 deletions src/screens/deck-list/main-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import { observer } from "mobx-react-lite";
import { css } from "@emotion/css";
import { css, cx } from "@emotion/css";
import { PublicDeck } from "./public-deck.tsx";
import { MyDeck } from "./my-deck.tsx";
import { deckListStore } from "../../store/deck-list-store.ts";
Expand All @@ -14,6 +14,7 @@ import WebApp from "@twa-dev/sdk";
import { assert } from "../../lib/typescript/assert.ts";
import { ListHeader } from "../../ui/list-header.tsx";
import { range } from "../../lib/array/range.ts";
import { reset } from "../../ui/reset.ts";

export const MainScreen = observer(() => {
useMount(() => {
Expand All @@ -38,18 +39,17 @@ export const MainScreen = observer(() => {
gap: 6,
})}
>
{deckListStore.myInfo?.state === "pending" &&
{deckListStore.isMyInfoLoading &&
range(deckListStore.skeletonLoaderData.myDecksCount).map((i) => (
<DeckLoading key={i} />
))}
{deckListStore.myInfo?.state === "fulfilled"
{deckListStore.myInfo
? deckListStore.myDecks.map((deck) => {
return <MyDeck key={deck.id} deck={deck} />;
})
: null}

{deckListStore.myInfo?.state === "fulfilled" &&
!deckListStore.myDecks.length ? (
{deckListStore.myInfo && !deckListStore.myDecks.length ? (
<Hint>
You don't have any personal deck yet. Feel free to{" "}
<span
Expand All @@ -66,8 +66,7 @@ export const MainScreen = observer(() => {
</Hint>
) : null}

{deckListStore.myInfo?.state === "fulfilled" &&
deckListStore.myDecks.length > 0 ? (
{deckListStore.myInfo && deckListStore.myDecks.length > 0 ? (
<Button
icon={"mdi-plus"}
onClick={() => {
Expand Down Expand Up @@ -96,30 +95,40 @@ export const MainScreen = observer(() => {
gap: 6,
})}
>
{deckListStore.myInfo?.state === "fulfilled" &&
!deckListStore.publicDecks.length ? (
<Hint>
Wow! 🌟 You've added them all! There are no more public decks left
to discover.
</Hint>
) : null}

{deckListStore.myInfo?.state === "fulfilled" ? (
{deckListStore.myInfo ? (
<>
{deckListStore.publicDecks.map((deck) => (
{deckListStore.publicDecksToDisplay.map((deck) => (
<PublicDeck key={deck.id} deck={deck} />
))}
<button
className={cx(
reset.button,
css({
marginTop: 6,
color: theme.linkColor,
fontSize: 16,
}),
)}
onClick={() => {
screenStore.go({ type: "deckCatalog" });
}}
>
<i
className={cx(css({ color: "inherit" }), "mdi mdi-magnify")}
/>{" "}
Explore more decks
</button>
</>
) : null}

{deckListStore.myInfo?.state === "pending" &&
{deckListStore.isMyInfoLoading &&
range(deckListStore.skeletonLoaderData.publicCount).map((i) => (
<DeckLoading key={i} />
))}
</div>
</div>

{deckListStore.myInfo?.state === "fulfilled" && (
{deckListStore.myInfo && (
<>
<div>
<ListHeader text={"News and updates"} />
Expand All @@ -138,7 +147,6 @@ export const MainScreen = observer(() => {
<div>
<Button
icon={"mdi-cog"}
disabled={deckListStore.myInfo?.state !== "fulfilled"}
onClick={() => {
screenStore.go({ type: "userSettings" });
}}
Expand Down
Loading

0 comments on commit 34d8fd3

Please sign in to comment.