Skip to content

Commit

Permalink
Smart unadded decks (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubk authored Dec 15, 2023
1 parent f0a2709 commit 735b81b
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 22 deletions.
28 changes: 20 additions & 8 deletions functions/db/databaseTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -306,14 +326,6 @@ export interface Database {
is_admin: boolean
}[]
}
get_users_with_review_to_notify_backup: {
Args: Record<PropertyKey, never>
Returns: {
user_id: number
review_count: number
last_reminded_date: string
}[]
}
}
Enums: {
[_ in never]: never
Expand Down
15 changes: 12 additions & 3 deletions functions/db/deck/get-un-added-public-decks-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ 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) {
throw new DatabaseException(error);
}

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,
};
}),
);
};
2 changes: 1 addition & 1 deletion src/screens/deck-catalog/deck-added-label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from "react";
export const DeckAddedLabel = () => {
return (
<div
title={'This deck is on your list'}
title={"This deck is on your list"}
className={css({
position: "absolute",
right: 0,
Expand Down
14 changes: 11 additions & 3 deletions src/screens/deck-list/main-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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";
import { ViewMoreDecksToggle } from "./view-more-decks-toggle.tsx";

export const MainScreen = observer(() => {
useMount(() => {
Expand All @@ -31,7 +32,14 @@ export const MainScreen = observer(() => {
})}
>
<div>
<ListHeader text={"My decks"} />
<ListHeader
text={"My decks"}
rightSlot={
deckListStore.shouldShowMyDecksToggle ? (
<ViewMoreDecksToggle />
) : undefined
}
/>
<div
className={css({
display: "flex",
Expand All @@ -44,7 +52,7 @@ export const MainScreen = observer(() => {
<DeckLoading key={i} />
))}
{deckListStore.myInfo
? deckListStore.myDecks.map((deck) => {
? deckListStore.myDecksVisible.map((deck) => {
return <MyDeck key={deck.id} deck={deck} />;
})
: null}
Expand Down Expand Up @@ -97,7 +105,7 @@ export const MainScreen = observer(() => {
>
{deckListStore.myInfo ? (
<>
{deckListStore.publicDecksToDisplay.map((deck) => (
{deckListStore.publicDecks.map((deck) => (
<PublicDeck key={deck.id} deck={deck} />
))}
<button
Expand Down
36 changes: 36 additions & 0 deletions src/screens/deck-list/view-more-decks-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { observer } from "mobx-react-lite";
import { css, cx } from "@emotion/css";
import { reset } from "../../ui/reset.ts";
import { theme } from "../../ui/theme.tsx";
import { deckListStore } from "../../store/deck-list-store.ts";
import { ChevronIcon } from "../../ui/chevron-icon.tsx";
import React from "react";

export const ViewMoreDecksToggle = observer(() => {
return (
<button
className={cx(
reset.button,
css({
position: "absolute",
right: 12,
top: 2,
color: theme.linkColor,
fontSize: 14,
textTransform: "uppercase",
display: "flex",
alignItems: "center",
gap: 4,
}),
)}
onClick={deckListStore.isMyDecksExpanded.toggle}
>
<span className={css({ transform: "translateY(2px)" })}>
<ChevronIcon
direction={deckListStore.isMyDecksExpanded.value ? "top" : "bottom"}
/>
</span>
{deckListStore.isMyDecksExpanded.value ? "Hide" : "Show all"}
</button>
);
});
44 changes: 40 additions & 4 deletions src/store/deck-list-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -30,6 +31,8 @@ export type DeckWithCardsWithReviewType = DeckWithCardsDbType & {
cardsToReview: DeckCardDbTypeWithType[];
};

const collapsedDecksLimit = 3;

export class DeckListStore {
myInfo?: MyInfoResponse;
isMyInfoLoading = false;
Expand All @@ -46,6 +49,8 @@ export class DeckListStore {

isDeckCardsLoading = false;

isMyDecksExpanded = new BooleanToggle(false);

constructor() {
makeAutoObservable(
this,
Expand Down Expand Up @@ -288,10 +293,6 @@ export class DeckListStore {
);
}

get publicDecksToDisplay() {
return this.publicDecks.slice(0, 3);
}

get myDecks(): DeckWithCardsWithReviewType[] {
if (!this.myInfo) {
return [];
Expand All @@ -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 &&
Expand Down
43 changes: 43 additions & 0 deletions src/ui/chevron-icon.tsx
Original file line number Diff line number Diff line change
@@ -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<SVGProps<SVGSVGElement>, "onClick" | "className"> & {
direction: Direction;
};

export const ChevronIcon = (props: Props) => {
const { direction, ...restProps } = props;
return (
<motion.svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
whileTap={{ scale: 0.9 }}
animate={{ rotate: getRotation(direction) }}
initial={false}
{...restProps}
>
<path
d="M4 10.5L8.5 6L13 10.5"
stroke="currentColor"
strokeWidth="2"
strokeMiterlimit="10"
strokeLinecap="round"
strokeLinejoin="round"
/>
</motion.svg>
);
};
13 changes: 10 additions & 3 deletions src/ui/list-header.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<h5
className={css({
Expand All @@ -15,11 +20,13 @@ export const ListHeader = (props: Props) => {
paddingTop: 4,
paddingBottom: 0,
marginBottom: 4,
position: "relative",
color: theme.hintColor,
textTransform: "uppercase",
})}
>
{props.text}
{text}
{rightSlot}
</h5>
);
};

0 comments on commit 735b81b

Please sign in to comment.