Switch data management to Dexie, add expression promotion/demotion
This commit is contained in:
parent
92f6efa772
commit
ae30e8d4a8
@ -16,6 +16,8 @@
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
},
|
||||
"dependencies": {
|
||||
"dexie": "^3.2.2",
|
||||
"dexie-react-hooks": "^1.1.1",
|
||||
"next": "12.2.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
|
@ -1,12 +1,16 @@
|
||||
import { Category, Expression, ExpressionId } from "../model";
|
||||
import { useExpressionData } from "./useExpressionData";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { database } from "../model";
|
||||
|
||||
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));
|
||||
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]) || []
|
||||
);
|
||||
}
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { MockData } from "../mock";
|
||||
import {
|
||||
Category,
|
||||
Expression,
|
||||
ExpressionSet,
|
||||
ExpressionToCategory,
|
||||
ExpressionToExpressionSet,
|
||||
} from "../model";
|
||||
|
||||
export interface ExpressionData {
|
||||
categories: Category[];
|
||||
expressions: Expression[];
|
||||
expression_sets: ExpressionSet[];
|
||||
expression_to_category: ExpressionToCategory[];
|
||||
expression_to_expression_set: ExpressionToExpressionSet[];
|
||||
}
|
||||
|
||||
export function useExpressionData(): ExpressionData {
|
||||
const [state] = useState(MockData);
|
||||
|
||||
return {
|
||||
categories: state.categories,
|
||||
expressions: state.expressions,
|
||||
expression_sets: state.expression_sets,
|
||||
expression_to_category: state.expression_to_category,
|
||||
expression_to_expression_set: state.expression_to_expression_set,
|
||||
};
|
||||
}
|
7
src/hooks/useExpressionFilterQueryIds.ts
Normal file
7
src/hooks/useExpressionFilterQueryIds.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export function useExpressionFilterQueryIds(): number[] {
|
||||
const { query } = useRouter();
|
||||
const filter_ids = (query["filter-ids"] || "") as string;
|
||||
return filter_ids.split(" ").map((id) => Number.parseInt(id));
|
||||
}
|
9
src/hooks/useExpressionSet.ts
Normal file
9
src/hooks/useExpressionSet.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { database } from "../model";
|
||||
|
||||
export function useExpressionSet(id: number) {
|
||||
return useLiveQuery(
|
||||
() => database.expression_sets.where({ id }).first(),
|
||||
[id]
|
||||
);
|
||||
}
|
6
src/hooks/useExpressionSetQueryId.ts
Normal file
6
src/hooks/useExpressionSetQueryId.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export function useExpressionSetQueryId(): number {
|
||||
const { query } = useRouter();
|
||||
return Number.parseInt(query["set-id"] as string) || 0;
|
||||
}
|
6
src/hooks/useExpressionSets.ts
Normal file
6
src/hooks/useExpressionSets.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { database, IndexedExpressionSet } from "../model";
|
||||
|
||||
export function useExpressionSets() {
|
||||
return useLiveQuery(() => database.expression_sets.toArray()) || [];
|
||||
}
|
@ -1,12 +1,21 @@
|
||||
import { Expression, ExpressionSetId } from "../model";
|
||||
import { useExpressionData } from "./useExpressionData";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { database } from "../model";
|
||||
|
||||
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));
|
||||
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]) || []
|
||||
);
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
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);
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
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);
|
||||
}
|
@ -56,25 +56,20 @@ export function parseRawData(
|
||||
const category_names = new Set(
|
||||
raw_expression_data.map((item) => item.category)
|
||||
);
|
||||
const categories: Category[] = Array.from(category_names).map(
|
||||
(name, index) => ({
|
||||
id: index + 1,
|
||||
name,
|
||||
description: name,
|
||||
})
|
||||
);
|
||||
const categories: Category[] = Array.from(category_names).map((name) => ({
|
||||
name,
|
||||
description: name,
|
||||
}));
|
||||
|
||||
const expressions: Expression[] = raw_expression_data.map(
|
||||
({ prompt, description }, index) => ({
|
||||
id: index + 1,
|
||||
({ prompt, description }) => ({
|
||||
prompt,
|
||||
description,
|
||||
})
|
||||
);
|
||||
|
||||
const expression_sets: ExpressionSet[] = raw_expression_set_data.map(
|
||||
({ name, description }, index) => ({
|
||||
id: index + 1,
|
||||
({ name, description }) => ({
|
||||
name,
|
||||
description,
|
||||
})
|
||||
@ -104,7 +99,7 @@ function matchExpressionAndCategory(
|
||||
{ category }: RawExpressionDataItem,
|
||||
categories: Category[]
|
||||
): ExpressionToCategory {
|
||||
const category_id = categories.find(({ name }) => name === category)?.id || 0;
|
||||
const category_id = categories.findIndex(({ name }) => name === category) + 1;
|
||||
return {
|
||||
category_id,
|
||||
expression_id,
|
||||
@ -117,7 +112,7 @@ function matchExpressionAndExpressionSet(
|
||||
expression_sets: ExpressionSet[]
|
||||
): ExpressionToExpressionSet {
|
||||
const expression_set_id =
|
||||
expression_sets.find(({ name }) => name === expression_set)?.id || 0;
|
||||
expression_sets.findIndex(({ name }) => name === expression_set) + 1;
|
||||
return {
|
||||
expression_id,
|
||||
expression_set_id,
|
||||
|
@ -1,7 +0,0 @@
|
||||
export type CategoryId = number;
|
||||
|
||||
export type Category = {
|
||||
id: CategoryId;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
export type ExpressionId = number;
|
||||
|
||||
export type Expression = {
|
||||
id: ExpressionId;
|
||||
prompt: string;
|
||||
description: string;
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
export type ExpressionSetId = number;
|
||||
|
||||
export type ExpressionSet = {
|
||||
id: ExpressionSetId;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
import { CategoryId } from "./Category";
|
||||
import { ExpressionId } from "./Expression";
|
||||
import { ExpressionSetId } from "./ExpressionSet";
|
||||
|
||||
export type ExpressionToCategory = {
|
||||
expression_id: ExpressionId;
|
||||
category_id: CategoryId;
|
||||
};
|
||||
|
||||
export type ExpressionToExpressionSet = {
|
||||
expression_id: ExpressionId;
|
||||
expression_set_id: ExpressionSetId;
|
||||
};
|
174
src/model/database.ts
Normal file
174
src/model/database.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import Dexie, { Table } from "dexie";
|
||||
import { MockData } from "../mock";
|
||||
import {
|
||||
Category,
|
||||
Expression,
|
||||
ExpressionSet,
|
||||
ExpressionToCategory,
|
||||
ExpressionToExpressionSet,
|
||||
} from "./types";
|
||||
|
||||
type WithId<T> = T & { id?: number };
|
||||
|
||||
export type IndexedExpression = WithId<Expression>;
|
||||
export type IndexedExpressionSet = WithId<ExpressionSet>;
|
||||
export type IndexedCategory = WithId<Category>;
|
||||
|
||||
class Database extends Dexie {
|
||||
expressions!: Table<IndexedExpression, number>;
|
||||
expression_sets!: Table<IndexedExpressionSet, number>;
|
||||
categories!: Table<IndexedCategory, number>;
|
||||
expression_to_expression_set!: Table<ExpressionToExpressionSet>;
|
||||
expression_to_category!: Table<ExpressionToCategory>;
|
||||
|
||||
constructor() {
|
||||
super("Database");
|
||||
this.version(1).stores({
|
||||
expressions: "++id, prompt, description",
|
||||
expression_sets: "++id, name, description",
|
||||
categories: "++id, name, description",
|
||||
expression_to_expression_set: "expression_id, expression_set_id",
|
||||
expression_to_category: "expression_id, category_id",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const database = new Database();
|
||||
database.on("populate", (transaction) => {
|
||||
const db = transaction as unknown as Database;
|
||||
const {
|
||||
expressions,
|
||||
expression_sets,
|
||||
categories,
|
||||
expression_to_expression_set,
|
||||
expression_to_category,
|
||||
} = MockData;
|
||||
db.expressions.bulkAdd(expressions);
|
||||
db.expression_sets.bulkAdd(expression_sets);
|
||||
db.categories.bulkAdd(categories);
|
||||
db.expression_to_expression_set.bulkAdd(expression_to_expression_set);
|
||||
db.expression_to_category.bulkAdd(expression_to_category);
|
||||
});
|
||||
|
||||
//
|
||||
// Trivial table operations
|
||||
//
|
||||
|
||||
export async function addExpression(expression: Expression) {
|
||||
return await database.expressions.add(expression);
|
||||
}
|
||||
|
||||
export async function addExpressionSet(expression_set: ExpressionSet) {
|
||||
return await database.expression_sets.add(expression_set);
|
||||
}
|
||||
|
||||
export async function addCategory(category: Category) {
|
||||
return await database.categories.add(category);
|
||||
}
|
||||
|
||||
//
|
||||
// Deletion operations
|
||||
//
|
||||
|
||||
export async function removeExpression(expression_id: number) {
|
||||
return await database.transaction(
|
||||
"rw",
|
||||
database.expression_sets,
|
||||
database.expression_to_category,
|
||||
database.expression_to_expression_set,
|
||||
() => {
|
||||
database.expression_sets.where({ id: expression_id }).delete();
|
||||
database.expression_to_category.where({ expression_id }).delete();
|
||||
database.expression_to_expression_set.where({ expression_id }).delete();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function removeCategory(category_id: number) {
|
||||
return await database.transaction(
|
||||
"rw",
|
||||
database.categories,
|
||||
database.expression_to_category,
|
||||
() => {
|
||||
database.categories.where({ id: category_id }).delete();
|
||||
database.expression_to_category.where({ category_id }).delete();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Relationship operations
|
||||
//
|
||||
|
||||
// (re)assigns an Expression to an expression set
|
||||
export async function assignExpressionToSet({
|
||||
expression_id,
|
||||
expression_set_id,
|
||||
}: ExpressionToExpressionSet) {
|
||||
return await database.transaction(
|
||||
"rw",
|
||||
database.expression_to_expression_set,
|
||||
() => {
|
||||
database.expression_to_expression_set
|
||||
.where("expression_id")
|
||||
.equals(expression_id)
|
||||
.delete();
|
||||
database.expression_to_expression_set.add({
|
||||
expression_id,
|
||||
expression_set_id,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// creates an expression-category relationship, prevents duplication
|
||||
export async function assignCategoryToExpression(
|
||||
relationship: ExpressionToCategory
|
||||
) {
|
||||
const existing = await database.expression_to_category
|
||||
.where(relationship)
|
||||
.first();
|
||||
if (existing) return existing;
|
||||
|
||||
return await database.expression_to_category.add(relationship);
|
||||
}
|
||||
|
||||
export async function unassignCategoryToExpression(
|
||||
relationship: ExpressionToCategory
|
||||
) {
|
||||
return await database.expression_to_category.where(relationship).delete();
|
||||
}
|
||||
|
||||
//
|
||||
// Complex utility function to add expression with its relationships function
|
||||
//
|
||||
|
||||
export interface addExpressionWithRelationshipsParams {
|
||||
expression: Expression;
|
||||
expression_set_id: number;
|
||||
category_ids: number[];
|
||||
}
|
||||
|
||||
export async function addExpressionWithRelationships({
|
||||
expression,
|
||||
expression_set_id,
|
||||
category_ids,
|
||||
}: addExpressionWithRelationshipsParams) {
|
||||
return await database.transaction(
|
||||
"rw",
|
||||
database.expressions,
|
||||
database.categories,
|
||||
database.expression_to_expression_set,
|
||||
() => {
|
||||
database.expressions.add(expression).then((expression_id) => {
|
||||
database.expression_to_expression_set.add({
|
||||
expression_id,
|
||||
expression_set_id,
|
||||
});
|
||||
database.expression_to_category.bulkAdd(
|
||||
category_ids.map((category_id) => ({ expression_id, category_id }))
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
@ -1,4 +1,2 @@
|
||||
export * from "./Category";
|
||||
export * from "./Expression";
|
||||
export * from "./ExpressionSet";
|
||||
export * from "./Relationships";
|
||||
export * from "./types";
|
||||
export * from "./database";
|
||||
|
24
src/model/types.ts
Normal file
24
src/model/types.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export type Category = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type Expression = {
|
||||
prompt: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type ExpressionSet = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type ExpressionToCategory = {
|
||||
expression_id: number;
|
||||
category_id: number;
|
||||
};
|
||||
|
||||
export type ExpressionToExpressionSet = {
|
||||
expression_id: number;
|
||||
expression_set_id: number;
|
||||
};
|
@ -2,8 +2,6 @@ import "../styles/globals.css";
|
||||
import "../styles/components.css";
|
||||
import type { AppProps } from "next/app";
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
|
@ -1,14 +1,17 @@
|
||||
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 { useQueriedExpressionSet } from "../../hooks/useQueriedExpressionSet";
|
||||
import { PageWithError } from "../../views/PageWithError/PageWithError";
|
||||
|
||||
const ExpressionSetDetailsPage: NextPage = () => {
|
||||
const expression_set = useQueriedExpressionSet();
|
||||
const expressions = useExpressionsInSet(expression_set?.id);
|
||||
const expression_set_id = useExpressionSetQueryId();
|
||||
const expression_set = useExpressionSet(expression_set_id);
|
||||
const expressions = useExpressionsInSet(expression_set_id);
|
||||
|
||||
// Fallback for expression set not found
|
||||
if (!expression_set) {
|
||||
@ -21,7 +24,7 @@ const ExpressionSetDetailsPage: NextPage = () => {
|
||||
<Page>
|
||||
<div className="page-with-padding scroll">
|
||||
<ExpressionSetInfo
|
||||
id={expression_set.id}
|
||||
id={expression_set.id!}
|
||||
name={expression_set.name}
|
||||
description={expression_set.description}
|
||||
expression_count={expressions.length}
|
||||
@ -36,7 +39,7 @@ const ExpressionSetDetailsPage: NextPage = () => {
|
||||
<div className="page-with-bottom-navigation">
|
||||
<section className="padding-small scroll">
|
||||
<ExpressionSetInfo
|
||||
id={expression_set.id}
|
||||
id={expression_set.id!}
|
||||
name={expression_set.name}
|
||||
description={expression_set.description}
|
||||
expression_count={expressions.length}
|
||||
@ -60,4 +63,6 @@ const ExpressionSetDetailsPage: NextPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpressionSetDetailsPage;
|
||||
export default dynamic(() => Promise.resolve(ExpressionSetDetailsPage), {
|
||||
ssr: false,
|
||||
});
|
||||
|
@ -1,13 +1,19 @@
|
||||
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 { useExpressionData } from "../../hooks/useExpressionData";
|
||||
import { useExpressionSets } from "../../hooks/useExpressionSets";
|
||||
import { useExpressionsInSet } from "../../hooks/useExpressionsInSet";
|
||||
import { ExpressionSet } from "../../model";
|
||||
import { IndexedExpressionSet } from "../../model";
|
||||
import { PageWithError } from "../../views/PageWithError";
|
||||
|
||||
const ExpressionSetListPage: NextPage = () => {
|
||||
const { expression_sets } = useExpressionData();
|
||||
const expression_sets = useExpressionSets();
|
||||
|
||||
if (!expression_sets?.length) {
|
||||
return <PageWithError message="No expression sets found" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
@ -25,8 +31,8 @@ const ExpressionSetListPage: NextPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
function ExpressionSetLink({ id, description, name }: ExpressionSet) {
|
||||
const expressions = useExpressionsInSet(id);
|
||||
function ExpressionSetLink({ id, description, name }: IndexedExpressionSet) {
|
||||
const expressions = useExpressionsInSet(id!) || [];
|
||||
return (
|
||||
<Link
|
||||
href={{
|
||||
@ -46,4 +52,6 @@ function ExpressionSetLink({ id, description, name }: ExpressionSet) {
|
||||
);
|
||||
}
|
||||
|
||||
export default ExpressionSetListPage;
|
||||
export default dynamic(() => Promise.resolve(ExpressionSetListPage), {
|
||||
ssr: false,
|
||||
});
|
||||
|
@ -1,28 +1,47 @@
|
||||
import type { NextPage } from "next";
|
||||
import Link from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { ExpressionCard } from "../../components";
|
||||
import { Page } from "../../components/Page";
|
||||
import { useExpressionCategories } from "../../hooks/useExpressionCategories";
|
||||
import { useQueriedExpressionSet } from "../../hooks/useQueriedExpressionSet";
|
||||
import { useRandomExpressionInSet } from "../../hooks/useRandomExpressionInSet";
|
||||
import { Category, Expression } from "../../model";
|
||||
import { useExpressionFilterQueryIds } from "../../hooks/useExpressionFilterQueryIds";
|
||||
import { useExpressionSetQueryId } from "../../hooks/useExpressionSetQueryId";
|
||||
import { useExpressionsInSet } from "../../hooks/useExpressionsInSet";
|
||||
import {
|
||||
assignExpressionToSet,
|
||||
IndexedCategory,
|
||||
IndexedExpression,
|
||||
} from "../../model";
|
||||
import { sample } from "../../util/array-utils";
|
||||
import { PageWithError } from "../../views/PageWithError/PageWithError";
|
||||
|
||||
// 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);
|
||||
// Query info
|
||||
const expression_set_id = useExpressionSetQueryId();
|
||||
const filter_ids = useExpressionFilterQueryIds();
|
||||
|
||||
// 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 views for expression set content not found
|
||||
if (!expression_set) {
|
||||
if (!expression_set_id) {
|
||||
return <PageWithError message="Expression set not found" />;
|
||||
}
|
||||
if (!expression) {
|
||||
return <PageWithError message="No expressions left in this set" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExpressionCardPracticeView
|
||||
key={expression.id}
|
||||
@ -33,8 +52,8 @@ const ExpressionPracticePage: NextPage = () => {
|
||||
};
|
||||
|
||||
interface ExpressionCardPracticeViewProps {
|
||||
expression: Expression;
|
||||
categories: Category[];
|
||||
expression: IndexedExpression;
|
||||
categories: IndexedCategory[];
|
||||
}
|
||||
|
||||
// Handle internal state here
|
||||
@ -42,7 +61,6 @@ function ExpressionCardPracticeView({
|
||||
expression,
|
||||
categories,
|
||||
}: ExpressionCardPracticeViewProps) {
|
||||
const { query, pathname } = useRouter();
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
return (
|
||||
<Page>
|
||||
@ -58,16 +76,8 @@ function ExpressionCardPracticeView({
|
||||
<section className="navigation-bottom">
|
||||
{revealed ? (
|
||||
<>
|
||||
<Link href={{ pathname, query }} passHref>
|
||||
<a className="navigation-item bottom text-navigation grow">
|
||||
<div>Wrong</div>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={{ pathname, query }} passHref>
|
||||
<a className="navigation-item bottom text-navigation grow">
|
||||
<div>Right</div>
|
||||
</a>
|
||||
</Link>
|
||||
<DemoteExpressionButton expression_id={expression.id!} />
|
||||
<PromoteExpressionButton expression_id={expression.id!} />
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
@ -83,4 +93,63 @@ function ExpressionCardPracticeView({
|
||||
);
|
||||
}
|
||||
|
||||
export default ExpressionPracticePage;
|
||||
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 (
|
||||
<button
|
||||
className="navigation-item bottom text-navigation grow"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span>Right</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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, query, push]);
|
||||
return (
|
||||
<button
|
||||
className="navigation-item bottom text-navigation grow"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span>Wrong</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default dynamic(() => Promise.resolve(ExpressionPracticePage), {
|
||||
ssr: false,
|
||||
});
|
||||
|
13
src/util/clamp.ts
Normal file
13
src/util/clamp.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export function clamp({
|
||||
value,
|
||||
lower,
|
||||
upper,
|
||||
}: {
|
||||
value: number;
|
||||
lower: number;
|
||||
upper: number;
|
||||
}): number {
|
||||
if (value > upper) return upper;
|
||||
if (value < lower) return lower;
|
||||
return value;
|
||||
}
|
10
yarn.lock
10
yarn.lock
@ -4786,6 +4786,16 @@ detect-port@^1.3.0:
|
||||
address "^1.0.1"
|
||||
debug "^2.6.0"
|
||||
|
||||
dexie-react-hooks@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/dexie-react-hooks/-/dexie-react-hooks-1.1.1.tgz#ff405cc89e5d899ddbac5e40d593f83f9a74106a"
|
||||
integrity sha512-Cam5JP6PxHN564RvWEoe8cqLhosW0O4CAZ9XEVYeGHJBa6KEJlOpd9CUpV3kmU9dm2MrW97/lk7qkf1xpij7gA==
|
||||
|
||||
dexie@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.2.tgz#fa6f2a3c0d6ed0766f8d97a03720056f88fe0e01"
|
||||
integrity sha512-q5dC3HPmir2DERlX+toCBbHQXW5MsyrFqPFcovkH9N2S/UW/H3H5AWAB6iEOExeraAu+j+zRDG+zg/D7YhH0qg==
|
||||
|
||||
diffie-hellman@^5.0.0:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||
|
Loading…
Reference in New Issue
Block a user