From 735b81b5493105696848fcc8cd173afd9f05e0a0 Mon Sep 17 00:00:00 2001 From: Egor Gorbachev <7gorbachevm@gmail.com> Date: Fri, 15 Dec 2023 14:32:47 +0700 Subject: [PATCH] Smart unadded decks (#27) --- functions/db/databaseTypes.ts | 28 ++++++++---- .../db/deck/get-un-added-public-decks-db.ts | 15 +++++-- src/screens/deck-catalog/deck-added-label.tsx | 2 +- src/screens/deck-list/main-screen.tsx | 14 ++++-- .../deck-list/view-more-decks-toggle.tsx | 36 +++++++++++++++ src/store/deck-list-store.ts | 44 +++++++++++++++++-- src/ui/chevron-icon.tsx | 43 ++++++++++++++++++ src/ui/list-header.tsx | 13 ++++-- 8 files changed, 173 insertions(+), 22 deletions(-) create mode 100644 src/screens/deck-list/view-more-decks-toggle.tsx create mode 100644 src/ui/chevron-icon.tsx diff --git a/functions/db/databaseTypes.ts b/functions/db/databaseTypes.ts index 1a2fd5fa..55179269 100644 --- a/functions/db/databaseTypes.ts +++ b/functions/db/databaseTypes.ts @@ -289,6 +289,26 @@ export interface Database { speak_locale: string | null }[] } + get_unadded_public_decks_smart: { + Args: { + user_id_param: number + } + Returns: { + id: number + created_at: string + name: string + author_id: number + description: string + is_public: boolean + share_id: string + speak_locale: string + speak_field: string + available_in: string + category_id: string + category_name: string + category_logo: string + }[] + } get_user_decks_deck_id: { Args: { usr_id: number @@ -306,14 +326,6 @@ export interface Database { is_admin: boolean }[] } - get_users_with_review_to_notify_backup: { - Args: Record - Returns: { - user_id: number - review_count: number - last_reminded_date: string - }[] - } } Enums: { [_ in never]: never diff --git a/functions/db/deck/get-un-added-public-decks-db.ts b/functions/db/deck/get-un-added-public-decks-db.ts index abbce253..98eddb64 100644 --- a/functions/db/deck/get-un-added-public-decks-db.ts +++ b/functions/db/deck/get-un-added-public-decks-db.ts @@ -6,8 +6,8 @@ import { decksWithCardsSchema } from "./decks-with-cards-schema.ts"; export const getUnAddedPublicDecksDb = async (env: EnvSafe, userId: number) => { const db = getDatabase(env); - const { data, error } = await db.rpc("get_unadded_public_decks", { - user_id: userId, + const { data, error } = await db.rpc("get_unadded_public_decks_smart", { + user_id_param: userId, }); if (error) { @@ -15,6 +15,15 @@ export const getUnAddedPublicDecksDb = async (env: EnvSafe, userId: number) => { } return decksWithCardsSchema.parse( - data.map((item) => ({ ...item, deck_card: [] })), + data.map((item) => { + const { category_name, category_logo, ...rest } = item; + return { + ...rest, + deck_card: [], + deck_category: rest.category_id + ? { name: category_name, logo: category_logo } + : undefined, + }; + }), ); }; diff --git a/src/screens/deck-catalog/deck-added-label.tsx b/src/screens/deck-catalog/deck-added-label.tsx index 2fec4dde..c8a0d227 100644 --- a/src/screens/deck-catalog/deck-added-label.tsx +++ b/src/screens/deck-catalog/deck-added-label.tsx @@ -5,7 +5,7 @@ import React from "react"; export const DeckAddedLabel = () => { return (
{ useMount(() => { @@ -31,7 +32,14 @@ export const MainScreen = observer(() => { })} >
- + + ) : undefined + } + />
{ ))} {deckListStore.myInfo - ? deckListStore.myDecks.map((deck) => { + ? deckListStore.myDecksVisible.map((deck) => { return ; }) : null} @@ -97,7 +105,7 @@ export const MainScreen = observer(() => { > {deckListStore.myInfo ? ( <> - {deckListStore.publicDecksToDisplay.map((deck) => ( + {deckListStore.publicDecks.map((deck) => ( ))} + ); +}); diff --git a/src/store/deck-list-store.ts b/src/store/deck-list-store.ts index db56ce29..a27b4f2b 100644 --- a/src/store/deck-list-store.ts +++ b/src/store/deck-list-store.ts @@ -17,6 +17,7 @@ import { assert } from "../lib/typescript/assert.ts"; import { ReviewStore } from "./review-store.ts"; import { reportHandledError } from "../lib/rollbar/rollbar.tsx"; import { UserDbType } from "../../functions/db/user/upsert-user-db.ts"; +import { BooleanToggle } from "../lib/mobx-form/boolean-toggle.ts"; export enum StartParamType { RepeatAll = "repeat_all", @@ -30,6 +31,8 @@ export type DeckWithCardsWithReviewType = DeckWithCardsDbType & { cardsToReview: DeckCardDbTypeWithType[]; }; +const collapsedDecksLimit = 3; + export class DeckListStore { myInfo?: MyInfoResponse; isMyInfoLoading = false; @@ -46,6 +49,8 @@ export class DeckListStore { isDeckCardsLoading = false; + isMyDecksExpanded = new BooleanToggle(false); + constructor() { makeAutoObservable( this, @@ -288,10 +293,6 @@ export class DeckListStore { ); } - get publicDecksToDisplay() { - return this.publicDecks.slice(0, 3); - } - get myDecks(): DeckWithCardsWithReviewType[] { if (!this.myInfo) { return []; @@ -304,6 +305,41 @@ export class DeckListStore { })); } + get shouldShowMyDecksToggle() { + return deckListStore.myDecks.length > collapsedDecksLimit; + } + + get myDecksVisible(): DeckWithCardsWithReviewType[] { + const myDecks = this.myDecks; + if (this.isMyDecksExpanded.value) { + return myDecks; + } + + return myDecks + .sort((a, b) => { + // sort decks by cardsToReview count with type 'repeat' first, then with type 'new' + const aRepeatCount = a.cardsToReview.filter( + (card) => card.type === "repeat", + ).length; + + const bRepeatCount = b.cardsToReview.filter( + (card) => card.type === "repeat", + ).length; + + if (aRepeatCount !== bRepeatCount) { + return bRepeatCount - aRepeatCount; + } + + const aNewCount = a.cardsToReview.length - aRepeatCount; + const bNewCount = b.cardsToReview.length - bRepeatCount; + if (aNewCount !== bNewCount) { + return bNewCount - aNewCount; + } + return a.name.localeCompare(b.name); + }) + .slice(0, collapsedDecksLimit); + } + get areAllDecksReviewed() { return ( this.myDecks.length > 0 && diff --git a/src/ui/chevron-icon.tsx b/src/ui/chevron-icon.tsx new file mode 100644 index 00000000..6bb16b88 --- /dev/null +++ b/src/ui/chevron-icon.tsx @@ -0,0 +1,43 @@ +import { motion } from "framer-motion"; +import React, { SVGProps } from "react"; + +type Direction = "top" | "bottom"; + +const getRotation = (direction: Direction) => { + switch (direction) { + case "top": + return 0; + case "bottom": + return 180; + } +}; + +type Props = Pick, "onClick" | "className"> & { + direction: Direction; +}; + +export const ChevronIcon = (props: Props) => { + const { direction, ...restProps } = props; + return ( + + + + ); +}; diff --git a/src/ui/list-header.tsx b/src/ui/list-header.tsx index 4bf119f1..fd239cae 100644 --- a/src/ui/list-header.tsx +++ b/src/ui/list-header.tsx @@ -1,10 +1,15 @@ import { css } from "@emotion/css"; import { theme } from "./theme.tsx"; -import React from "react"; +import React, { ReactNode } from "react"; -type Props = { text: string }; +type Props = { + text: string; + rightSlot?: ReactNode; +}; export const ListHeader = (props: Props) => { + const { text, rightSlot } = props; + return (
{ paddingTop: 4, paddingBottom: 0, marginBottom: 4, + position: "relative", color: theme.hintColor, textTransform: "uppercase", })} > - {props.text} + {text} + {rightSlot}
); };