From 4cd9a47dc37412860f3eec17b96fe6ff023b0b52 Mon Sep 17 00:00:00 2001 From: idylls Date: Sun, 7 May 2023 13:54:27 -0400 Subject: [PATCH] Preactify --- _build_frontend.ts | 7 +- backend/api.ts | 29 ++-- common/api.ts | 29 +++- common/lib/imm.ts | 25 +-- common/schema.ts | 14 +- common/utils.ts | 6 + deno.json | 7 + deno.lock | 14 ++ frontend/components/App/App.ts | 82 ++++++++++ frontend/components/App/actions.ts | 33 ---- frontend/components/App/state.ts | 47 ++++++ frontend/components/App/view.ts | 57 ------- frontend/components/Home/Home.ts | 24 +++ frontend/components/Home/actions.ts | 27 ---- frontend/components/Home/state.ts | 19 +++ frontend/components/Home/view.ts | 30 ---- frontend/components/SchemaEditor/Fields.ts | 8 +- .../SchemaEditor/Fields/FieldOptions.ts | 151 ++++++++++++++++++ .../components/SchemaEditor/Fields/Fields.ts | 78 +++++++++ .../components/SchemaEditor/SchemaEditor.ts | 63 ++++++++ frontend/components/SchemaEditor/actions.ts | 118 -------------- frontend/components/SchemaEditor/state.ts | 130 +++++++++++++++ frontend/components/SchemaEditor/view.ts | 71 -------- frontend/components/lib/Labeled.ts | 30 +--- frontend/components/lib/ShowHide.ts | 38 +---- frontend/components/lib/input/Checkbox.ts | 17 ++ frontend/components/lib/input/NumberInput.ts | 51 +++--- frontend/components/lib/input/Select.ts | 49 +++--- frontend/components/lib/input/TextInput.ts | 36 ++--- frontend/components/lib/input/Validated.ts | 52 ++---- frontend/fetch.ts | 37 +++++ frontend/main.ts | 20 ++- frontend/state/store.ts | 26 +-- frontend/style.scss | 16 +- frontend/utils.ts | 2 + 35 files changed, 869 insertions(+), 574 deletions(-) create mode 100644 frontend/components/App/App.ts delete mode 100644 frontend/components/App/actions.ts create mode 100644 frontend/components/App/state.ts delete mode 100644 frontend/components/App/view.ts create mode 100644 frontend/components/Home/Home.ts delete mode 100644 frontend/components/Home/actions.ts create mode 100644 frontend/components/Home/state.ts delete mode 100644 frontend/components/Home/view.ts create mode 100644 frontend/components/SchemaEditor/Fields/FieldOptions.ts create mode 100644 frontend/components/SchemaEditor/Fields/Fields.ts create mode 100644 frontend/components/SchemaEditor/SchemaEditor.ts delete mode 100644 frontend/components/SchemaEditor/actions.ts create mode 100644 frontend/components/SchemaEditor/state.ts delete mode 100644 frontend/components/SchemaEditor/view.ts create mode 100644 frontend/components/lib/input/Checkbox.ts create mode 100644 frontend/fetch.ts diff --git a/_build_frontend.ts b/_build_frontend.ts index c8b92e1..b0c1bd5 100644 --- a/_build_frontend.ts +++ b/_build_frontend.ts @@ -2,6 +2,7 @@ import { emptyDir } from "https://deno.land/std@0.162.0/fs/mod.ts"; import sass from "https://deno.land/x/denosass@1.0.6/mod.ts"; import { build, stop } from "https://deno.land/x/esbuild@v0.17.5/mod.js"; import { httpImports } from "https://deno.land/x/esbuild_plugin_http_imports@v1.3.0/index.ts"; +import * as importMap from "npm:esbuild-plugin-import-map"; await emptyDir("./build"); @@ -13,13 +14,17 @@ compiler.to_file({ format: "compressed", }); +const denoJson = JSON.parse(await Deno.readTextFile("./deno.json")); + +importMap.load(denoJson); + const res = await build({ entryPoints: ["./frontend/main.ts"], write: true, bundle: true, outfile: "./build/app.js", sourcemap: "inline", - plugins: [httpImports()], + plugins: [httpImports(), importMap.plugin()], }); console.debug(res); diff --git a/backend/api.ts b/backend/api.ts index aa32a8b..2749e80 100644 --- a/backend/api.ts +++ b/backend/api.ts @@ -6,21 +6,26 @@ const handlers: MakeHandlers = { listSchemas: async (limit) => [ { id: 0, - name: "Test", - fields: [ - { - kind: "string", - name: "test", - required: false, - options: { - displayAs: "input", - minLength: 0, - }, + latestVersion: { + version: 1, + schema: { + name: "Test", + fields: [ + { + name: "name", + options: { + displayAs: "input", + minLength: 0, + }, + kind: "string", + required: true, + }, + ], }, - ], + }, }, ], - saveSchema: async (schema) => ({ ...schema, id: 0 }), + saveSchema: async ({ schema, id }) => "todo", }, }; diff --git a/common/api.ts b/common/api.ts index 3f1f4b3..ab6343f 100644 --- a/common/api.ts +++ b/common/api.ts @@ -1,12 +1,35 @@ import { z } from "./deps/zod.ts"; import { ReplacerFn, ReviverFn, sig } from "./deps/yaypi.ts"; -import { MaybePersistedSchema, Schema } from "./schema.ts"; +import { Schema } from "./schema.ts"; export const Api = { v1: { - listSchemas: sig(z.number().nullable(), z.array(Schema)), - saveSchema: sig(MaybePersistedSchema, Schema), + listSchemas: sig( + z.number().nullable(), + z.array( + z.object({ + id: z.number(), + latestVersion: z.object({ + version: z.number(), + schema: Schema, + }), + }), + ), + ), + saveSchema: sig( + z.object({ + id: z.number().optional(), + schema: Schema, + }), + z.union([ + z.object({ + id: z.number(), + version: z.number(), + }), + z.string(), + ]), + ), }, }; diff --git a/common/lib/imm.ts b/common/lib/imm.ts index a34321c..8580a62 100644 --- a/common/lib/imm.ts +++ b/common/lib/imm.ts @@ -1,23 +1,24 @@ -import { Const } from "../utils.ts"; - -export const setIdx = ( - arr: Const, - index: number, - newValue: Const, -): Const => { - const newArr = [...arr] as Const[]; +export const setIdx = (arr: T[], index: number, newValue: T): T[] => { + const newArr = [...arr]; newArr[index] = newValue; return newArr; }; -export const push = (arr: Const, newValue: T) => [...arr, newValue]; +export const push = (arr: T[], newValue: T) => [...arr, newValue]; + +export const deleteIdx = (arr: T[], index: number): T[] => { + const newArr = [...arr]; + newArr.splice(index, 1); + + return newArr; +}; export const setProp = ( - obj: Const, + obj: T, key: K, - newValue: Const, -): Const => ({ + newValue: T[K], +): T => ({ ...obj, [key]: newValue, }); diff --git a/common/schema.ts b/common/schema.ts index 6a24f93..448de5f 100644 --- a/common/schema.ts +++ b/common/schema.ts @@ -1,6 +1,5 @@ import { z } from "./deps/zod.ts"; import { autoProxy, unproxy } from "./lib/autoproxy.ts"; -import { MaybePersisted } from "./persisted.ts"; import { Const } from "./utils.ts"; import { ValidationErrors } from "./validation.ts"; @@ -72,7 +71,6 @@ export const Field = z.union(makeZodFields()); export type Field = z.infer; const SchemaBase = { - id: z.number(), fields: z.array(Field), name: z.string(), }; @@ -91,6 +89,7 @@ export type Schema = RefineID, SchemaID>; export const MaybePersistedSchema = z.object({ ...SchemaBase, id: z.number().optional(), + version: z.number().optional(), }); export type MaybePersistedSchema = z.infer; @@ -127,14 +126,21 @@ export const validateSchema = ( const kind = field.kind; if (kind == "number") { const options = field.options; - if (options.max && options.min && options.min >= options.max) { + if ( + options.max != undefined && + options.min != undefined && + options.min >= options.max + ) { (errors.fields[i].options as any).min = [ "Min must be less than or equal to max", ]; } } else if (kind == "string") { const options = field.options; - if (options.maxLength && options.maxLength < options.minLength) { + if ( + options.maxLength != undefined && + options.maxLength < options.minLength + ) { (errors.fields[i].options as any).maxLength = [ "Max length must be greater than or equal to min", ]; diff --git a/common/utils.ts b/common/utils.ts index a580c33..ddd7eb5 100644 --- a/common/utils.ts +++ b/common/utils.ts @@ -27,3 +27,9 @@ export type ReturnTypes any)[]> = { [K in keyof T]: ReturnType; }[number]; type A = ReturnTypes<[() => number, () => string]>; + +export type UnionToIntersection = ( + U extends any ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never; diff --git a/deno.json b/deno.json index 2c63c08..6d141c6 100644 --- a/deno.json +++ b/deno.json @@ -1,2 +1,9 @@ { + "imports": { + "frontend": "./frontend", + "swel": "../swel/mod.ts", + "preact": "https://esm.sh/preact@10.13.2", + "preact/debug": "https://esm.sh/preact@10.13.2/debug", + "preact/hooks": "https://esm.sh/preact@10.13.2/hooks" + } } diff --git a/deno.lock b/deno.lock index 04b9f18..c9b5a07 100644 --- a/deno.lock +++ b/deno.lock @@ -106,7 +106,10 @@ "https://deno.land/x/zod@v3.21.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4", "https://deno.land/x/zod@v3.21.4/types.ts": "b5d061babea250de14fc63764df5b3afa24f2b088a1d797fc060ba49a0ddff28", "https://esm.sh/preact@10.13.2": "5ebf0838bbc3c32fc6e78a4cd9dd672f2c36386d0595815d721a6b0941278488", + "https://esm.sh/preact@10.13.2/debug": "5c2442ec06daa3bf686c41b3e4020b4ee23e046a4fcc034e319429942308855e", "https://esm.sh/preact@10.13.2/hooks": "884334b1560448cf16b4f14841fffdb8707615373a3c76c676a6f9e5c77e43b2", + "https://esm.sh/stable/preact@10.13.2/deno/debug.js": "1a982c30a858af0ab7f9893fc981bb14ad5cbeffef1a8c3f33c4fbe7d80109a3", + "https://esm.sh/stable/preact@10.13.2/deno/devtools.js": "dcda4e6c6f034d19f220e6b44e070e1dc81e841b0c03bb1048c8fde88935a5c8", "https://esm.sh/stable/preact@10.13.2/deno/hooks.js": "c7a8e703bcbc6a05949f329b618c33d5d1ea5fee113ddcea44ff0f527af8556f", "https://esm.sh/stable/preact@10.13.2/deno/preact.mjs": "365fab897381f4f403f859c5d12939084560545567108cc90dae901bbe892578", "https://esm.sh/v118/preact@10.13.2/hooks/src/index.d.ts": "5c29febb624fc25d71cb0e125848c9b711e233337a08f7eacfade38fd4c14cc3", @@ -116,5 +119,16 @@ "https://git.idylls.net/idylls/shirt/raw/tag/2022.07.27/deps/path.ts": "1181eaa5350db0c5461678e53ed796122bcc4a1e7fd43d49fcc50be80e2fff88", "https://git.idylls.net/idylls/shirt/raw/tag/2022.07.27/mod.ts": "0a9bd75a2d7982526df49d800224ee6bf06aee7cb16f9a8d217220c6bdbbbfdb", "https://git.idylls.net/idylls/yaypi/raw/tag/2022.07.27/mod.ts": "5ed6326b9f440e7a36266dbeb255d1f32d24a65d7fd2454e2d10355bd96e54c5" + }, + "npm": { + "specifiers": { + "esbuild-plugin-import-map": "esbuild-plugin-import-map@2.1.0" + }, + "packages": { + "esbuild-plugin-import-map@2.1.0": { + "integrity": "sha512-rlI9H8f1saIqYEUNHxDmIMGZZFroANyD6q3Aht6aXyOq/aOdO6jp5VFF1+n3o9AUe+wAtQcn93Wv1Vuj9na0hg==", + "dependencies": {} + } + } } } diff --git a/frontend/components/App/App.ts b/frontend/components/App/App.ts new file mode 100644 index 0000000..f4d0479 --- /dev/null +++ b/frontend/components/App/App.ts @@ -0,0 +1,82 @@ +import { ComponentChild, Fragment, VNode, createContext, h } from "preact"; +import { useContext, useEffect, useMemo, useReducer } from "preact/hooks"; +import { Home } from "../Home/Home.ts"; +import { Action, State, ViewState, ViewStateKind, reduce } from "./state.ts"; +import { Dispatch } from "../../utils.ts"; +import { saveStateToLocalStorage } from "../../state/store.ts"; +import { Const } from "../../../common/utils.ts"; +import { SchemaEditor } from "../SchemaEditor/SchemaEditor.ts"; +import { goHome } from "./state.ts"; +import { MakeClient, makeClient } from "../../../common/deps/yaypi.ts"; +import { Api } from "../../../common/api.ts"; + +type ViewStates = { + [K in ViewState["kind"]]: ViewState & { kind: K }; +}; + +type ViewRegistry_ = { + // @ts-ignore: weird + [K in keyof ViewStates]: ViewStates[K]["state"]; +}; + +type ViewRegistry = { + [K in keyof ViewRegistry_]: (p: { + state: ViewRegistry_[K]; + dispatch: Dispatch; + client: Client; + }) => VNode; +}; + +const VIEW_REGISTRY: ViewRegistry = { + Home: (p) => h(Home, p), + SchemaEditor: (p) => h(SchemaEditor, p), +}; + +const AppView = ( + p: ViewState & { dispatch: Dispatch; client: Client }, +) => { + const View = VIEW_REGISTRY[p.kind]; + + return h(View as any, p); +}; + +const VIEW_NAMES: Record = { + Home: "Home", + SchemaEditor: "Schema Editor", +}; + +const Layout = (p: { + state: State; + dispatch: Dispatch; + client: Client; +}) => { + const view = h(AppView, { + ...p.state.view, + dispatch: p.dispatch, + client: p.client, + }); + + const name = VIEW_NAMES[p.state.view.kind]; + + const onClickHome = () => { + p.dispatch(goHome()); + }; + + return h( + Fragment, + {}, + h("nav", {}, h("a", { onClick: onClickHome }, "Zchema"), h("a", {}, name)), + h("main", {}, view), + ); +}; + +export const App = (p: State) => { + const [appState, dispatch] = useReducer(reduce, p); + const client = useMemo(() => makeClient(Api, "/api"), []); + + useEffect(() => { + saveStateToLocalStorage(appState as Const); + }, [appState]); + + return h(Layout, { state: appState, dispatch, client }); +}; diff --git a/frontend/components/App/actions.ts b/frontend/components/App/actions.ts deleted file mode 100644 index 2e4cdbc..0000000 --- a/frontend/components/App/actions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { unreachable } from "../../../common/lib/dev.ts"; -import { Reduce } from "../../state/lib/store.ts"; -import { Action as HomeAction, reduce as reduceHome } from "../Home/actions.ts"; -import { - Action as SchemaEditorAction, - reduce as reduceSchemaEditor, -} from "../SchemaEditor/actions.ts"; -import { AppState } from "./view.ts"; - -export type Action = HomeAction | SchemaEditorAction; - -export const reduce: Reduce = (state, action) => { - if (state.view.kind == "Home") { - return { - view: reduceHome(state.view.state, action as HomeAction), - }; - } - - if (state.view.kind == "SchemaEditor") { - return { - ...state, - view: { - ...state.view, - state: reduceSchemaEditor( - state.view.state, - action as SchemaEditorAction, - ), - }, - }; - } - - return unreachable(); -}; diff --git a/frontend/components/App/state.ts b/frontend/components/App/state.ts new file mode 100644 index 0000000..4a13c8b --- /dev/null +++ b/frontend/components/App/state.ts @@ -0,0 +1,47 @@ +import { Reducer } from "preact/hooks"; +import { + Action as HomeAction, + State as HomeState, + reduce as reduceHome, +} from "../Home/state.ts"; +import { + Action as SchemaEditorAction, + State as SchemaEditorState, + reduce as reduceSchemaEditor, +} from "../SchemaEditor/state.ts"; +import { ReturnTypes } from "../../../common/utils.ts"; + +export const goHome = () => ({ kind: "goHome" }); +export type Action = + | HomeAction + | SchemaEditorAction + | ReturnTypes<[typeof goHome]>; +type ViewState_ = { kind: Kind; state: T }; + +export type ViewState = + | ViewState_<"Home", HomeState> + | ViewState_<"SchemaEditor", SchemaEditorState>; + +export type ViewStateKind = ViewState["kind"]; +export type State = { + view: ViewState; +}; + +export const reduce: Reducer = (state, action) => { + if (action.kind == "goHome") { + return { + ...state, + view: { kind: "Home", state: null }, + }; + } else if (state.view.kind === "Home") { + return { + ...state, + view: reduceHome(state.view, action as HomeAction), + }; + } else { + return { + ...state, + view: reduceSchemaEditor(state.view, action as SchemaEditorAction), + }; + } +}; diff --git a/frontend/components/App/view.ts b/frontend/components/App/view.ts deleted file mode 100644 index 6e8b868..0000000 --- a/frontend/components/App/view.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Const } from "../../../common/utils.ts"; -import { swel } from "../../deps/swel.ts"; -import { Dispatch } from "../../state/lib/store.ts"; -import { Home, State as HomeState } from "../Home/view.ts"; -import { - SchemaEditor, - State as SchemaEditorState, -} from "../SchemaEditor/view.ts"; -import { CmpPaintComponent, Paint } from "../component.ts"; -import { Action } from "./actions.ts"; - -type ViewState_ = { kind: Kind; state: T }; - -export type ViewState = - | ViewState_<"Home", HomeState> - | ViewState_<"SchemaEditor", SchemaEditorState>; -export type ViewStateKind = ViewState["kind"]; - -export type AppState = { - view: ViewState; -}; - -type ViewRegistry_ = { - [K in ViewStateKind]: ViewState & { kind: K }; -}; -type ViewRegistry = { - // @ts-ignore: Weird - [K in keyof ViewRegistry_]: CmpPaintComponent; -}; - -export class App extends CmpPaintComponent { - private viewRegistry: ViewRegistry; - - constructor(private dispatch: Dispatch) { - super(); - - this.viewRegistry = { - SchemaEditor: new SchemaEditor(this.dispatch), - Home: new Home(this.dispatch), - }; - } - - private container = swel("div", { className: "app" }); - - get el() { - return this.container; - } - - paint_: Paint = (cur) => { - const view = this.viewRegistry[cur.view.kind]; - if (!this.previousModel || cur.view.kind != this.previousModel.view.kind) { - this.container.replaceChildren(view.el); - } - - view.paint(cur.view.state as any as never); - }; -} diff --git a/frontend/components/Home/Home.ts b/frontend/components/Home/Home.ts new file mode 100644 index 0000000..fc2759c --- /dev/null +++ b/frontend/components/Home/Home.ts @@ -0,0 +1,24 @@ +import { h } from "preact"; +import { Dispatch } from "../../utils.ts"; +import { Action } from "./state.ts"; +import { Client } from "../../fetch.ts"; + +export const Home = (p: { dispatch: Dispatch; client: Client }) => { + const button = h( + "button", + { + onClick: () => p.dispatch({ kind: "editNewSchema" }), + }, + "New schema", + ); + + const b = h( + "button", + { + onClick: () => p.dispatch({ kind: "hello" }), + }, + "Say hello", + ); + + return h("div", {}, button, b); +}; diff --git a/frontend/components/Home/actions.ts b/frontend/components/Home/actions.ts deleted file mode 100644 index ec205af..0000000 --- a/frontend/components/Home/actions.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { unreachable } from "../../../common/lib/dev.ts"; -import { Const, ReturnTypes } from "../../../common/utils.ts"; -import { ViewState } from "../App/view.ts"; -import { State } from "./view.ts"; - -export const newSchema = () => ({ kind: "newSchema" as const }); -export type Action = ReturnTypes<[typeof newSchema]>; - -export const reduce = ( - state: Const, - action: Action, -): Const => { - if (action.kind == "newSchema") { - return { - kind: "SchemaEditor", - state: { - schema: { - fields: [], - name: "New schema", - }, - errors: {}, - }, - }; - } - - return unreachable(); -}; diff --git a/frontend/components/Home/state.ts b/frontend/components/Home/state.ts new file mode 100644 index 0000000..81ae908 --- /dev/null +++ b/frontend/components/Home/state.ts @@ -0,0 +1,19 @@ +import { Reducer } from "preact/hooks"; +import { ViewState } from "../App/state.ts"; + +export type State = null; +export type Action = { kind: "editNewSchema" } | { kind: "hello" }; + +export const reduce = (state: ViewState, action: Action): ViewState => { + if (action.kind == "hello") { + return state; + } + + return { + kind: "SchemaEditor", + state: { + fields: [], + name: "New schema", + }, + }; +}; diff --git a/frontend/components/Home/view.ts b/frontend/components/Home/view.ts deleted file mode 100644 index 63bd4f3..0000000 --- a/frontend/components/Home/view.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { swel } from "../../deps/swel.ts"; -import { Dispatch } from "../../state/lib/store.ts"; -import { CmpPaintComponent, Paint } from "../component.ts"; -import { Action, newSchema } from "./actions.ts"; - -export type State = undefined; - -export class Home extends CmpPaintComponent { - private newSchemaButton = swel( - "button", - { - on: { click: () => this.dispatch(newSchema()) }, - }, - "New schema", - ); - - private container = swel("div", { className: "home" }, [ - this.newSchemaButton, - ]); - - constructor(private dispatch: Dispatch) { - super(); - } - - get el() { - return this.container; - } - - paint_: Paint = (cur) => {}; -} diff --git a/frontend/components/SchemaEditor/Fields.ts b/frontend/components/SchemaEditor/Fields.ts index 491eb90..b427a9f 100644 --- a/frontend/components/SchemaEditor/Fields.ts +++ b/frontend/components/SchemaEditor/Fields.ts @@ -74,7 +74,7 @@ class NumberFieldOptions extends CmpPaintComponent< this.minInput = new Validated( new ToggleField( new NumberInput({ - onChange: (n) => { + onInput: (n) => { dispatch(setFieldOptionProp(fieldIndex, "min", n)); }, }), @@ -90,7 +90,7 @@ class NumberFieldOptions extends CmpPaintComponent< this.maxInput = new Validated( new ToggleField( new NumberInput({ - onChange: (n) => { + onInput: (n) => { dispatch(setFieldOptionProp(fieldIndex, "max", n)); }, }), @@ -163,7 +163,7 @@ class StringFieldOptions extends CmpPaintComponent< new Labeled( "Min length", new NumberInput({ - onChange: (v) => + onInput: (v) => dispatch(setFieldOptionProp(fieldIndex, "minLength", v)), }), ), @@ -173,7 +173,7 @@ class StringFieldOptions extends CmpPaintComponent< this.maxLength = new Validated( new ToggleField( new NumberInput({ - onChange: (n) => + onInput: (n) => dispatch(setFieldOptionProp(fieldIndex, "maxLength", n)), }), { diff --git a/frontend/components/SchemaEditor/Fields/FieldOptions.ts b/frontend/components/SchemaEditor/Fields/FieldOptions.ts new file mode 100644 index 0000000..7e19ba0 --- /dev/null +++ b/frontend/components/SchemaEditor/Fields/FieldOptions.ts @@ -0,0 +1,151 @@ +import { VNode, h } from "preact"; +import { + FieldKind, + FieldOptions as FieldOptionsModels, +} from "../../../../common/schema.ts"; +import { ValidationErrors } from "../../../../common/validation.ts"; +import { Select } from "../../lib/input/Select.ts"; +import { Labeled } from "../../lib/Labeled.ts"; +import { NumberInput } from "../../lib/input/NumberInput.ts"; +import { Callback } from "../../../utils.ts"; +import { Checkbox } from "../../lib/input/Checkbox.ts"; +import { ShowHide } from "../../lib/ShowHide.ts"; +import { Validated } from "../../lib/input/Validated.ts"; + +const ToggleField = (p: { + label: string; + onToggle: Callback<[boolean]>; + errors?: string[]; + show: boolean; + inner: () => VNode; +}) => { + return h( + "div", + { class: "toggle-field" }, + h( + Validated, + { errors: p.errors }, + h( + Labeled, + { label: p.label }, + h(Checkbox, { onToggle: p.onToggle, checked: p.show }), + ), + h(ShowHide, { show: p.show, inner: p.inner }), + ), + ); +}; + +export type SetFieldOption = < + KK extends keyof FieldOptionsModels[K], +>( + optionProp: KK, + value: FieldOptionsModels[K][KK], +) => void; + +type Props = { + options: FieldOptionsModels[K]; + errors?: ValidationErrors; + setFieldOption: SetFieldOption; +}; + +const StringFieldOptions = (p: Props<"string">) => { + type DisplayAs = FieldOptionsModels["string"]["displayAs"]; + + const displayAs = h( + Labeled, + { label: "Display as" }, + h(Select, { + value: p.options.displayAs, + options: { + input: "input", + textarea: "textarea", + }, + onChange: (v) => p.setFieldOption("displayAs", v), + }), + ); + + const minLength = h( + Validated, + { errors: p.errors?.minLength }, + h( + Labeled, + { label: "Min length" }, + h(NumberInput, { + max: p.options.maxLength, + value: p.options.minLength, + onInput: (v) => p.setFieldOption("minLength", v), + }), + ), + ); + + const maxLength = h(ToggleField, { + label: "Max length?", + errors: p.errors?.maxLength, + show: p.options.maxLength != undefined, + onToggle: (active) => p.setFieldOption("maxLength", active ? 0 : undefined), + inner: () => { + return h(NumberInput, { + value: p.options.maxLength ?? 0, + min: p.options.minLength, + onInput: (v) => p.setFieldOption("maxLength", v), + }); + }, + }); + + return h( + "div", + { class: "field-options string" }, + displayAs, + minLength, + maxLength, + ); +}; +const NumberFieldOptions = (p: Props<"number">) => { + const min = h(ToggleField, { + errors: p.errors?.min, + label: "Min?", + show: p.options.min != undefined, + onToggle: (b) => p.setFieldOption("min", b ? 0 : undefined), + inner: () => + h(NumberInput, { + max: p.options.max, + value: p.options.min ?? 0, + onInput: (v) => p.setFieldOption("min", v), + }), + }); + + const max = h(ToggleField, { + errors: p.errors?.max, + label: "Max?", + show: p.options.max != undefined, + onToggle: (b) => p.setFieldOption("max", b ? 0 : undefined), + inner: () => + h(NumberInput, { + min: p.options.min, + value: p.options.max ?? 0, + onInput: (v) => p.setFieldOption("max", v), + }), + }); + + return h("div", { class: "field-options number" }, min, max); +}; + +type FieldOptionComponentRegistry = { + [K in FieldKind]: (p: Props) => VNode; +}; + +const FIELD_OPTION_COMPONENT_REGISTRY: FieldOptionComponentRegistry = { + number: NumberFieldOptions, + string: StringFieldOptions, +}; + +export const FieldOptions = (p: { + kind: FieldKind; + options: FieldOptionsModels[FieldKind]; + errors?: ValidationErrors; + setFieldOption: SetFieldOption; +}) => { + const Component = FIELD_OPTION_COMPONENT_REGISTRY[p.kind]; + + return h(Component as any, p); +}; diff --git a/frontend/components/SchemaEditor/Fields/Fields.ts b/frontend/components/SchemaEditor/Fields/Fields.ts new file mode 100644 index 0000000..351de9c --- /dev/null +++ b/frontend/components/SchemaEditor/Fields/Fields.ts @@ -0,0 +1,78 @@ +import { h } from "preact"; +import { FieldKind, Field as FieldModel } from "../../../../common/schema.ts"; +import { ValidationErrors } from "../../../../common/validation.ts"; +import { Callback, Dispatch } from "../../../utils.ts"; +import { Action, deleteField, setFieldOption, setFieldProp } from "../state.ts"; +import { Validated } from "../../lib/input/Validated.ts"; +import { TextInput } from "../../lib/input/TextInput.ts"; +import { Select } from "../../lib/input/Select.ts"; +import { Labeled } from "../../lib/Labeled.ts"; +import { FieldOptions, SetFieldOption } from "./FieldOptions.ts"; + +const Field = (p: { + field: FieldModel; + errors?: ValidationErrors; + setName: Callback<[string]>; + setKind: Callback<[FieldKind]>; + setFieldOption: SetFieldOption; + delete: Callback<[]>; +}) => { + const nameInput = h( + Validated, + { errors: p.errors?.name }, + h(TextInput, { + value: p.field.name, + placeholder: "Field name", + onInput: p.setName, + }), + ); + + const kindSelect = h( + Labeled, + { label: "Kind" }, + h(Select, { + options: { string: "string", number: "number" }, + value: p.field.kind, + onChange: p.setKind, + }), + ); + + const options = h(FieldOptions, { + kind: p.field.kind, + options: p.field.options, + errors: p.errors?.options, + setFieldOption: p.setFieldOption, + }); + + const deleteBtn = h("button", { onClick: p.delete }, "Delete"); + + return h( + "div", + { class: "field" }, + nameInput, + kindSelect, + options, + deleteBtn, + ); +}; + +export const Fields = (p: { + fields: FieldModel[]; + errors?: ValidationErrors; + dispatch: Dispatch; +}) => { + const fields = p.fields.map((f, i) => { + const errors = p.errors?.[i]; + + return h(Field, { + field: f, + errors, + setName: (s) => p.dispatch(setFieldProp(i, "name", s)), + setKind: (s) => p.dispatch(setFieldProp(i, "kind", s)), + setFieldOption: (pr, v) => p.dispatch(setFieldOption(i, pr, v)), + delete: () => p.dispatch(deleteField(i)), + }); + }); + + return h("div", { class: "fields" }, ...fields); +}; diff --git a/frontend/components/SchemaEditor/SchemaEditor.ts b/frontend/components/SchemaEditor/SchemaEditor.ts new file mode 100644 index 0000000..eeeb0bd --- /dev/null +++ b/frontend/components/SchemaEditor/SchemaEditor.ts @@ -0,0 +1,63 @@ +import { h } from "preact"; +import { Dispatch } from "../../utils.ts"; +import { State, Action, setName, addField } from "./state.ts"; +import { Action as AppAction } from "../App/state.ts"; +import { TextInput } from "../lib/input/TextInput.ts"; +import { Validated } from "../lib/input/Validated.ts"; +import { validateSchema } from "../../../common/schema.ts"; +import { Fields } from "./Fields/Fields.ts"; +import { Client } from "../App/App.ts"; +import { assert } from "../../../common/deps/yaypi.ts"; +import { goHome } from "../App/state.ts"; + +export const SchemaEditor = (p: { + state: State; + dispatch: Dispatch; + client: Client; +}) => { + const errors = validateSchema(p.state); + + const nameInput = h( + Validated, + { + errors: errors.name, + }, + h(TextInput, { + placeholder: "Schema name", + value: p.state.name, + onInput: (s) => p.dispatch(setName(s)), + class: "name", + }), + ); + + const fields = h(Fields, { + fields: p.state.fields, + dispatch: p.dispatch, + errors: errors.fields, + }); + + const addFieldBtn = h( + "button", + { onClick: () => p.dispatch(addField()) }, + "Add field", + ); + + const save = async () => { + const result = assert(await p.client.v1.saveSchema(p.state)); + if (typeof result === "string") { + console.error(result); + } + + p.dispatch(goHome()); + }; + const saveBtn = h("button", { onClick: () => save() }, "Save"); + + return h( + "div", + { class: "schema-editor" }, + nameInput, + fields, + addFieldBtn, + saveBtn, + ); +}; diff --git a/frontend/components/SchemaEditor/actions.ts b/frontend/components/SchemaEditor/actions.ts deleted file mode 100644 index a48be59..0000000 --- a/frontend/components/SchemaEditor/actions.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { unreachable } from "../../../common/lib/dev.ts"; -import { push, setIdx, setProp } from "../../../common/lib/imm.ts"; -import { - Field, - FieldKind, - FieldOptions, - MaybePersistedSchema, - defaultOptionsForFieldKind, - validateSchema, -} from "../../../common/schema.ts"; -import { Const, ReturnTypes } from "../../../common/utils.ts"; -import { Reduce } from "../../state/lib/store.ts"; -import { State } from "./view.ts"; - -export const setName = (name: string) => ({ setName: name }); -export const addField = () => "addField" as const; -export const setFieldProp =

( - index: number, - prop: P, - value: Field[P], -) => ({ - index, - prop, - value, -}); - -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( - k: infer I, -) => void - ? I - : never; -export type AllOptions = UnionToIntersection; -export const setFieldOptionProp =

( - index: number, - optionProp: P, - value: Required, -) => ({ - index, - optionProp, - value, -}); -export type Action = ReturnTypes< - [ - typeof setName, - typeof addField, - typeof setFieldProp, - typeof setFieldOptionProp, - ] ->; - -const reduceSchema = ( - schema: Const, - action: Action, -): Const => { - console.debug(action); - if (action == "addField") { - return setProp( - schema, - "fields", - push(schema.fields, { - kind: "string", - name: "New field", - options: defaultOptionsForFieldKind("string"), - required: false, - }), - ); - } - - if ("setName" in action) { - return setProp(schema, "name", action.setName); - } - - if ("prop" in action) { - let field = setProp(schema.fields[action.index], action.prop, action.value); - - if (action.prop == "kind") { - field = setProp(field, "options", { - ...defaultOptionsForFieldKind(action.value as Field["kind"]), - ...field.options, - }); - } - - return setProp( - schema, - "fields", - setIdx(schema.fields, action.index, field), - ); - } - - if ("optionProp" in action) { - return setProp( - schema, - "fields", - setIdx( - schema.fields, - action.index, - setProp( - schema.fields[action.index], - "options", - setProp( - schema.fields[action.index].options as any, - action.optionProp as never, - action.value as never, - ), - ), - ), - ); - } - - return unreachable(); -}; - -export const reduce: Reduce = (state, action) => { - const schema = reduceSchema(state.schema, action); - const errors = validateSchema(schema); - - return { schema, errors }; -}; diff --git a/frontend/components/SchemaEditor/state.ts b/frontend/components/SchemaEditor/state.ts new file mode 100644 index 0000000..745318e --- /dev/null +++ b/frontend/components/SchemaEditor/state.ts @@ -0,0 +1,130 @@ +import { Reducer } from "preact/hooks"; +import { + Field, + FieldKind, + FieldOptions, + MaybePersistedSchema, + defaultOptionsForFieldKind, +} from "../../../common/schema.ts"; +import { ViewState } from "../App/state.ts"; +import { ReturnTypes, UnionToIntersection } from "../../../common/utils.ts"; +import { deleteIdx, setIdx, setProp } from "../../../common/lib/imm.ts"; + +export const setName = (name: string) => ({ kind: "setName" as const, name }); +export const addField = () => ({ kind: "addField" as const }); +export const setFieldProp = ( + index: number, + prop: K, + value: Field[K], +) => ({ + kind: "setFieldProp" as const, + index, + prop, + value, +}); + +export type AllOptions = UnionToIntersection; +export const setFieldOption = ( + index: number, + option: K, + value: AllOptions[K], +) => ({ + kind: "setFieldOption" as const, + index, + option, + value, +}); +export const deleteField = (index: number) => ({ + kind: "deleteField" as const, + index, +}); +export type Action = ReturnTypes< + [ + typeof setName, + typeof addField, + typeof setFieldProp, + typeof deleteField, + typeof setFieldOption, + ] +>; + +export type State = MaybePersistedSchema; + +const reduce_ = (state: State, action: Action): ViewState => { + if (action.kind == "setName") { + return { + kind: "SchemaEditor", + state: { + ...state, + name: action.name, + }, + }; + } else if (action.kind == "addField") { + return { + kind: "SchemaEditor", + state: { + ...state, + fields: [ + ...state.fields, + { + kind: "string", + name: "New field", + options: defaultOptionsForFieldKind("string"), + required: false, + }, + ], + }, + }; + } else if (action.kind == "setFieldProp") { + let field = setProp(state.fields[action.index], action.prop, action.value); + if (action.prop == "kind") { + field = setProp(field, "options", { + ...defaultOptionsForFieldKind(field.kind), + ...field.options, + }); + } + + return { + kind: "SchemaEditor", + state: { + ...state, + fields: setIdx(state.fields, action.index, field), + }, + }; + } else if (action.kind == "setFieldOption") { + return { + kind: "SchemaEditor", + state: setProp( + state, + "fields", + setIdx( + state.fields, + action.index, + setProp( + state.fields[action.index], + "options", + setProp( + state.fields[action.index].options, + action.option as never, + action.value as never, + ), + ), + ), + ), + }; + } else if (action.kind == "deleteField") { + return { + kind: "SchemaEditor", + state: setProp(state, "fields", deleteIdx(state.fields, action.index)), + }; + } + + return { + kind: "SchemaEditor", + state, + }; +}; + +export const reduce: Reducer = (state, action) => { + return reduce_(state.state as State, action); +}; diff --git a/frontend/components/SchemaEditor/view.ts b/frontend/components/SchemaEditor/view.ts deleted file mode 100644 index 335f686..0000000 --- a/frontend/components/SchemaEditor/view.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - MaybePersistedSchema, - ValidateSchemaResult, - validateSchema, -} from "../../../common/schema.ts"; -import { swel } from "../../deps/swel.ts"; -import { Dispatch } from "../../state/lib/store.ts"; -import { CmpPaintComponent, Paint } from "../component.ts"; -import { TextInput } from "../lib/input/TextInput.ts"; -import { Validated } from "../lib/input/Validated.ts"; -import { Fields } from "./Fields.ts"; -import { Action, setName, addField } from "./actions.ts"; - -export type State = { - schema: MaybePersistedSchema; - errors: ValidateSchemaResult; -}; - -export class SchemaEditor extends CmpPaintComponent { - private nameInput: Validated; - private fields: Fields; - private container: HTMLDivElement; - - constructor(dispatch: Dispatch) { - super(); - - this.nameInput = new Validated( - new TextInput({ - onInput: (s) => dispatch(setName(s)), - }), - ); - - const addFieldBtn = swel( - "button", - { - on: { - click: () => { - dispatch(addField()); - }, - }, - }, - "Add field", - ); - - this.fields = new Fields(dispatch); - - this.container = swel("div", { className: "schema-editor" }, [ - this.nameInput.el, - this.fields.el, - addFieldBtn, - ]); - } - - get el() { - return this.container; - } - - paint_: Paint = (cur) => { - this.nameInput.paint({ - model: cur.schema.name, - error: cur.errors.name?.[0], - }); - - const fields = cur.schema.fields.map((f, i) => ({ - field: f, - errors: cur.errors.fields?.[i], - })); - - this.fields.paint(fields); - }; -} diff --git a/frontend/components/lib/Labeled.ts b/frontend/components/lib/Labeled.ts index 920181a..1cdc53f 100644 --- a/frontend/components/lib/Labeled.ts +++ b/frontend/components/lib/Labeled.ts @@ -1,27 +1,5 @@ -import { swel } from "../../deps/swel.ts"; -import { - CmpPaintComponent, - ComponentModel, - Paint, - PaintComponent, -} from "../component.ts"; +import { h, ComponentChildren } from "preact"; -export class Labeled< - Child extends PaintComponent, -> extends CmpPaintComponent> { - private label: HTMLLabelElement; - - constructor(text: string, public child: Child) { - super(); - - this.label = swel("label", {}, [text, this.child.el]); - } - - get el(): HTMLElement { - return this.label; - } - - paint_: Paint> = (c) => { - this.child.paint(c); - }; -} +export const Labeled = (p: { label: string; children?: ComponentChildren }) => { + return h("label", {}, p.label, p.children); +}; diff --git a/frontend/components/lib/ShowHide.ts b/frontend/components/lib/ShowHide.ts index 5081586..d0fddd6 100644 --- a/frontend/components/lib/ShowHide.ts +++ b/frontend/components/lib/ShowHide.ts @@ -1,35 +1,9 @@ -import { swel } from "../../deps/swel.ts"; -import { ComponentModel, Paint } from "../component.ts"; -import { CmpPaintComponent } from "../component.ts"; +import { Fragment, VNode, h } from "preact"; -export type ShowHideModel = { - show: boolean; - model: T; -}; - -export class ShowHide< - Child extends CmpPaintComponent, -> extends CmpPaintComponent>> { - private container: HTMLDivElement; - - constructor(private child: Child) { - super(); - - this.container = swel("div", {}, this.child.el); +export const ShowHide = (p: { show: boolean; inner: () => VNode }) => { + if (p.show) { + return p.inner(); } - get el(): HTMLElement { - return this.container; - } - - protected paint_: Paint>> = (cur) => { - if (cur.show) { - this.child.paint(cur.model); - if (!this.previousModel?.show) { - this.container.replaceChildren(this.child.el); - } - } else { - this.container.replaceChildren(""); - } - }; -} + return h(Fragment, {}); +}; diff --git a/frontend/components/lib/input/Checkbox.ts b/frontend/components/lib/input/Checkbox.ts new file mode 100644 index 0000000..465e82f --- /dev/null +++ b/frontend/components/lib/input/Checkbox.ts @@ -0,0 +1,17 @@ +import { h } from "preact"; +import { useRef } from "preact/hooks"; +import { Callback } from "../../../utils.ts"; + +export const Checkbox = (p: { + checked: boolean; + onToggle: Callback<[boolean]>; +}) => { + const ref = useRef(null); + + return h("input", { + ref, + type: "checkbox", + checked: p.checked, + onChange: () => p.onToggle(ref.current!.checked), + }); +}; diff --git a/frontend/components/lib/input/NumberInput.ts b/frontend/components/lib/input/NumberInput.ts index db9545b..173c6c2 100644 --- a/frontend/components/lib/input/NumberInput.ts +++ b/frontend/components/lib/input/NumberInput.ts @@ -1,33 +1,24 @@ -import { swel } from "../../../deps/swel.ts"; +import { JSX, h } from "preact"; +import { useRef } from "preact/hooks"; import { Callback } from "../../../utils.ts"; -import { CmpPaintComponent, Paint } from "../../component.ts"; -export class NumberInput extends CmpPaintComponent { - private input: HTMLInputElement = swel("input", { type: "number" }); +export const NumberInput = ( + p: Omit, "onInput"> & { + value: number; + min?: number; + max?: number; + onInput: Callback<[number]>; + }, +) => { + const ref = useRef(null); - constructor(p?: Partial<{ onChange: Callback<[number]> }>) { - super(); - - if (p?.onChange) { - this.input.addEventListener("input", () => - p.onChange!(Number(this.input.value)), - ); - } - } - - get el() { - return this.input; - } - - setMin = (n?: number) => { - this.input.min = `${n ?? ""}`; - }; - - setMax = (n?: number) => { - this.input.max = `${n ?? ""}`; - }; - - paint_: Paint = (cur) => { - this.input.value = `${cur}`; - }; -} + return h("input", { + ...p, + type: "number", + ref, + min: p.min, + max: p.max, + value: p.value, + onInput: () => p.onInput(Number(ref.current!.value)), + }); +}; diff --git a/frontend/components/lib/input/Select.ts b/frontend/components/lib/input/Select.ts index 67e4532..c8a1000 100644 --- a/frontend/components/lib/input/Select.ts +++ b/frontend/components/lib/input/Select.ts @@ -1,33 +1,26 @@ -import { swel } from "../../../deps/swel.ts"; +import { h } from "preact"; +import { useRef } from "preact/hooks"; import { Callback } from "../../../utils.ts"; -import { CmpPaintComponent, Paint } from "../../component.ts"; export type Options = Record; -export class Select extends CmpPaintComponent { - private select: HTMLSelectElement; +export const Select = (p: { + value: Values; + options: Options; + onChange: Callback<[Values]>; +}) => { + const ref = useRef(null); + const select = h( + "select", + { + ref, + value: p.value, + onChange: () => p.onChange(ref.current!.value as Values), + }, + ...Object.entries(p.options).map(([k, v]) => + h("option", { value: `${v}` }, k), + ), + ); - constructor(options: Options, onChange: Callback<[Values]>) { - super(); - - this.select = swel( - "select", - { - on: { - change: () => onChange(this.select.value as Values), - }, - }, - (Object.keys(options) as (keyof typeof options)[]).map((o) => - swel("option", { value: `${o}` }, options[o]), - ), - ); - } - - get el(): HTMLElement { - return this.select; - } - - protected paint_: Paint = (cur) => { - this.select.value = cur as string; - }; -} + return select; +}; diff --git a/frontend/components/lib/input/TextInput.ts b/frontend/components/lib/input/TextInput.ts index eca14c3..d4f4f33 100644 --- a/frontend/components/lib/input/TextInput.ts +++ b/frontend/components/lib/input/TextInput.ts @@ -1,23 +1,19 @@ -import { swel } from "../../../deps/swel.ts"; +import { h, JSX } from "preact"; +import { useRef } from "preact/hooks"; import { Callback } from "../../../utils.ts"; -import { CmpPaintComponent, Paint } from "../../component.ts"; -export class TextInput extends CmpPaintComponent { - private input: HTMLInputElement = swel("input"); +export const TextInput = ( + p: Omit, "onInput"> & { + placeholder?: string; + value: string; + onInput: Callback<[string]>; + }, +) => { + const ref = useRef(null); - constructor(p?: Partial<{ onInput: Callback<[string]> }>) { - super(); - - if (p?.onInput) { - this.input.addEventListener("input", () => p.onInput!(this.input.value)); - } - } - - get el() { - return this.input; - } - - paint_: Paint = (cur) => { - this.input.value = cur; - }; -} + return h("input", { + ...p, + ref, + onInput: () => p.onInput(ref.current!.value), + }); +}; diff --git a/frontend/components/lib/input/Validated.ts b/frontend/components/lib/input/Validated.ts index a6fdb54..9dde936 100644 --- a/frontend/components/lib/input/Validated.ts +++ b/frontend/components/lib/input/Validated.ts @@ -1,39 +1,15 @@ -import { swel } from "../../../deps/swel.ts"; -import { CmpPaintComponent, ComponentModel, Paint } from "../../component.ts"; - -export type ValidatedModel = { - model: T; - error?: string; +import { ComponentChildren, h } from "preact"; + +export const Validated = (p: { + errors?: string[]; + children?: ComponentChildren; +}) => { + const errorText = h("div", { class: "error-message" }, ...(p.errors ?? [])); + + return h( + "div", + { class: `validated ${p.errors ? "error" : ""}` }, + p.children, + errorText, + ); }; - -export class Validated< - Child extends CmpPaintComponent, -> extends CmpPaintComponent>> { - private container: HTMLDivElement; - private errorMessage = swel("div", { className: "error-message" }); - - constructor(public child: Child) { - super(); - - this.container = swel("div", { className: "validated" }, [ - (this.child as any).el, - this.errorMessage, - ]); - } - - get el() { - return this.container; - } - - paint_: Paint>> = ({ model, error }) => { - if (error) { - this.container.classList.add("error"); - this.errorMessage.replaceChildren(error); - } else { - this.container.classList.remove("error"); - this.errorMessage.replaceChildren(" "); - } - - (this.child as any).paint(model); - }; -} diff --git a/frontend/fetch.ts b/frontend/fetch.ts new file mode 100644 index 0000000..f77740d --- /dev/null +++ b/frontend/fetch.ts @@ -0,0 +1,37 @@ +import { Reducer, useMemo, useReducer } from "preact/hooks"; +import { MakeClient, assert, makeClient } from "../common/deps/yaypi.ts"; +import { Api } from "../common/api.ts"; + +export type Client = MakeClient; + +export type Loadable = + | null + | { loading: true } + | { loading: boolean; data: T } + | { loading: boolean; error: string }; +export const isLoading = (loadable: Loadable) => loadable?.loading; + +export type Data = { + schemas: Loadable>; +}; +export type Action = never; + +const reduce: Reducer = (data, action) => { + return data; +}; + +const emptyData: Data = { + schemas: null, +}; + +// NOTE: this should only be used in App.ts +export const useFetch = () => { + const client = useMemo(() => makeClient(Api, "/api"), []); + const [data, reducer] = useReducer(reduce, emptyData); + + const listSchemas = async ( + ...args: Parameters + ) => { + const schemas = assert(await client.v1.listSchemas(...args)); + }; +}; diff --git a/frontend/main.ts b/frontend/main.ts index 284f914..3d2a8ce 100644 --- a/frontend/main.ts +++ b/frontend/main.ts @@ -1,12 +1,20 @@ -import { App } from "./components/App/view.ts"; -import { LOCAL_STORAGE_KEY, initStore } from "./state/store.ts"; +import "preact/debug"; +import { h, render } from "preact"; + +import { App } from "./components/App/App.ts"; + +import { + LOCAL_STORAGE_KEY, + defaultAppState, + loadStateFromLocalStorage, +} from "./state/store.ts"; const init = () => { - let app: App | null = null; - const { dispatch } = initStore((cur, prev) => app?.paint(cur)); - app = new App(dispatch); + const state = loadStateFromLocalStorage(); + // const state = defaultAppState; + const app = h(App, state); - document.body.replaceChildren(app.el); + render(app, document.body); }; document.addEventListener("DOMContentLoaded", init); diff --git a/frontend/state/store.ts b/frontend/state/store.ts index e0754d8..cbc2320 100644 --- a/frontend/state/store.ts +++ b/frontend/state/store.ts @@ -1,34 +1,20 @@ import { Const } from "../../common/utils.ts"; -import { OnChange, createStore } from "./lib/store.ts"; -import { Action, reduce } from "../components/App/actions.ts"; -import { AppState } from "../components/App/view.ts"; +import { State } from "../components/App/state.ts"; -const defaultAppState: AppState = { - view: { kind: "Home", state: undefined }, +export const defaultAppState: State = { + view: { kind: "Home", state: null }, }; export const LOCAL_STORAGE_KEY = "zchemaAppState"; -const saveStateToLocalStorage = (state: Const) => { +export const saveStateToLocalStorage = (state: Const) => { localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state)); }; -const loadStateFromLocalStorage = (): AppState | null => { +export const loadStateFromLocalStorage = (): State => { const r = localStorage.getItem(LOCAL_STORAGE_KEY); if (r == null) { - return r; + return defaultAppState; } return JSON.parse(r); }; - -export const initStore = (paint: OnChange) => { - const state = loadStateFromLocalStorage() ?? defaultAppState; - - const store = createStore(state, reduce, (cur, prev) => { - console.debug({ state: cur, prev }); - saveStateToLocalStorage(cur); - paint(cur, prev); - }); - - return store; -}; diff --git a/frontend/style.scss b/frontend/style.scss index c2953ce..b3cf955 100644 --- a/frontend/style.scss +++ b/frontend/style.scss @@ -13,6 +13,14 @@ input, button, select { font-size: inherit; } +input { + width: 100%; + + &[type="checkbox"] { + width: unset; + } +} + // Page Layout body { margin: 0; @@ -20,11 +28,15 @@ body { } // Components -.nav-chrome { +nav { padding: 1em; + + display: flex; + flex-direction: row; + gap: 1em; } -.main { +main { padding: 1em; } diff --git a/frontend/utils.ts b/frontend/utils.ts index b4d2469..fd93ef5 100644 --- a/frontend/utils.ts +++ b/frontend/utils.ts @@ -2,3 +2,5 @@ import { Const } from "../common/utils.ts"; export type Callback = (...args: T) => unknown; export type Update = (cb: (value: T) => void) => void; + +export type Dispatch = (a: Action) => unknown;