From d4b838106ab51cb05806adb125edcc63f1a5b161 Mon Sep 17 00:00:00 2001 From: Thiago Chaves Date: Sat, 16 Jul 2022 11:53:23 +0300 Subject: [PATCH] Refactor page components, reduce flicker, remove orphaned module Move some business logic out of the page components. They are now responsible for just filtering out the unsuccessful render paths and selecting between an error view or a success view passing the successfully fetched data. --- .../ExpressionSetCard/{index.tsx => index.ts} | 0 src/components/Navigation/NavigationItem.tsx | 1 - .../Navigation/{index.tsx => index.ts} | 0 src/components/Page/Page.stories.tsx | 3 +- src/components/index.ts | 3 + src/hooks/index.ts | 6 + src/hooks/useExpressionCategories.ts | 20 +-- src/hooks/useExpressionSets.ts | 4 +- src/hooks/useExpressionsInSet.ts | 25 +-- src/mock/index.ts | 1 - src/mock/mock-data.ts | 2 +- src/model/database.ts | 2 +- src/model/index.ts | 2 +- src/pages/404.tsx | 4 +- src/pages/_app.tsx | 2 +- src/pages/expression-sets/details.tsx | 60 ++----- src/pages/expression-sets/index.tsx | 50 +----- src/pages/expression-sets/practice.tsx | 162 +++--------------- src/pages/index.tsx | 2 +- src/util/data-utils.ts | 40 ----- src/util/index.ts | 2 + .../ErrorView.tsx} | 4 +- src/views/ErrorView/index.ts | 1 + .../DemoteExpressionButton.tsx | 45 +++++ .../ExpressionPracticeCardView.tsx | 45 +++++ .../ExpressionPracticeView.tsx | 27 +++ .../PromoteExpressionButton.tsx | 30 ++++ src/views/ExpressionPracticeView/index.ts | 1 + .../ExpressionSetDetailsView.tsx | 53 ++++++ src/views/ExpressionSetDetailsView/index.ts | 1 + .../ExpressionSetLink.tsx | 30 ++++ .../ExpressionSetListView.tsx | 23 +++ src/views/ExpressionSetListView/index.ts | 1 + src/views/PageWithError/index.ts | 1 - src/views/index.ts | 4 + 35 files changed, 344 insertions(+), 313 deletions(-) rename src/components/ExpressionSetCard/{index.tsx => index.ts} (100%) rename src/components/Navigation/{index.tsx => index.ts} (100%) create mode 100644 src/hooks/index.ts delete mode 100644 src/mock/index.ts delete mode 100644 src/util/data-utils.ts create mode 100644 src/util/index.ts rename src/views/{PageWithError/PageWithError.tsx => ErrorView/ErrorView.tsx} (68%) create mode 100644 src/views/ErrorView/index.ts create mode 100644 src/views/ExpressionPracticeView/DemoteExpressionButton.tsx create mode 100644 src/views/ExpressionPracticeView/ExpressionPracticeCardView.tsx create mode 100644 src/views/ExpressionPracticeView/ExpressionPracticeView.tsx create mode 100644 src/views/ExpressionPracticeView/PromoteExpressionButton.tsx create mode 100644 src/views/ExpressionPracticeView/index.ts create mode 100644 src/views/ExpressionSetDetailsView/ExpressionSetDetailsView.tsx create mode 100644 src/views/ExpressionSetDetailsView/index.ts create mode 100644 src/views/ExpressionSetListView/ExpressionSetLink.tsx create mode 100644 src/views/ExpressionSetListView/ExpressionSetListView.tsx create mode 100644 src/views/ExpressionSetListView/index.ts delete mode 100644 src/views/PageWithError/index.ts create mode 100644 src/views/index.ts diff --git a/src/components/ExpressionSetCard/index.tsx b/src/components/ExpressionSetCard/index.ts similarity index 100% rename from src/components/ExpressionSetCard/index.tsx rename to src/components/ExpressionSetCard/index.ts diff --git a/src/components/Navigation/NavigationItem.tsx b/src/components/Navigation/NavigationItem.tsx index d39b619..d8e4be1 100644 --- a/src/components/Navigation/NavigationItem.tsx +++ b/src/components/Navigation/NavigationItem.tsx @@ -1,7 +1,6 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { UrlObject } from "url"; export interface NavigationItemProps { text: string; diff --git a/src/components/Navigation/index.tsx b/src/components/Navigation/index.ts similarity index 100% rename from src/components/Navigation/index.tsx rename to src/components/Navigation/index.ts diff --git a/src/components/Page/Page.stories.tsx b/src/components/Page/Page.stories.tsx index c0e4a13..a891998 100644 --- a/src/components/Page/Page.stories.tsx +++ b/src/components/Page/Page.stories.tsx @@ -5,7 +5,6 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import { Page } from "./Page"; -import { Navigation, NavigationItem } from "../Navigation"; export default { title: "Components/Page", @@ -13,7 +12,7 @@ export default { } as ComponentMeta; export const Example: ComponentStory = (args) => ( - +
Content goes here
diff --git a/src/components/index.ts b/src/components/index.ts index 45c6538..7da765b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1,5 @@ export * from "./ExpressionCard"; +export * from "./ExpressionSetCard"; +export * from "./ExpressionSetInfo"; export * from "./Navigation"; +export * from "./Page"; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..2158ec8 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,6 @@ +export * from "./useExpressionCategories"; +export * from "./useExpressionFilterQueryIds"; +export * from "./useExpressionSet"; +export * from "./useExpressionSetQueryId"; +export * from "./useExpressionSets"; +export * from "./useExpressionsInSet"; diff --git a/src/hooks/useExpressionCategories.ts b/src/hooks/useExpressionCategories.ts index 8319772..96052a6 100644 --- a/src/hooks/useExpressionCategories.ts +++ b/src/hooks/useExpressionCategories.ts @@ -2,15 +2,13 @@ import { useLiveQuery } from "dexie-react-hooks"; import { database } from "../model"; export function useExpressionCategories(expression_id: number) { - return ( - useLiveQuery(() => { - return database.expression_to_category - .where({ expression_id }) - .toArray() - .then((relationships) => { - const category_ids = relationships.map((item) => item.category_id); - return database.categories.where("id").anyOf(category_ids).toArray(); - }); - }, [expression_id]) || [] - ); + return useLiveQuery(() => { + return database.expression_to_category + .where({ expression_id }) + .toArray() + .then((relationships) => { + const category_ids = relationships.map((item) => item.category_id); + return database.categories.where("id").anyOf(category_ids).toArray(); + }); + }, [expression_id]); } diff --git a/src/hooks/useExpressionSets.ts b/src/hooks/useExpressionSets.ts index b8aa79a..280a4fc 100644 --- a/src/hooks/useExpressionSets.ts +++ b/src/hooks/useExpressionSets.ts @@ -1,6 +1,6 @@ import { useLiveQuery } from "dexie-react-hooks"; -import { database, IndexedExpressionSet } from "../model"; +import { database } from "../model"; export function useExpressionSets() { - return useLiveQuery(() => database.expression_sets.toArray()) || []; + return useLiveQuery(() => database.expression_sets.toArray()); } diff --git a/src/hooks/useExpressionsInSet.ts b/src/hooks/useExpressionsInSet.ts index 371acfc..c4d77a6 100644 --- a/src/hooks/useExpressionsInSet.ts +++ b/src/hooks/useExpressionsInSet.ts @@ -2,20 +2,13 @@ import { useLiveQuery } from "dexie-react-hooks"; import { database } from "../model"; export function useExpressionsInSet(expression_set_id: number) { - return ( - useLiveQuery(() => { - return database.expression_to_expression_set - .where({ expression_set_id }) - .toArray() - .then((relationships) => { - const expression_ids = relationships.map( - (item) => item.expression_id - ); - return database.expressions - .where("id") - .anyOf(expression_ids) - .toArray(); - }); - }, [expression_set_id]) || [] - ); + return useLiveQuery(() => { + return database.expression_to_expression_set + .where({ expression_set_id }) + .toArray() + .then((relationships) => { + const expression_ids = relationships.map((item) => item.expression_id); + return database.expressions.where("id").anyOf(expression_ids).toArray(); + }); + }, [expression_set_id]); } diff --git a/src/mock/index.ts b/src/mock/index.ts deleted file mode 100644 index 94b9574..0000000 --- a/src/mock/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./mock-data"; diff --git a/src/mock/mock-data.ts b/src/mock/mock-data.ts index 24d6073..84f3d6d 100644 --- a/src/mock/mock-data.ts +++ b/src/mock/mock-data.ts @@ -4,7 +4,7 @@ import { ExpressionSet, ExpressionToCategory, ExpressionToExpressionSet, -} from "../model"; +} from "../model/types"; interface RawExpressionDataItem { prompt: string; diff --git a/src/model/database.ts b/src/model/database.ts index 2466a02..a08e348 100644 --- a/src/model/database.ts +++ b/src/model/database.ts @@ -1,5 +1,5 @@ +import { MockData } from "../mock/mock-data"; import Dexie, { Table } from "dexie"; -import { MockData } from "../mock"; import { Category, Expression, diff --git a/src/model/index.ts b/src/model/index.ts index 031f1be..650d00f 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -1,2 +1,2 @@ -export * from "./types"; export * from "./database"; +export * from "./types"; diff --git a/src/pages/404.tsx b/src/pages/404.tsx index c32f5f5..c8a666e 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,10 +1,10 @@ import type { NextPage } from "next"; -import { PageWithError } from "../views/PageWithError"; +import { ErrorView } from "../views"; const PageTitle = "Flash Card App - 404"; const Page404: NextPage = () => { - return ; + return ; }; export default Page404; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 22f6f7c..80976d1 100755 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -3,7 +3,7 @@ import "../styles/components.css"; import type { AppProps } from "next/app"; import { Navigation } from "../components"; -export default function MyApp({ Component, pageProps }: AppProps) { +export default function FlashCardApp({ Component, pageProps }: AppProps) { return (
diff --git a/src/pages/expression-sets/details.tsx b/src/pages/expression-sets/details.tsx index 5039f2a..d7f0ddc 100644 --- a/src/pages/expression-sets/details.tsx +++ b/src/pages/expression-sets/details.tsx @@ -1,12 +1,13 @@ import type { NextPage } from "next"; import dynamic from "next/dynamic"; import Link from "next/link"; -import { ExpressionSetInfo } from "../../components/ExpressionSetInfo/ExpressionSetInfo"; -import { Page } from "../../components/Page"; -import { useExpressionSet } from "../../hooks/useExpressionSet"; -import { useExpressionSetQueryId } from "../../hooks/useExpressionSetQueryId"; -import { useExpressionsInSet } from "../../hooks/useExpressionsInSet"; -import { PageWithError } from "../../views/PageWithError/PageWithError"; +import { ExpressionSetInfo, Page } from "../../components"; +import { + useExpressionSet, + useExpressionSetQueryId, + useExpressionsInSet, +} from "../../hooks"; +import { ErrorView, ExpressionSetDetailsView } from "../../views"; function pageTitle(name?: string) { return `Flash Card App - ${name || "Sets"}`; @@ -18,53 +19,18 @@ const ExpressionSetDetailsPage: NextPage = () => { const expressions = useExpressionsInSet(expression_set_id); // Fallback for expression set not found + if (!expressions) return null; if (!expression_set) { - return ( - - ); + return ; } // Fallback for expression set empty - if (expressions.length === 0) - return ( - -
- -

No expressions left in this set.

-
-
- ); - return ( -
-
- -
-
- - - Practice this set - - -
-
+
); }; diff --git a/src/pages/expression-sets/index.tsx b/src/pages/expression-sets/index.tsx index d2b8326..0ee18ed 100644 --- a/src/pages/expression-sets/index.tsx +++ b/src/pages/expression-sets/index.tsx @@ -1,61 +1,25 @@ import type { NextPage } from "next"; import dynamic from "next/dynamic"; -import Link from "next/link"; -import { ExpressionSetCard } from "../../components/ExpressionSetCard"; -import { Page } from "../../components/Page"; -import { useExpressionSets } from "../../hooks/useExpressionSets"; -import { useExpressionsInSet } from "../../hooks/useExpressionsInSet"; -import { IndexedExpressionSet } from "../../model"; -import { PageWithError } from "../../views/PageWithError"; +import { Page } from "../../components"; +import { useExpressionSets } from "../../hooks"; +import { ErrorView, ExpressionSetListView } from "../../views"; const PageTitle = "Flash Card App - Sets"; const ExpressionSetListPage: NextPage = () => { const expression_sets = useExpressionSets(); - - if (!expression_sets?.length) { - return ( - - ); + if (!expression_sets) return null; + if (!expression_sets.length) { + return ; } return ( -
- {expression_sets.map(({ id, name, description }) => ( - - ))} -
+
); }; -function ExpressionSetLink({ id, description, name }: IndexedExpressionSet) { - const expressions = useExpressionsInSet(id!) || []; - return ( - - - - - - ); -} - export default dynamic(() => Promise.resolve(ExpressionSetListPage), { ssr: false, }); diff --git a/src/pages/expression-sets/practice.tsx b/src/pages/expression-sets/practice.tsx index 2f1c1f0..f1e789e 100644 --- a/src/pages/expression-sets/practice.tsx +++ b/src/pages/expression-sets/practice.tsx @@ -1,162 +1,44 @@ -import type { NextPage } from "next"; +import { NextPage } from "next"; import dynamic from "next/dynamic"; -import { useRouter } from "next/router"; -import { useCallback, useMemo, useState } from "react"; -import { ExpressionCard } from "../../components"; -import { Page } from "../../components/Page"; -import { useExpressionCategories } from "../../hooks/useExpressionCategories"; -import { useExpressionFilterQueryIds } from "../../hooks/useExpressionFilterQueryIds"; -import { useExpressionSetQueryId } from "../../hooks/useExpressionSetQueryId"; -import { useExpressionsInSet } from "../../hooks/useExpressionsInSet"; +import { Page } from "../../components"; import { - assignExpressionToSet, - IndexedCategory, - IndexedExpression, -} from "../../model"; -import { sample } from "../../util/array-utils"; -import { PageWithError } from "../../views/PageWithError/PageWithError"; + useExpressionFilterQueryIds, + useExpressionSetQueryId, + useExpressionsInSet, +} from "../../hooks"; +import { ErrorView } from "../../views"; +import { ExpressionPracticeView } from "../../views"; const PageTitle = "Flash Card App - Practice"; -// Do random selection here so we don't keep flipping states with interaction const ExpressionPracticePage: NextPage = () => { - // Query info const expression_set_id = useExpressionSetQueryId(); const filter_ids = useExpressionFilterQueryIds(); + const expressions = useExpressionsInSet(expression_set_id); - // Filter out failed expressions and select random expression - const expressions = useExpressionsInSet(expression_set_id).filter( - (expression) => !filter_ids.includes(expression.id!) - ); - const expression: IndexedExpression | undefined = useMemo( - () => (expressions ? sample(expressions) : undefined), - [expressions] - ); - - // Fetch categories that the expression relates to - const categories = useExpressionCategories(expression?.id || 0); + // Fallback rendering for content not yet fetched + if (!expressions) return null; + if (!filter_ids) return null; // Fallback views for expression set content not found if (!expression_set_id) { - return ( - - ); + return ; } - if (!expression) { - return ( - - ); - } - - return ( - + const filtered_expressions = expressions.filter( + (expression) => !filter_ids.includes(expression.id!) ); -}; + if (!filtered_expressions.length) { + return ( + + ); + } -interface ExpressionCardPracticeViewProps { - expression: IndexedExpression; - categories: IndexedCategory[]; -} - -// Handle internal state here -function ExpressionCardPracticeView({ - expression, - categories, -}: ExpressionCardPracticeViewProps) { - const [revealed, setRevealed] = useState(false); return ( -
-
- category.name)} - description={expression.description} - show_description={revealed} - /> -
-
- {revealed ? ( - <> - - - - ) : ( - - )} -
-
+
); -} - -interface ExpressionIdProps { - expression_id: number; -} - -function PromoteExpressionButton({ expression_id }: ExpressionIdProps) { - const expression_set_id = useExpressionSetQueryId(); - const handleClick = useCallback(() => { - assignExpressionToSet({ - expression_id, - expression_set_id: expression_set_id + 1, - }); - }, [expression_id, expression_set_id]); - - return ( - - ); -} - -function DemoteExpressionButton({ expression_id }: ExpressionIdProps) { - const { query, pathname, push } = useRouter(); - const expression_set_id = useExpressionSetQueryId(); - const handleClick = useCallback(() => { - if (expression_set_id === 1) { - const filter_ids = query["filter-ids"] - ? `${query["filter-ids"]} ${expression_id}` - : `${expression_id}`; - push({ - pathname, - query: { - ...query, - "filter-ids": filter_ids, - }, - }); - } else { - assignExpressionToSet({ - expression_id, - expression_set_id: Math.max(1, expression_set_id - 1), - }); - } - }, [expression_id, expression_set_id, pathname, push, query]); - - return ( - - ); -} +}; export default dynamic(() => Promise.resolve(ExpressionPracticePage), { ssr: false, diff --git a/src/pages/index.tsx b/src/pages/index.tsx index e892539..2737bc6 100755 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,5 +1,5 @@ import type { NextPage } from "next"; -import { Page } from "../components/Page"; +import { Page } from "../components"; const PageTitle = "Flash Card App"; diff --git a/src/util/data-utils.ts b/src/util/data-utils.ts deleted file mode 100644 index 774fdda..0000000 --- a/src/util/data-utils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - Category, - Expression, - ExpressionToCategory, - ExpressionToExpressionSet, -} from "../model"; - -interface GetExpressionsInSetParams { - expression_set_id: number; - expressions: Expression[]; - expression_to_expression_set: ExpressionToExpressionSet[]; -} - -export function getExpressionsInSet({ - expression_set_id, - expression_to_expression_set, - expressions, -}: GetExpressionsInSetParams): Expression[] { - const expression_ids = expression_to_expression_set - .filter((item) => item.expression_set_id === expression_set_id) - .map((item) => item.expression_id); - return expressions.filter((item) => expression_ids.includes(item.id)); -} - -interface GetCategoriesInExpressionParams { - expression_id: number; - categories: Category[]; - expression_to_category: ExpressionToCategory[]; -} - -export function getCategoriesInExpression({ - categories, - expression_id, - expression_to_category, -}: GetCategoriesInExpressionParams) { - const category_ids = expression_to_category - .filter((item) => item.expression_id === expression_id) - .map((item) => item.category_id); - return categories.filter((item) => category_ids.includes(item.id)); -} diff --git a/src/util/index.ts b/src/util/index.ts new file mode 100644 index 0000000..02e18a4 --- /dev/null +++ b/src/util/index.ts @@ -0,0 +1,2 @@ +export * from "./array-utils"; +export * from "./clamp"; diff --git a/src/views/PageWithError/PageWithError.tsx b/src/views/ErrorView/ErrorView.tsx similarity index 68% rename from src/views/PageWithError/PageWithError.tsx rename to src/views/ErrorView/ErrorView.tsx index 05f1f2a..0ce50f6 100644 --- a/src/views/PageWithError/PageWithError.tsx +++ b/src/views/ErrorView/ErrorView.tsx @@ -1,11 +1,11 @@ import { Page } from "../../components/Page"; -export interface PageWithErrorProps { +export interface ErrorViewProps { title: string; message: string; } -export function PageWithError({ title, message }: PageWithErrorProps) { +export function ErrorView({ title, message }: ErrorViewProps) { return (
diff --git a/src/views/ErrorView/index.ts b/src/views/ErrorView/index.ts new file mode 100644 index 0000000..c1ca494 --- /dev/null +++ b/src/views/ErrorView/index.ts @@ -0,0 +1 @@ +export * from "./ErrorView"; diff --git a/src/views/ExpressionPracticeView/DemoteExpressionButton.tsx b/src/views/ExpressionPracticeView/DemoteExpressionButton.tsx new file mode 100644 index 0000000..a728a2d --- /dev/null +++ b/src/views/ExpressionPracticeView/DemoteExpressionButton.tsx @@ -0,0 +1,45 @@ +import { useRouter } from "next/router"; +import { useCallback } from "react"; +import { useExpressionSetQueryId } from "../../hooks"; +import { assignExpressionToSet } from "../../model"; + +// TODO fix promotion algorithm so it uses a destination expression_set_id + +export interface DemoteExpressionButtonProps { + expression_id: number; +} + +export function DemoteExpressionButton({ + expression_id, +}: DemoteExpressionButtonProps) { + const { query, pathname, push } = useRouter(); + const expression_set_id = useExpressionSetQueryId(); + const handleClick = useCallback(() => { + if (expression_set_id === 1) { + const filter_ids = query["filter-ids"] + ? `${query["filter-ids"]} ${expression_id}` + : `${expression_id}`; + push({ + pathname, + query: { + ...query, + "filter-ids": filter_ids, + }, + }); + } else { + assignExpressionToSet({ + expression_id, + expression_set_id: Math.max(1, expression_set_id - 1), + }); + } + }, [expression_id, expression_set_id, pathname, push, query]); + + return ( + + ); +} diff --git a/src/views/ExpressionPracticeView/ExpressionPracticeCardView.tsx b/src/views/ExpressionPracticeView/ExpressionPracticeCardView.tsx new file mode 100644 index 0000000..d97fcef --- /dev/null +++ b/src/views/ExpressionPracticeView/ExpressionPracticeCardView.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { ExpressionCard } from "../../components"; +import { IndexedExpression, IndexedCategory } from "../../model"; +import { DemoteExpressionButton } from "./DemoteExpressionButton"; +import { PromoteExpressionButton } from "./PromoteExpressionButton"; + +export interface ExpressionPracticeCardViewProps { + expression: IndexedExpression; + categories: IndexedCategory[]; +} + +// Handle internal state here +export function ExpressionPracticeCardView({ + expression, + categories, +}: ExpressionPracticeCardViewProps) { + const [revealed, setRevealed] = useState(false); + return ( +
+
+ category.name)} + description={expression.description} + show_description={revealed} + /> +
+
+ {revealed ? ( + <> + + + + ) : ( + + )} +
+
+ ); +} diff --git a/src/views/ExpressionPracticeView/ExpressionPracticeView.tsx b/src/views/ExpressionPracticeView/ExpressionPracticeView.tsx new file mode 100644 index 0000000..1b11dee --- /dev/null +++ b/src/views/ExpressionPracticeView/ExpressionPracticeView.tsx @@ -0,0 +1,27 @@ +import { useMemo } from "react"; +import { useExpressionCategories } from "../../hooks"; +import { IndexedExpression } from "../../model"; +import { sample } from "../../util"; +import { ExpressionPracticeCardView } from "./ExpressionPracticeCardView"; + +export interface ExpressionPracticeViewProps { + select_from_expressions: IndexedExpression[]; +} + +export function ExpressionPracticeView({ + select_from_expressions: expressions, +}: ExpressionPracticeViewProps) { + const expression = useMemo(() => sample(expressions), [expressions]); + const categories = useExpressionCategories(expression.id!); + if (!categories) return null; + + // Delegate internal interaction state to next component so it resets + // on an expression switch + return ( + + ); +} diff --git a/src/views/ExpressionPracticeView/PromoteExpressionButton.tsx b/src/views/ExpressionPracticeView/PromoteExpressionButton.tsx new file mode 100644 index 0000000..8b7d6b2 --- /dev/null +++ b/src/views/ExpressionPracticeView/PromoteExpressionButton.tsx @@ -0,0 +1,30 @@ +import { useCallback } from "react"; +import { useExpressionSetQueryId } from "../../hooks"; +import { assignExpressionToSet } from "../../model"; + +// TODO fix promotion algorithm so it uses a destination expression_set_id + +export interface PromoteExpressionButtonProps { + expression_id: number; +} + +export function PromoteExpressionButton({ + expression_id, +}: PromoteExpressionButtonProps) { + const expression_set_id = useExpressionSetQueryId(); + const handleClick = useCallback(() => { + assignExpressionToSet({ + expression_id, + expression_set_id: expression_set_id + 1, + }); + }, [expression_id, expression_set_id]); + + return ( + + ); +} diff --git a/src/views/ExpressionPracticeView/index.ts b/src/views/ExpressionPracticeView/index.ts new file mode 100644 index 0000000..fb01f42 --- /dev/null +++ b/src/views/ExpressionPracticeView/index.ts @@ -0,0 +1 @@ +export * from "./ExpressionPracticeView"; diff --git a/src/views/ExpressionSetDetailsView/ExpressionSetDetailsView.tsx b/src/views/ExpressionSetDetailsView/ExpressionSetDetailsView.tsx new file mode 100644 index 0000000..b90f6ae --- /dev/null +++ b/src/views/ExpressionSetDetailsView/ExpressionSetDetailsView.tsx @@ -0,0 +1,53 @@ +import Link from "next/link"; +import { ExpressionSetInfo } from "../../components"; +import { IndexedExpressionSet } from "../../model"; + +export interface ExpressionSetDetailsViewProps { + expression_set: IndexedExpressionSet; + expression_count: number; +} + +export function ExpressionSetDetailsView({ + expression_set, + expression_count, +}: ExpressionSetDetailsViewProps) { + if (!expression_count) { + return ( +
+ +

No expressions left in this set.

+
+ ); + } + + return ( +
+
+ +
+
+ + + Practice this set + + +
+
+ ); +} diff --git a/src/views/ExpressionSetDetailsView/index.ts b/src/views/ExpressionSetDetailsView/index.ts new file mode 100644 index 0000000..24780d3 --- /dev/null +++ b/src/views/ExpressionSetDetailsView/index.ts @@ -0,0 +1 @@ +export * from "./ExpressionSetDetailsView"; diff --git a/src/views/ExpressionSetListView/ExpressionSetLink.tsx b/src/views/ExpressionSetListView/ExpressionSetLink.tsx new file mode 100644 index 0000000..8fd1a9d --- /dev/null +++ b/src/views/ExpressionSetListView/ExpressionSetLink.tsx @@ -0,0 +1,30 @@ +import Link from "next/link"; +import { ExpressionSetCard } from "../../components"; +import { useExpressionsInSet } from "../../hooks"; +import { IndexedExpressionSet } from "../../model"; + +export function ExpressionSetLink({ + id, + description, + name, +}: IndexedExpressionSet) { + const expressions = useExpressionsInSet(id!) || []; + + return ( + + + + + + ); +} diff --git a/src/views/ExpressionSetListView/ExpressionSetListView.tsx b/src/views/ExpressionSetListView/ExpressionSetListView.tsx new file mode 100644 index 0000000..8e133d3 --- /dev/null +++ b/src/views/ExpressionSetListView/ExpressionSetListView.tsx @@ -0,0 +1,23 @@ +import { IndexedExpressionSet } from "../../model"; +import { ExpressionSetLink } from "./ExpressionSetLink"; + +export interface ExpressionSetListViewProps { + expression_sets: IndexedExpressionSet[]; +} + +export function ExpressionSetListView({ + expression_sets, +}: ExpressionSetListViewProps) { + return ( +
+ {expression_sets.map(({ id, name, description }) => ( + + ))} +
+ ); +} diff --git a/src/views/ExpressionSetListView/index.ts b/src/views/ExpressionSetListView/index.ts new file mode 100644 index 0000000..9e75d86 --- /dev/null +++ b/src/views/ExpressionSetListView/index.ts @@ -0,0 +1 @@ +export * from "./ExpressionSetListView"; diff --git a/src/views/PageWithError/index.ts b/src/views/PageWithError/index.ts deleted file mode 100644 index 1ac8772..0000000 --- a/src/views/PageWithError/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./PageWithError"; diff --git a/src/views/index.ts b/src/views/index.ts new file mode 100644 index 0000000..5f5427e --- /dev/null +++ b/src/views/index.ts @@ -0,0 +1,4 @@ +export * from "./ErrorView"; +export * from "./ExpressionPracticeView"; +export * from "./ExpressionSetDetailsView"; +export * from "./ExpressionSetListView";