From 6f600cf4116f898db7a2957611e03ccfcfd5fc2f Mon Sep 17 00:00:00 2001 From: Thiago Chaves Date: Fri, 15 Jul 2022 01:29:10 +0300 Subject: [PATCH] Separate error views, minor refactors, stub interaction on practice page --- .../ExpressionSetCard.stories.tsx | 2 +- .../ExpressionSetCard/ExpressionSetCard.tsx | 6 +- .../ExpressionSetInfo/ExpressionSetInfo.tsx | 6 +- src/hooks/useExpressionCategories.ts | 12 ++ src/hooks/useExpressionsInSet.ts | 12 ++ src/hooks/useQueriedExpressionSet.ts | 14 ++ src/hooks/useRandomExpressionInSet.ts | 11 ++ src/pages/expression-sets/details.tsx | 35 ++--- src/pages/expression-sets/index.tsx | 49 ++++--- src/pages/expression-sets/practice.tsx | 122 +++++++++--------- src/styles/components.css | 12 +- src/views/PageWithError/PageWithError.tsx | 15 +++ src/views/PageWithError/index.ts | 1 + 13 files changed, 184 insertions(+), 113 deletions(-) create mode 100644 src/hooks/useExpressionCategories.ts create mode 100644 src/hooks/useExpressionsInSet.ts create mode 100644 src/hooks/useQueriedExpressionSet.ts create mode 100644 src/hooks/useRandomExpressionInSet.ts create mode 100644 src/views/PageWithError/PageWithError.tsx create mode 100644 src/views/PageWithError/index.ts diff --git a/src/components/ExpressionSetCard/ExpressionSetCard.stories.tsx b/src/components/ExpressionSetCard/ExpressionSetCard.stories.tsx index ec45ace..a5fff93 100644 --- a/src/components/ExpressionSetCard/ExpressionSetCard.stories.tsx +++ b/src/components/ExpressionSetCard/ExpressionSetCard.stories.tsx @@ -19,5 +19,5 @@ export const Daily = Template.bind({}); Daily.args = { name: "Daily", description: "Words for daily practice", - word_count: 11, + expression_count: 11, }; diff --git a/src/components/ExpressionSetCard/ExpressionSetCard.tsx b/src/components/ExpressionSetCard/ExpressionSetCard.tsx index 0585d57..c2d48c6 100644 --- a/src/components/ExpressionSetCard/ExpressionSetCard.tsx +++ b/src/components/ExpressionSetCard/ExpressionSetCard.tsx @@ -1,19 +1,19 @@ export interface ExpressionSetCardProps { name: string; description: string; - word_count: number; + expression_count: number; } export function ExpressionSetCard({ name, description, - word_count, + expression_count, }: ExpressionSetCardProps) { return (

{name}

- {word_count} word(s) + {expression_count} expressions(s)

{description}

diff --git a/src/components/ExpressionSetInfo/ExpressionSetInfo.tsx b/src/components/ExpressionSetInfo/ExpressionSetInfo.tsx index b7a573c..eda52f3 100644 --- a/src/components/ExpressionSetInfo/ExpressionSetInfo.tsx +++ b/src/components/ExpressionSetInfo/ExpressionSetInfo.tsx @@ -5,14 +5,14 @@ export interface ExpressionSetInfo { id: number; name: string; description: string; - word_count: number; + expression_count: number; } export function ExpressionSetInfo({ id, name, description, - word_count, + expression_count, }: ExpressionSetInfo) { return (
@@ -38,7 +38,7 @@ export function ExpressionSetInfo({ - {word_count} word(s) + {expression_count} expression(s)

{description}

diff --git a/src/hooks/useExpressionCategories.ts b/src/hooks/useExpressionCategories.ts new file mode 100644 index 0000000..2cbb47b --- /dev/null +++ b/src/hooks/useExpressionCategories.ts @@ -0,0 +1,12 @@ +import { Category, Expression, ExpressionId } from "../model"; +import { useExpressionData } from "./useExpressionData"; + +export function useExpressionCategories( + expression_id?: ExpressionId | undefined +): Category[] { + const { categories, expression_to_category } = useExpressionData(); + 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/hooks/useExpressionsInSet.ts b/src/hooks/useExpressionsInSet.ts new file mode 100644 index 0000000..c10dc8d --- /dev/null +++ b/src/hooks/useExpressionsInSet.ts @@ -0,0 +1,12 @@ +import { Expression, ExpressionSetId } from "../model"; +import { useExpressionData } from "./useExpressionData"; + +export function useExpressionsInSet( + expression_set_id: ExpressionSetId | undefined +): Expression[] { + const { expressions, expression_to_expression_set } = useExpressionData(); + 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)); +} diff --git a/src/hooks/useQueriedExpressionSet.ts b/src/hooks/useQueriedExpressionSet.ts new file mode 100644 index 0000000..0f2f923 --- /dev/null +++ b/src/hooks/useQueriedExpressionSet.ts @@ -0,0 +1,14 @@ +import { useRouter } from "next/router"; +import { ExpressionSet } from "../model"; +import { useExpressionData } from "./useExpressionData"; + +export function useQueriedExpressionSet(): ExpressionSet | undefined { + const { query } = useRouter(); + const { expression_sets } = useExpressionData(); + const expression_set_id = Number.parseInt(query["set-id"] as string); + + if (!Number.isInteger(expression_set_id)) { + return undefined; + } + return expression_sets.find((item) => item.id === expression_set_id); +} diff --git a/src/hooks/useRandomExpressionInSet.ts b/src/hooks/useRandomExpressionInSet.ts new file mode 100644 index 0000000..2c8df15 --- /dev/null +++ b/src/hooks/useRandomExpressionInSet.ts @@ -0,0 +1,11 @@ +import { Expression, ExpressionSetId } from "../model"; +import { sample } from "../util/array-utils"; +import { useExpressionsInSet } from "./useExpressionsInSet"; + +export function useRandomExpressionInSet( + expression_set_id: ExpressionSetId | undefined +): Expression | undefined { + const expressions = useExpressionsInSet(expression_set_id); + if (expressions.length === 0) return undefined; + return sample(expressions); +} diff --git a/src/pages/expression-sets/details.tsx b/src/pages/expression-sets/details.tsx index 8d0a3af..ba4c04f 100644 --- a/src/pages/expression-sets/details.tsx +++ b/src/pages/expression-sets/details.tsx @@ -1,33 +1,22 @@ import type { NextPage } from "next"; import Link from "next/link"; -import { useRouter } from "next/router"; import { ExpressionSetInfo } from "../../components/ExpressionSetInfo/ExpressionSetInfo"; import { Page } from "../../components/Page"; -import { useExpressionData } from "../../hooks/useExpressionData"; +import { useExpressionsInSet } from "../../hooks/useExpressionsInSet"; +import { useQueriedExpressionSet } from "../../hooks/useQueriedExpressionSet"; +import { PageWithError } from "../../views/PageWithError/PageWithError"; const ExpressionSetDetailsPage: NextPage = () => { - const { query } = useRouter(); - const { expression_sets, expression_to_expression_set } = useExpressionData(); + const expression_set = useQueriedExpressionSet(); + const expressions = useExpressionsInSet(expression_set?.id); // Fallback for expression set not found - const expression_set_id = Number.parseInt(query["set-id"] as string); - const expression_set = expression_sets.find( - (item) => item.id === expression_set_id - ); - if (!expression_set) - return ( - -
-

Expression set not found

-
-
- ); + if (!expression_set) { + return ; + } - // Fallback for expression set empty TODO fix style - const word_count = expression_to_expression_set.filter( - (item) => item.expression_set_id === expression_set.id - ).length; - if (!word_count) + // Fallback for expression set empty + if (expressions.length === 0) return (
@@ -35,7 +24,7 @@ const ExpressionSetDetailsPage: NextPage = () => { id={expression_set.id} name={expression_set.name} description={expression_set.description} - word_count={word_count} + expression_count={expressions.length} />

No expressions left in this set.

@@ -50,7 +39,7 @@ const ExpressionSetDetailsPage: NextPage = () => { id={expression_set.id} name={expression_set.name} description={expression_set.description} - word_count={word_count} + expression_count={expressions.length} />
diff --git a/src/pages/expression-sets/index.tsx b/src/pages/expression-sets/index.tsx index ec09738..a3723fe 100644 --- a/src/pages/expression-sets/index.tsx +++ b/src/pages/expression-sets/index.tsx @@ -3,38 +3,47 @@ import Link from "next/link"; import { ExpressionSetCard } from "../../components/ExpressionSetCard"; import { Page } from "../../components/Page"; import { useExpressionData } from "../../hooks/useExpressionData"; +import { useExpressionsInSet } from "../../hooks/useExpressionsInSet"; +import { ExpressionSet } from "../../model"; const ExpressionSetListPage: NextPage = () => { - const { expression_sets, expression_to_expression_set } = useExpressionData(); + const { expression_sets } = useExpressionData(); return (
{expression_sets.map(({ id, name, description }) => ( - - - item.expression_set_id === id - ).length - } - /> - - + id={id} + name={name} + description={description} + /> ))}
); }; +function ExpressionSetLink({ id, description, name }: ExpressionSet) { + const expressions = useExpressionsInSet(id); + return ( + + + + + + ); +} + export default ExpressionSetListPage; diff --git a/src/pages/expression-sets/practice.tsx b/src/pages/expression-sets/practice.tsx index 0ce6dfb..53cbda9 100644 --- a/src/pages/expression-sets/practice.tsx +++ b/src/pages/expression-sets/practice.tsx @@ -1,86 +1,86 @@ import type { NextPage } from "next"; import Link from "next/link"; import { useRouter } from "next/router"; +import { useState } from "react"; import { ExpressionCard } from "../../components"; import { Page } from "../../components/Page"; -import { useExpressionData } from "../../hooks/useExpressionData"; -import { sample } from "../../util/array-utils"; -import { - getCategoriesInExpression, - getExpressionsInSet, -} from "../../util/data-utils"; +import { useExpressionCategories } from "../../hooks/useExpressionCategories"; +import { useQueriedExpressionSet } from "../../hooks/useQueriedExpressionSet"; +import { useRandomExpressionInSet } from "../../hooks/useRandomExpressionInSet"; +import { Category, Expression } from "../../model"; +import { PageWithError } from "../../views/PageWithError/PageWithError"; -const ExpressionSetListPage: NextPage = () => { - const { query, pathname } = useRouter(); - const { - expressions, - categories, - expression_sets, - expression_to_expression_set, - expression_to_category, - } = useExpressionData(); +// Do random selection here so we don't keep flipping states with interaction +const ExpressionPracticePage: NextPage = () => { + const expression_set = useQueriedExpressionSet(); + const expression = useRandomExpressionInSet(expression_set?.id); + const categories = useExpressionCategories(expression?.id); - // Fallback for expression set not found - const expression_set = expression_sets.find( - (item) => item.id === Number.parseInt(query["set-id"] as string) - ); - if (!expression_set) - return ( - -

Expression set not found

-
- ); - - // Fallback for expression not found - const expression_candidates = getExpressionsInSet({ - expression_set_id: expression_set.id, - expression_to_expression_set, - expressions, - }); - - if (expression_candidates.length === 0) { - return ( - -

No expressions left in this set

-
- ); + // Fallback views for expression set content not found + if (!expression_set) { + return ; } + if (!expression) { + return ; + } + return ( + + ); +}; - // Choose word and find categories - // Find categories - const expression = sample(expression_candidates); - const expression_categories = getCategoriesInExpression({ - categories, - expression_id: expression.id, - expression_to_category, - }); +interface ExpressionCardPracticeViewProps { + expression: Expression; + categories: Category[]; +} +// Handle internal state here +function ExpressionCardPracticeView({ + expression, + categories, +}: ExpressionCardPracticeViewProps) { + const { query, pathname } = useRouter(); + const [revealed, setRevealed] = useState(false); return (
category.name)} + categories={categories.map((category) => category.name)} description={expression.description} - show_description + show_description={revealed} />
- - -
wrong
-
- - - -
right
-
- + {revealed ? ( + <> + + +
Wrong
+
+ + + +
Right
+
+ + + ) : ( + + )}
); -}; +} -export default ExpressionSetListPage; +export default ExpressionPracticePage; diff --git a/src/styles/components.css b/src/styles/components.css index 73c1f1c..8b31d83 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -93,6 +93,14 @@ height: 64px; } +.navigation-item:hover { + background-color: antiquewhite; +} + +.navigation-item.active { + background-color: bisque; +} + .navigation-item-bottom { display: flex; flex-shrink: 0; @@ -105,11 +113,11 @@ height: 80px; } -.navigation-item:hover { +.navigation-item-bottom:hover { background-color: antiquewhite; } -.navigation-item.active { +.navigation-item-bottom.active { background-color: bisque; } diff --git a/src/views/PageWithError/PageWithError.tsx b/src/views/PageWithError/PageWithError.tsx new file mode 100644 index 0000000..440e24c --- /dev/null +++ b/src/views/PageWithError/PageWithError.tsx @@ -0,0 +1,15 @@ +import { Page } from "../../components/Page"; + +export interface PageWithErrorProps { + message: string; +} + +export function PageWithError({ message }: PageWithErrorProps) { + return ( + +
+

{message}

+
+
+ ); +} diff --git a/src/views/PageWithError/index.ts b/src/views/PageWithError/index.ts new file mode 100644 index 0000000..1ac8772 --- /dev/null +++ b/src/views/PageWithError/index.ts @@ -0,0 +1 @@ +export * from "./PageWithError";