Separate error views, minor refactors, stub interaction on practice page
This commit is contained in:
parent
72249d9105
commit
6f600cf411
@ -19,5 +19,5 @@ export const Daily = Template.bind({});
|
||||
Daily.args = {
|
||||
name: "Daily",
|
||||
description: "Words for daily practice",
|
||||
word_count: 11,
|
||||
expression_count: 11,
|
||||
};
|
||||
|
@ -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 (
|
||||
<article className="content-card">
|
||||
<h2 className="content-row text-title margin-small">{name}</h2>
|
||||
<span className="content-row text-meta margin-small">
|
||||
{word_count} word(s)
|
||||
{expression_count} expressions(s)
|
||||
</span>
|
||||
<p className="content-row text-details margin-paragraph">{description}</p>
|
||||
</article>
|
||||
|
@ -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 (
|
||||
<section>
|
||||
@ -38,7 +38,7 @@ export function ExpressionSetInfo({
|
||||
</Link>
|
||||
</h2>
|
||||
<span className="content-row text-meta margin-small">
|
||||
{word_count} word(s)
|
||||
{expression_count} expression(s)
|
||||
</span>
|
||||
<p className="content-row text-details margin-paragraph">{description}</p>
|
||||
</section>
|
||||
|
12
src/hooks/useExpressionCategories.ts
Normal file
12
src/hooks/useExpressionCategories.ts
Normal 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));
|
||||
}
|
12
src/hooks/useExpressionsInSet.ts
Normal file
12
src/hooks/useExpressionsInSet.ts
Normal 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));
|
||||
}
|
14
src/hooks/useQueriedExpressionSet.ts
Normal file
14
src/hooks/useQueriedExpressionSet.ts
Normal 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);
|
||||
}
|
11
src/hooks/useRandomExpressionInSet.ts
Normal file
11
src/hooks/useRandomExpressionInSet.ts
Normal 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);
|
||||
}
|
@ -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 (
|
||||
<Page>
|
||||
<div className="page-with-padding scroll">
|
||||
<p className="text-details">Expression set not found</p>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
if (!expression_set) {
|
||||
return <PageWithError message="Expression set not found" />;
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<Page>
|
||||
<div className="page-with-padding scroll">
|
||||
@ -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}
|
||||
/>
|
||||
<p className="text-details">No expressions left in this set.</p>
|
||||
</div>
|
||||
@ -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}
|
||||
/>
|
||||
</section>
|
||||
<section className="navigation-bottom">
|
||||
|
@ -3,16 +3,32 @@ 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 (
|
||||
<Page>
|
||||
<div className="page-with-padding content-list scroll">
|
||||
{expression_sets.map(({ id, name, description }) => (
|
||||
<Link
|
||||
<ExpressionSetLink
|
||||
key={id}
|
||||
id={id}
|
||||
name={name}
|
||||
description={description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
function ExpressionSetLink({ id, description, name }: ExpressionSet) {
|
||||
const expressions = useExpressionsInSet(id);
|
||||
return (
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/expression-sets/details",
|
||||
query: { "set-id": id },
|
||||
@ -23,18 +39,11 @@ const ExpressionSetListPage: NextPage = () => {
|
||||
<ExpressionSetCard
|
||||
name={name}
|
||||
description={description}
|
||||
word_count={
|
||||
expression_to_expression_set.filter(
|
||||
(item) => item.expression_set_id === id
|
||||
).length
|
||||
}
|
||||
expression_count={expressions.length}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ExpressionSetListPage;
|
||||
|
@ -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)
|
||||
// Fallback views for expression set content not found
|
||||
if (!expression_set) {
|
||||
return <PageWithError message="Expression set not found" />;
|
||||
}
|
||||
if (!expression) {
|
||||
return <PageWithError message="No expressions left in this set" />;
|
||||
}
|
||||
return (
|
||||
<Page>
|
||||
<p className="text-details">Expression set not found</p>
|
||||
</Page>
|
||||
<ExpressionCardPracticeView
|
||||
key={expression.id}
|
||||
expression={expression}
|
||||
categories={categories}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<Page>
|
||||
<p className="text-details">No expressions left in this set</p>
|
||||
</Page>
|
||||
);
|
||||
interface ExpressionCardPracticeViewProps {
|
||||
expression: Expression;
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
// Choose word and find categories
|
||||
// Find categories
|
||||
const expression = sample(expression_candidates);
|
||||
const expression_categories = getCategoriesInExpression({
|
||||
// Handle internal state here
|
||||
function ExpressionCardPracticeView({
|
||||
expression,
|
||||
categories,
|
||||
expression_id: expression.id,
|
||||
expression_to_category,
|
||||
});
|
||||
|
||||
}: ExpressionCardPracticeViewProps) {
|
||||
const { query, pathname } = useRouter();
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
return (
|
||||
<Page>
|
||||
<div className="page-with-bottom-navigation">
|
||||
<section className="padding-small scroll">
|
||||
<ExpressionCard
|
||||
prompt={expression.prompt}
|
||||
categories={expression_categories.map((category) => category.name)}
|
||||
categories={categories.map((category) => category.name)}
|
||||
description={expression.description}
|
||||
show_description
|
||||
show_description={revealed}
|
||||
/>
|
||||
</section>
|
||||
<section className="navigation-bottom">
|
||||
{revealed ? (
|
||||
<>
|
||||
<Link href={{ pathname, query }} passHref>
|
||||
<a className="navigation-item-bottom text-navigation grow">
|
||||
<div>wrong</div>
|
||||
<div>Wrong</div>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={{ pathname, query }} passHref>
|
||||
<a className="navigation-item-bottom text-navigation grow">
|
||||
<div>right</div>
|
||||
<div>Right</div>
|
||||
</a>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="navigation-item-bottom text-navigation grow"
|
||||
onClick={() => setRevealed(true)}
|
||||
>
|
||||
Reveal
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ExpressionSetListPage;
|
||||
export default ExpressionPracticePage;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
15
src/views/PageWithError/PageWithError.tsx
Normal file
15
src/views/PageWithError/PageWithError.tsx
Normal 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>
|
||||
);
|
||||
}
|
1
src/views/PageWithError/index.ts
Normal file
1
src/views/PageWithError/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./PageWithError";
|
Loading…
Reference in New Issue
Block a user