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"
|
"**/*": "prettier --write --ignore-unknown"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dexie": "^3.2.2",
|
||||||
|
"dexie-react-hooks": "^1.1.1",
|
||||||
"next": "12.2.0",
|
"next": "12.2.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0"
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import { Category, Expression, ExpressionId } from "../model";
|
import { useLiveQuery } from "dexie-react-hooks";
|
||||||
import { useExpressionData } from "./useExpressionData";
|
import { database } from "../model";
|
||||||
|
|
||||||
export function useExpressionCategories(
|
export function useExpressionCategories(expression_id: number) {
|
||||||
expression_id?: ExpressionId | undefined
|
return (
|
||||||
): Category[] {
|
useLiveQuery(() => {
|
||||||
const { categories, expression_to_category } = useExpressionData();
|
return database.expression_to_category
|
||||||
const category_ids = expression_to_category
|
.where({ expression_id })
|
||||||
.filter((item) => item.expression_id === expression_id)
|
.toArray()
|
||||||
.map((item) => item.category_id);
|
.then((relationships) => {
|
||||||
return categories.filter((item) => category_ids.includes(item.id));
|
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 { useLiveQuery } from "dexie-react-hooks";
|
||||||
import { useExpressionData } from "./useExpressionData";
|
import { database } from "../model";
|
||||||
|
|
||||||
export function useExpressionsInSet(
|
export function useExpressionsInSet(expression_set_id: number) {
|
||||||
expression_set_id: ExpressionSetId | undefined
|
return (
|
||||||
): Expression[] {
|
useLiveQuery(() => {
|
||||||
const { expressions, expression_to_expression_set } = useExpressionData();
|
return database.expression_to_expression_set
|
||||||
const expression_ids = expression_to_expression_set
|
.where({ expression_set_id })
|
||||||
.filter((item) => item.expression_set_id === expression_set_id)
|
.toArray()
|
||||||
.map((item) => item.expression_id);
|
.then((relationships) => {
|
||||||
return expressions.filter((item) => expression_ids.includes(item.id));
|
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(
|
const category_names = new Set(
|
||||||
raw_expression_data.map((item) => item.category)
|
raw_expression_data.map((item) => item.category)
|
||||||
);
|
);
|
||||||
const categories: Category[] = Array.from(category_names).map(
|
const categories: Category[] = Array.from(category_names).map((name) => ({
|
||||||
(name, index) => ({
|
|
||||||
id: index + 1,
|
|
||||||
name,
|
name,
|
||||||
description: name,
|
description: name,
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
const expressions: Expression[] = raw_expression_data.map(
|
const expressions: Expression[] = raw_expression_data.map(
|
||||||
({ prompt, description }, index) => ({
|
({ prompt, description }) => ({
|
||||||
id: index + 1,
|
|
||||||
prompt,
|
prompt,
|
||||||
description,
|
description,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const expression_sets: ExpressionSet[] = raw_expression_set_data.map(
|
const expression_sets: ExpressionSet[] = raw_expression_set_data.map(
|
||||||
({ name, description }, index) => ({
|
({ name, description }) => ({
|
||||||
id: index + 1,
|
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
})
|
})
|
||||||
@ -104,7 +99,7 @@ function matchExpressionAndCategory(
|
|||||||
{ category }: RawExpressionDataItem,
|
{ category }: RawExpressionDataItem,
|
||||||
categories: Category[]
|
categories: Category[]
|
||||||
): ExpressionToCategory {
|
): ExpressionToCategory {
|
||||||
const category_id = categories.find(({ name }) => name === category)?.id || 0;
|
const category_id = categories.findIndex(({ name }) => name === category) + 1;
|
||||||
return {
|
return {
|
||||||
category_id,
|
category_id,
|
||||||
expression_id,
|
expression_id,
|
||||||
@ -117,7 +112,7 @@ function matchExpressionAndExpressionSet(
|
|||||||
expression_sets: ExpressionSet[]
|
expression_sets: ExpressionSet[]
|
||||||
): ExpressionToExpressionSet {
|
): ExpressionToExpressionSet {
|
||||||
const expression_set_id =
|
const expression_set_id =
|
||||||
expression_sets.find(({ name }) => name === expression_set)?.id || 0;
|
expression_sets.findIndex(({ name }) => name === expression_set) + 1;
|
||||||
return {
|
return {
|
||||||
expression_id,
|
expression_id,
|
||||||
expression_set_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 "./types";
|
||||||
export * from "./Expression";
|
export * from "./database";
|
||||||
export * from "./ExpressionSet";
|
|
||||||
export * from "./Relationships";
|
|
||||||
|
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 "../styles/components.css";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return <Component {...pageProps} />;
|
return <Component {...pageProps} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyApp;
|
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ExpressionSetInfo } from "../../components/ExpressionSetInfo/ExpressionSetInfo";
|
import { ExpressionSetInfo } from "../../components/ExpressionSetInfo/ExpressionSetInfo";
|
||||||
import { Page } from "../../components/Page";
|
import { Page } from "../../components/Page";
|
||||||
|
import { useExpressionSet } from "../../hooks/useExpressionSet";
|
||||||
|
import { useExpressionSetQueryId } from "../../hooks/useExpressionSetQueryId";
|
||||||
import { useExpressionsInSet } from "../../hooks/useExpressionsInSet";
|
import { useExpressionsInSet } from "../../hooks/useExpressionsInSet";
|
||||||
import { useQueriedExpressionSet } from "../../hooks/useQueriedExpressionSet";
|
|
||||||
import { PageWithError } from "../../views/PageWithError/PageWithError";
|
import { PageWithError } from "../../views/PageWithError/PageWithError";
|
||||||
|
|
||||||
const ExpressionSetDetailsPage: NextPage = () => {
|
const ExpressionSetDetailsPage: NextPage = () => {
|
||||||
const expression_set = useQueriedExpressionSet();
|
const expression_set_id = useExpressionSetQueryId();
|
||||||
const expressions = useExpressionsInSet(expression_set?.id);
|
const expression_set = useExpressionSet(expression_set_id);
|
||||||
|
const expressions = useExpressionsInSet(expression_set_id);
|
||||||
|
|
||||||
// Fallback for expression set not found
|
// Fallback for expression set not found
|
||||||
if (!expression_set) {
|
if (!expression_set) {
|
||||||
@ -21,7 +24,7 @@ const ExpressionSetDetailsPage: NextPage = () => {
|
|||||||
<Page>
|
<Page>
|
||||||
<div className="page-with-padding scroll">
|
<div className="page-with-padding scroll">
|
||||||
<ExpressionSetInfo
|
<ExpressionSetInfo
|
||||||
id={expression_set.id}
|
id={expression_set.id!}
|
||||||
name={expression_set.name}
|
name={expression_set.name}
|
||||||
description={expression_set.description}
|
description={expression_set.description}
|
||||||
expression_count={expressions.length}
|
expression_count={expressions.length}
|
||||||
@ -36,7 +39,7 @@ const ExpressionSetDetailsPage: NextPage = () => {
|
|||||||
<div className="page-with-bottom-navigation">
|
<div className="page-with-bottom-navigation">
|
||||||
<section className="padding-small scroll">
|
<section className="padding-small scroll">
|
||||||
<ExpressionSetInfo
|
<ExpressionSetInfo
|
||||||
id={expression_set.id}
|
id={expression_set.id!}
|
||||||
name={expression_set.name}
|
name={expression_set.name}
|
||||||
description={expression_set.description}
|
description={expression_set.description}
|
||||||
expression_count={expressions.length}
|
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 type { NextPage } from "next";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
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 { useExpressionSets } from "../../hooks/useExpressionSets";
|
||||||
import { useExpressionsInSet } from "../../hooks/useExpressionsInSet";
|
import { useExpressionsInSet } from "../../hooks/useExpressionsInSet";
|
||||||
import { ExpressionSet } from "../../model";
|
import { IndexedExpressionSet } from "../../model";
|
||||||
|
import { PageWithError } from "../../views/PageWithError";
|
||||||
|
|
||||||
const ExpressionSetListPage: NextPage = () => {
|
const ExpressionSetListPage: NextPage = () => {
|
||||||
const { expression_sets } = useExpressionData();
|
const expression_sets = useExpressionSets();
|
||||||
|
|
||||||
|
if (!expression_sets?.length) {
|
||||||
|
return <PageWithError message="No expression sets found" />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@ -25,8 +31,8 @@ const ExpressionSetListPage: NextPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function ExpressionSetLink({ id, description, name }: ExpressionSet) {
|
function ExpressionSetLink({ id, description, name }: IndexedExpressionSet) {
|
||||||
const expressions = useExpressionsInSet(id);
|
const expressions = useExpressionsInSet(id!) || [];
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={{
|
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 type { NextPage } from "next";
|
||||||
import Link from "next/link";
|
import dynamic from "next/dynamic";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { ExpressionCard } from "../../components";
|
import { ExpressionCard } from "../../components";
|
||||||
import { Page } from "../../components/Page";
|
import { Page } from "../../components/Page";
|
||||||
import { useExpressionCategories } from "../../hooks/useExpressionCategories";
|
import { useExpressionCategories } from "../../hooks/useExpressionCategories";
|
||||||
import { useQueriedExpressionSet } from "../../hooks/useQueriedExpressionSet";
|
import { useExpressionFilterQueryIds } from "../../hooks/useExpressionFilterQueryIds";
|
||||||
import { useRandomExpressionInSet } from "../../hooks/useRandomExpressionInSet";
|
import { useExpressionSetQueryId } from "../../hooks/useExpressionSetQueryId";
|
||||||
import { Category, Expression } from "../../model";
|
import { useExpressionsInSet } from "../../hooks/useExpressionsInSet";
|
||||||
|
import {
|
||||||
|
assignExpressionToSet,
|
||||||
|
IndexedCategory,
|
||||||
|
IndexedExpression,
|
||||||
|
} from "../../model";
|
||||||
|
import { sample } from "../../util/array-utils";
|
||||||
import { PageWithError } from "../../views/PageWithError/PageWithError";
|
import { PageWithError } from "../../views/PageWithError/PageWithError";
|
||||||
|
|
||||||
// Do random selection here so we don't keep flipping states with interaction
|
// Do random selection here so we don't keep flipping states with interaction
|
||||||
const ExpressionPracticePage: NextPage = () => {
|
const ExpressionPracticePage: NextPage = () => {
|
||||||
const expression_set = useQueriedExpressionSet();
|
// Query info
|
||||||
const expression = useRandomExpressionInSet(expression_set?.id);
|
const expression_set_id = useExpressionSetQueryId();
|
||||||
const categories = useExpressionCategories(expression?.id);
|
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
|
// Fallback views for expression set content not found
|
||||||
if (!expression_set) {
|
if (!expression_set_id) {
|
||||||
return <PageWithError message="Expression set not found" />;
|
return <PageWithError message="Expression set not found" />;
|
||||||
}
|
}
|
||||||
if (!expression) {
|
if (!expression) {
|
||||||
return <PageWithError message="No expressions left in this set" />;
|
return <PageWithError message="No expressions left in this set" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpressionCardPracticeView
|
<ExpressionCardPracticeView
|
||||||
key={expression.id}
|
key={expression.id}
|
||||||
@ -33,8 +52,8 @@ const ExpressionPracticePage: NextPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface ExpressionCardPracticeViewProps {
|
interface ExpressionCardPracticeViewProps {
|
||||||
expression: Expression;
|
expression: IndexedExpression;
|
||||||
categories: Category[];
|
categories: IndexedCategory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle internal state here
|
// Handle internal state here
|
||||||
@ -42,7 +61,6 @@ function ExpressionCardPracticeView({
|
|||||||
expression,
|
expression,
|
||||||
categories,
|
categories,
|
||||||
}: ExpressionCardPracticeViewProps) {
|
}: ExpressionCardPracticeViewProps) {
|
||||||
const { query, pathname } = useRouter();
|
|
||||||
const [revealed, setRevealed] = useState(false);
|
const [revealed, setRevealed] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@ -58,16 +76,8 @@ function ExpressionCardPracticeView({
|
|||||||
<section className="navigation-bottom">
|
<section className="navigation-bottom">
|
||||||
{revealed ? (
|
{revealed ? (
|
||||||
<>
|
<>
|
||||||
<Link href={{ pathname, query }} passHref>
|
<DemoteExpressionButton expression_id={expression.id!} />
|
||||||
<a className="navigation-item bottom text-navigation grow">
|
<PromoteExpressionButton expression_id={expression.id!} />
|
||||||
<div>Wrong</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<Link href={{ pathname, query }} passHref>
|
|
||||||
<a className="navigation-item bottom text-navigation grow">
|
|
||||||
<div>Right</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<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"
|
address "^1.0.1"
|
||||||
debug "^2.6.0"
|
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:
|
diffie-hellman@^5.0.0:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||||
|
Loading…
Reference in New Issue
Block a user