Separate error views, minor refactors, stub interaction on practice page

This commit is contained in:
Thiago Chaves 2022-07-15 01:29:10 +03:00
parent 72249d9105
commit 6f600cf411
13 changed files with 184 additions and 113 deletions

View File

@ -19,5 +19,5 @@ export const Daily = Template.bind({});
Daily.args = { Daily.args = {
name: "Daily", name: "Daily",
description: "Words for daily practice", description: "Words for daily practice",
word_count: 11, expression_count: 11,
}; };

View File

@ -1,19 +1,19 @@
export interface ExpressionSetCardProps { export interface ExpressionSetCardProps {
name: string; name: string;
description: string; description: string;
word_count: number; expression_count: number;
} }
export function ExpressionSetCard({ export function ExpressionSetCard({
name, name,
description, description,
word_count, expression_count,
}: ExpressionSetCardProps) { }: ExpressionSetCardProps) {
return ( return (
<article className="content-card"> <article className="content-card">
<h2 className="content-row text-title margin-small">{name}</h2> <h2 className="content-row text-title margin-small">{name}</h2>
<span className="content-row text-meta margin-small"> <span className="content-row text-meta margin-small">
{word_count} word(s) {expression_count} expressions(s)
</span> </span>
<p className="content-row text-details margin-paragraph">{description}</p> <p className="content-row text-details margin-paragraph">{description}</p>
</article> </article>

View File

@ -5,14 +5,14 @@ export interface ExpressionSetInfo {
id: number; id: number;
name: string; name: string;
description: string; description: string;
word_count: number; expression_count: number;
} }
export function ExpressionSetInfo({ export function ExpressionSetInfo({
id, id,
name, name,
description, description,
word_count, expression_count,
}: ExpressionSetInfo) { }: ExpressionSetInfo) {
return ( return (
<section> <section>
@ -38,7 +38,7 @@ export function ExpressionSetInfo({
</Link> </Link>
</h2> </h2>
<span className="content-row text-meta margin-small"> <span className="content-row text-meta margin-small">
{word_count} word(s) {expression_count} expression(s)
</span> </span>
<p className="content-row text-details margin-paragraph">{description}</p> <p className="content-row text-details margin-paragraph">{description}</p>
</section> </section>

View File

@ -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));
}

View File

@ -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));
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -1,33 +1,22 @@
import type { NextPage } from "next"; import type { NextPage } from "next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
import { ExpressionSetInfo } from "../../components/ExpressionSetInfo/ExpressionSetInfo"; import { ExpressionSetInfo } from "../../components/ExpressionSetInfo/ExpressionSetInfo";
import { Page } from "../../components/Page"; 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 ExpressionSetDetailsPage: NextPage = () => {
const { query } = useRouter(); const expression_set = useQueriedExpressionSet();
const { expression_sets, expression_to_expression_set } = useExpressionData(); const expressions = useExpressionsInSet(expression_set?.id);
// Fallback for expression set not found // Fallback for expression set not found
const expression_set_id = Number.parseInt(query["set-id"] as string); if (!expression_set) {
const expression_set = expression_sets.find( return <PageWithError message="Expression set not found" />;
(item) => item.id === expression_set_id }
);
if (!expression_set)
return (
<Page>
<div className="page-with-padding scroll">
<p className="text-details">Expression set not found</p>
</div>
</Page>
);
// Fallback for expression set empty TODO fix style // Fallback for expression set empty
const word_count = expression_to_expression_set.filter( if (expressions.length === 0)
(item) => item.expression_set_id === expression_set.id
).length;
if (!word_count)
return ( return (
<Page> <Page>
<div className="page-with-padding scroll"> <div className="page-with-padding scroll">
@ -35,7 +24,7 @@ const ExpressionSetDetailsPage: NextPage = () => {
id={expression_set.id} id={expression_set.id}
name={expression_set.name} name={expression_set.name}
description={expression_set.description} description={expression_set.description}
word_count={word_count} expression_count={expressions.length}
/> />
<p className="text-details">No expressions left in this set.</p> <p className="text-details">No expressions left in this set.</p>
</div> </div>
@ -50,7 +39,7 @@ const ExpressionSetDetailsPage: NextPage = () => {
id={expression_set.id} id={expression_set.id}
name={expression_set.name} name={expression_set.name}
description={expression_set.description} description={expression_set.description}
word_count={word_count} expression_count={expressions.length}
/> />
</section> </section>
<section className="navigation-bottom"> <section className="navigation-bottom">

View File

@ -3,16 +3,32 @@ import Link from "next/link";
import { ExpressionSetCard } from "../../components/ExpressionSetCard"; import { ExpressionSetCard } from "../../components/ExpressionSetCard";
import { Page } from "../../components/Page"; import { Page } from "../../components/Page";
import { useExpressionData } from "../../hooks/useExpressionData"; import { useExpressionData } from "../../hooks/useExpressionData";
import { useExpressionsInSet } from "../../hooks/useExpressionsInSet";
import { ExpressionSet } from "../../model";
const ExpressionSetListPage: NextPage = () => { const ExpressionSetListPage: NextPage = () => {
const { expression_sets, expression_to_expression_set } = useExpressionData(); const { expression_sets } = useExpressionData();
return ( return (
<Page> <Page>
<div className="page-with-padding content-list scroll"> <div className="page-with-padding content-list scroll">
{expression_sets.map(({ id, name, description }) => ( {expression_sets.map(({ id, name, description }) => (
<Link <ExpressionSetLink
key={id} key={id}
id={id}
name={name}
description={description}
/>
))}
</div>
</Page>
);
};
function ExpressionSetLink({ id, description, name }: ExpressionSet) {
const expressions = useExpressionsInSet(id);
return (
<Link
href={{ href={{
pathname: "/expression-sets/details", pathname: "/expression-sets/details",
query: { "set-id": id }, query: { "set-id": id },
@ -23,18 +39,11 @@ const ExpressionSetListPage: NextPage = () => {
<ExpressionSetCard <ExpressionSetCard
name={name} name={name}
description={description} description={description}
word_count={ expression_count={expressions.length}
expression_to_expression_set.filter(
(item) => item.expression_set_id === id
).length
}
/> />
</a> </a>
</Link> </Link>
))}
</div>
</Page>
); );
}; }
export default ExpressionSetListPage; export default ExpressionSetListPage;

View File

@ -1,86 +1,86 @@
import type { NextPage } from "next"; import type { NextPage } from "next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import { ExpressionCard } from "../../components"; import { ExpressionCard } from "../../components";
import { Page } from "../../components/Page"; import { Page } from "../../components/Page";
import { useExpressionData } from "../../hooks/useExpressionData"; import { useExpressionCategories } from "../../hooks/useExpressionCategories";
import { sample } from "../../util/array-utils"; import { useQueriedExpressionSet } from "../../hooks/useQueriedExpressionSet";
import { import { useRandomExpressionInSet } from "../../hooks/useRandomExpressionInSet";
getCategoriesInExpression, import { Category, Expression } from "../../model";
getExpressionsInSet, import { PageWithError } from "../../views/PageWithError/PageWithError";
} from "../../util/data-utils";
const ExpressionSetListPage: NextPage = () => { // Do random selection here so we don't keep flipping states with interaction
const { query, pathname } = useRouter(); const ExpressionPracticePage: NextPage = () => {
const { const expression_set = useQueriedExpressionSet();
expressions, const expression = useRandomExpressionInSet(expression_set?.id);
categories, const categories = useExpressionCategories(expression?.id);
expression_sets,
expression_to_expression_set,
expression_to_category,
} = useExpressionData();
// Fallback for expression set not found // Fallback views for expression set content not found
const expression_set = expression_sets.find( if (!expression_set) {
(item) => item.id === Number.parseInt(query["set-id"] as string) return <PageWithError message="Expression set not found" />;
); }
if (!expression_set) if (!expression) {
return <PageWithError message="No expressions left in this set" />;
}
return ( return (
<Page> <ExpressionCardPracticeView
<p className="text-details">Expression set not found</p> key={expression.id}
</Page> expression={expression}
categories={categories}
/>
); );
};
// Fallback for expression not found interface ExpressionCardPracticeViewProps {
const expression_candidates = getExpressionsInSet({ expression: Expression;
expression_set_id: expression_set.id, categories: Category[];
expression_to_expression_set,
expressions,
});
if (expression_candidates.length === 0) {
return (
<Page>
<p className="text-details">No expressions left in this set</p>
</Page>
);
} }
// Choose word and find categories // Handle internal state here
// Find categories function ExpressionCardPracticeView({
const expression = sample(expression_candidates); expression,
const expression_categories = getCategoriesInExpression({
categories, categories,
expression_id: expression.id, }: ExpressionCardPracticeViewProps) {
expression_to_category, const { query, pathname } = useRouter();
}); const [revealed, setRevealed] = useState(false);
return ( return (
<Page> <Page>
<div className="page-with-bottom-navigation"> <div className="page-with-bottom-navigation">
<section className="padding-small scroll"> <section className="padding-small scroll">
<ExpressionCard <ExpressionCard
prompt={expression.prompt} prompt={expression.prompt}
categories={expression_categories.map((category) => category.name)} categories={categories.map((category) => category.name)}
description={expression.description} description={expression.description}
show_description show_description={revealed}
/> />
</section> </section>
<section className="navigation-bottom"> <section className="navigation-bottom">
{revealed ? (
<>
<Link href={{ pathname, query }} passHref> <Link href={{ pathname, query }} passHref>
<a className="navigation-item-bottom text-navigation grow"> <a className="navigation-item-bottom text-navigation grow">
<div>wrong</div> <div>Wrong</div>
</a> </a>
</Link> </Link>
<Link href={{ pathname, query }} passHref> <Link href={{ pathname, query }} passHref>
<a className="navigation-item-bottom text-navigation grow"> <a className="navigation-item-bottom text-navigation grow">
<div>right</div> <div>Right</div>
</a> </a>
</Link> </Link>
</>
) : (
<button
className="navigation-item-bottom text-navigation grow"
onClick={() => setRevealed(true)}
>
Reveal
</button>
)}
</section> </section>
</div> </div>
</Page> </Page>
); );
}; }
export default ExpressionSetListPage; export default ExpressionPracticePage;

View File

@ -93,6 +93,14 @@
height: 64px; height: 64px;
} }
.navigation-item:hover {
background-color: antiquewhite;
}
.navigation-item.active {
background-color: bisque;
}
.navigation-item-bottom { .navigation-item-bottom {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
@ -105,11 +113,11 @@
height: 80px; height: 80px;
} }
.navigation-item:hover { .navigation-item-bottom:hover {
background-color: antiquewhite; background-color: antiquewhite;
} }
.navigation-item.active { .navigation-item-bottom.active {
background-color: bisque; background-color: bisque;
} }

View File

@ -0,0 +1,15 @@
import { Page } from "../../components/Page";
export interface PageWithErrorProps {
message: string;
}
export function PageWithError({ message }: PageWithErrorProps) {
return (
<Page>
<div className="page-with-padding">
<p className="text-meta">{message}</p>
</div>
</Page>
);
}

View File

@ -0,0 +1 @@
export * from "./PageWithError";