diff --git a/package-lock.json b/package-lock.json index 3c05123..5fe0d1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,16 @@ "@prisma/client": "^4.9.0", "@solidjs/meta": "^0.28.2", "@solidjs/router": "^0.8.2", + "classnames": "^2.3.2", "prisma": "^4.9.0", "solid-js": "^1.7.2", "solid-start": "^0.2.26", + "solid-styled-components": "^0.28.5", "undici": "^5.15.1" }, "devDependencies": { "@types/debug": "^4.1.7", + "sass": "^1.62.0", "solid-start-node": "^0.2.19", "typedoc": "^0.24.4", "typescript": "^4.9.4", @@ -2561,6 +2564,37 @@ "node": ">=4" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/color-convert": { "version": "1.9.3", "license": "MIT", @@ -2960,6 +2994,14 @@ "node": ">=4" } }, + "node_modules/goober": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.12.tgz", + "integrity": "sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2987,6 +3029,12 @@ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==" }, + "node_modules/immutable": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "devOptional": true + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3463,6 +3511,17 @@ } ] }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -3732,6 +3791,23 @@ "node": ">=6" } }, + "node_modules/sass": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz", + "integrity": "sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==", + "devOptional": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/semver": { "version": "6.3.0", "license": "ISC", @@ -3902,32 +3978,6 @@ "vite": "*" } }, - "node_modules/solid-start/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/solid-start/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3949,15 +3999,16 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/solid-start/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/solid-styled-components": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/solid-styled-components/-/solid-styled-components-0.28.5.tgz", + "integrity": "sha512-vwTcdp76wZNnESIzB6rRZ3U55NgcSAQXCiiRIiEFhxTFqT0bEh/warNT1qaRZu4OkAzrBkViOngF35ktI8sc4A==", "dependencies": { - "picomatch": "^2.2.1" + "csstype": "^3.1.0", + "goober": "^2.1.10" }, - "engines": { - "node": ">=8.10.0" + "peerDependencies": { + "solid-js": "^1.4.4" } }, "node_modules/source-map": { diff --git a/package.json b/package.json index 9dc1844..6463820 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "type": "module", "devDependencies": { "@types/debug": "^4.1.7", + "sass": "^1.62.0", "solid-start-node": "^0.2.19", "typedoc": "^0.24.4", "typescript": "^4.9.4", @@ -17,9 +18,11 @@ "@prisma/client": "^4.9.0", "@solidjs/meta": "^0.28.2", "@solidjs/router": "^0.8.2", + "classnames": "^2.3.2", "prisma": "^4.9.0", "solid-js": "^1.7.2", "solid-start": "^0.2.26", + "solid-styled-components": "^0.28.5", "undici": "^5.15.1" }, "engines": { diff --git a/src/components/todos/Footer.module.scss b/src/components/todos/Footer.module.scss new file mode 100644 index 0000000..6ade885 --- /dev/null +++ b/src/components/todos/Footer.module.scss @@ -0,0 +1,51 @@ +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; + + &:before { + content: ""; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); + } +} + +.todoCount { + float: left; + text-align: left; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filterButton { + display: inline; + + a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; + + &.selected { + border-color: rgba(175, 47, 47, 0.2); + } + } +} diff --git a/src/components/todos/Footer.tsx b/src/components/todos/Footer.tsx new file mode 100644 index 0000000..1bdd5b4 --- /dev/null +++ b/src/components/todos/Footer.tsx @@ -0,0 +1,45 @@ +import classNames from "classnames"; +import { Setter } from "solid-js"; + +import { NowShowing } from "~/lib/todos/common"; +import styles from "./Footer.module.scss"; + +export interface IFooterProps { + count: number; + nowShowing: NowShowing; + setNowShowing: Setter; +} + +export default function Footer(props: IFooterProps) { + const activeTodoWord = props.count == 1 ? "item" : "items"; + + const clearButton = <>; + + const showButton = (text: string, value: NowShowing) => { + return ( +
  • + props.setNowShowing(value)} + class={classNames(props.nowShowing === value && styles.selected)} + > + {text} + +
  • + ); + }; + + return ( + + ); +} diff --git a/src/components/todos/TodoItem.module.scss b/src/components/todos/TodoItem.module.scss new file mode 100644 index 0000000..8b94b99 --- /dev/null +++ b/src/components/todos/TodoItem.module.scss @@ -0,0 +1,127 @@ +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.todoItem { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; + + &:last-child { + border-bottom: none; + } + + label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; + } + + &:hover .destroy { + display: block; + } +} + +.toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; + opacity: 0; + + label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; + } + + & + label { + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; + } + + &:checked + label { + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E"); + } +} + +.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.editing .edit { + display: block; + width: 506px; + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.edit { + display: block; + width: calc(100% - 43px); + padding: 12px 16px; + margin: 0 0 0 43px; + + position: relative; + // margin: 0; + // width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + color: inherit; + // padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; + + &:hover { + color: #af5b5e; + } + + &:after { + content: "×"; + } +} diff --git a/src/components/todos/TodoItem.tsx b/src/components/todos/TodoItem.tsx new file mode 100644 index 0000000..2957787 --- /dev/null +++ b/src/components/todos/TodoItem.tsx @@ -0,0 +1,86 @@ +import classNames from "classnames"; +import { batch, createSignal, Show } from "solid-js"; +import styles from "./TodoItem.module.scss"; + +const ENTER_KEY = 13; +const ESCAPE_KEY = 27; + +export interface ITodoItemProps { + title: string; + completed: boolean; + updating: boolean; + + onRemove: () => Promise; + onCommit: (newTitle: string) => Promise; + onComplete: (completed: boolean) => Promise; +} + +export default function TodoItem(props: ITodoItemProps) { + const [isEditing, setIsEditing] = createSignal(false); + const [editText, setEditText] = createSignal(""); + let inputBox: HTMLInputElement | undefined; + + const beginEdit = () => { + batch(() => { + setEditText(props.title); + setIsEditing(true); + }); + inputBox?.focus(); + }; + + const endEditAndCommit = async () => { + await props.onCommit(editText()); + setIsEditing(false); + }; + + const endEditAndCancel = () => { + setIsEditing(false); + }; + + const handleKey = async (e) => { + setEditText(e.currentTarget.value); + if (e.keyCode === ESCAPE_KEY) { + endEditAndCancel(); + } else if (e.keyCode === ENTER_KEY) { + await endEditAndCommit(); + } + }; + + const liClasses = classNames( + styles.todoItem, + props.completed ? styles.completed : null, + isEditing() ? styles.editing : null, + ); + + return ( +
  • + +
    + props.onComplete(e.currentTarget.checked)} + /> + +
    +
    + + + setEditText(e.currentTarget.value)} + /> + +
  • + ); +} diff --git a/src/core/db.ts b/src/core/db.ts index 429231b..9755e8e 100644 --- a/src/core/db.ts +++ b/src/core/db.ts @@ -123,14 +123,12 @@ export class Database { }); } } - - return await this.prisma.node.findFirst({ - where: { id: request.id }, - select: { id: true, label: true, metadata: true }, - }); }); - return node; + return await this.prisma.node.findFirst({ + where: { id: request.id }, + select: { id: true, label: true, metadata: true }, + }); } public async removeNode(request: IRemoveNodeRequest): Promise { diff --git a/src/lib/todos/client.ts b/src/lib/todos/client.ts new file mode 100644 index 0000000..a14a16f --- /dev/null +++ b/src/lib/todos/client.ts @@ -0,0 +1,23 @@ +import { createEffect } from "solid-js"; +import { createStore, SetStoreFunction, Store } from "solid-js/store"; + +export function createTodoStore( + init: T +): [Store, SetStoreFunction] { + // const localState = localStorage.getItem(name); + + const [state, setState] = createStore( + init || [] + // localState ? JSON.parse(localState) : init + ); + + createEffect(() => { + // localStorage.setItem(name, JSON.stringify(state)); + }); + + return [state, setState]; +} + +export function removeIndex(array: readonly T[], index: number): T[] { + return [...array.slice(0, index), ...array.slice(index + 1)]; +} diff --git a/src/lib/todos/common.ts b/src/lib/todos/common.ts new file mode 100644 index 0000000..6c1cf5d --- /dev/null +++ b/src/lib/todos/common.ts @@ -0,0 +1,5 @@ +export enum NowShowing { + ALL_TODOS = "all_todos", + ACTIVE_TODOS = "active_todos", + COMPLETED_TODOS = "completed_todos", +} diff --git a/src/lib/todos/server.ts b/src/lib/todos/server.ts new file mode 100644 index 0000000..05f8364 --- /dev/null +++ b/src/lib/todos/server.ts @@ -0,0 +1,68 @@ +import { FetchedNode } from "~/core/types"; +import { db, todosApp } from "~/db"; + +export interface ITodoItem { + id: string; + title: string; + completed: boolean; + committed: boolean; + deleted: boolean; +} + +export const retrieveMeta = (node: FetchedNode, key: string) => { + const meta = node.metadata.find( + (meta) => meta.appId == todosApp.id && meta.appKey == key + ); + if (!meta) return null; + const metaValue = meta.value.toString(); + return JSON.parse(metaValue); +}; + +export const nodeToTodoItem = (node: FetchedNode) => ({ + id: node.id, + title: retrieveMeta(node, "title"), + completed: retrieveMeta(node, "completed"), + committed: true, + deleted: false, +}); + +export async function addTodoActionImpl({ title }: { title: string }) { + const metadata_keys = new Map(); + const setMeta = (appKey: string, value: any) => + metadata_keys.set({ appId: todosApp.id, appKey }, JSON.stringify(value)); + setMeta("title", title); + setMeta("completed", false); + + const node = await db.createNode({ + label: `todo-${title}`, + metadata_keys, + }); + + return node; +} + +export async function updateTodoActionImpl({ + id, + title, + completed, +}: { + id: string; + title: string; + completed: boolean; +}) { + const metadata_keys = new Map(); + const setMeta = (appKey: string, value: any) => + metadata_keys.set({ appId: todosApp.id, appKey }, JSON.stringify(value)); + setMeta("title", title); + setMeta("completed", completed); + console.log("Updating", metadata_keys); + + const node = await db.updateNode({ id, metadata_keys }); + if (!node) return; + + return nodeToTodoItem(node); +} + +export async function removeTodoActionImpl({ id }: { id: string }) { + await db.removeNode({ id }); +} diff --git a/src/routes/todos/index.module.scss b/src/routes/todos/index.module.scss new file mode 100644 index 0000000..c9fe802 --- /dev/null +++ b/src/routes/todos/index.module.scss @@ -0,0 +1,105 @@ +.container { + min-width: 230px; + max-width: 550px; + margin: 0 auto; +} + +:focus { + outline: none; +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; + line-height: 1.4em; +} + +.todoApp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); + + input::-webkit-input-placeholder, + input::-moz-placeholder, + input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; + } + + h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; + } +} + +.newTodo { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); +} + +.toggleAll { + width: 1px; + height: 1px; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; + + & + label { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -13px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + + &:before { + content: "❯"; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; + } + } + + &:checked + label:before { + color: #737373; + } +} + +.todoList { + margin: 0; + padding: 0; + list-style: none; +} diff --git a/src/routes/todos/index.tsx b/src/routes/todos/index.tsx index aa9588f..eaaab47 100644 --- a/src/routes/todos/index.tsx +++ b/src/routes/todos/index.tsx @@ -1,35 +1,20 @@ -import { For, batch, createEffect, createSignal, JSX } from "solid-js"; -import { SetStoreFunction, Store, createStore } from "solid-js/store"; +import { For, batch, createSignal, JSX } from "solid-js"; import { useRouteData } from "solid-start"; import { createServerAction$, createServerData$ } from "solid-start/server"; -import { FetchedNode } from "~/core/types"; +import Footer from "~/components/todos/Footer"; +import styles from "./index.module.scss"; +import TodoItem from "~/components/todos/TodoItem"; import { db, todosApp } from "~/db"; - -const retrieveMeta = (node: FetchedNode, key: string) => { - const meta = node.metadata.find( - (meta) => meta.appId == todosApp.id && meta.appKey == key - ); - if (!meta) return null; - const metaValue = meta.value.toString(); - return JSON.parse(metaValue); -}; - -const nodeToTodoItem = (node: FetchedNode) => ({ - id: node.id, - title: retrieveMeta(node, "title"), - completed: retrieveMeta(node, "completed"), - committed: true, - deleted: false, -}); - -interface TodoItem { - id: string; - title: string; - completed: boolean; - committed: boolean; - deleted: boolean; -} +import { createTodoStore, removeIndex } from "~/lib/todos/client"; +import { + addTodoActionImpl, + nodeToTodoItem, + removeTodoActionImpl, + ITodoItem, + updateTodoActionImpl, +} from "~/lib/todos/server"; +import { NowShowing } from "~/lib/todos/common"; export function routeData() { return createServerData$(async () => { @@ -53,28 +38,12 @@ export default function Todos() { const { initTodos } = data; const [newTitle, setTitle] = createSignal(""); - const [todos, setTodos] = createTodoStore("todos", initTodos); + const [todos, setTodos] = createTodoStore(initTodos); + const [nowShowing, setNowShowing] = createSignal(NowShowing.ALL_TODOS); - const [adding, addTodoAction] = createServerAction$( - async ({ title }: { title: string }) => { - const metadata_keys = new Map(); - const setMeta = (appKey: string, value: any) => - metadata_keys.set( - { appId: todosApp.id, appKey }, - JSON.stringify(value) - ); - setMeta("title", title); - setMeta("completed", false); - - const node = await db.createNode({ - label: `todo-${title}`, - metadata_keys, - }); - - return node; - }, - { invalidate: ["todos"] } - ); + const [adding, addTodoAction] = createServerAction$(addTodoActionImpl, { + invalidate: ["todos"], + }); const addTodo = async (e: SubmitEvent) => { e.preventDefault(); @@ -102,34 +71,13 @@ export default function Todos() { }; const [updating, updateTodoAction] = createServerAction$( - async ({ - id, - title, - completed, - }: { - id: string; - title: string; - completed: boolean; - }) => { - const metadata_keys = new Map(); - const setMeta = (appKey: string, value: any) => - metadata_keys.set( - { appId: todosApp.id, appKey }, - JSON.stringify(value) - ); - setMeta("title", title); - setMeta("completed", completed); - - const node = await db.updateNode({ id, metadata_keys }); - if (!node) return; - - return nodeToTodoItem(node); - } + updateTodoActionImpl, + { invalidate: ["todos"] } ); const updateNode = async ( idx: number, - prop: keyof TodoItem, + prop: "title" | "completed", newValue: any ) => { batch(() => { @@ -138,22 +86,25 @@ export default function Todos() { }); const todoItem = todos[idx]; - const node = await updateTodoAction({ + const updateProps = { id: todoItem.id, title: todoItem.title, completed: todoItem.completed, - }); - if (node) + }; + updateProps[prop] = newValue; + + const node = await updateTodoAction(updateProps); + if (node) { batch(() => { + setTodos(idx, "title", node.title); setTodos(idx, "completed", node.completed); setTodos(idx, "committed", true); }); + } }; const [removing, removeTodoAction] = createServerAction$( - async ({ id }: { id: string }) => { - await db.removeNode({ id }); - }, + removeTodoActionImpl, { invalidate: ["todos"] } ); @@ -170,74 +121,89 @@ export default function Todos() { setTodos((t) => removeIndex(t, idx)); }; - return ( - <> -

    Simple Todos Example

    -
    + const activeTodoCount = () => + todos.reduce((accum, todo) => (todo.completed ? accum : accum + 1), 0); + + const shownTodos = () => + todos.filter((todo) => { + switch (nowShowing()) { + case NowShowing.ALL_TODOS: + return true; + case NowShowing.ACTIVE_TODOS: + return !todo.completed; + case NowShowing.COMPLETED_TODOS: + return todo.completed; + } + }); + + const todoItems = ( + + {(todo, i) => { + const style: JSX.CSSProperties = {}; + if (todo.deleted) style["text-decoration"] = "line-through"; + return ( + removeTodo(i())} + onCommit={(string) => updateNode(i(), "title", string)} + onComplete={(done) => updateNode(i(), "completed", done)} + /> + ); + }} + + ); + + let main; + if (shownTodos().length) { + main = ( +
    setTitle(e.currentTarget.value)} - disabled={adding.pending} + id="toggle-all" + class={styles.toggleAll} + type="checkbox" + // onChange={(e) => this.toggleAll(e)} + checked={activeTodoCount() === 0} /> - - + -

    Todos:

    - - {(todo, i) => { - const style: JSX.CSSProperties = {}; - if (todo.deleted) style["text-decoration"] = "line-through"; - return ( -
    -

    {JSON.stringify(todo)}

    +
      {todoItems}
    +
    + ); + } - - updateNode(i(), "completed", e.currentTarget.checked) - } - /> - - updateNode(i(), "title", e.currentTarget.value) - } - /> - - - {updating?.input?.id == todo.id && - updating.pending && - "saving..."} - - ); - }} - - - ); -} - -function createTodoStore( - name: string, - init: T -): [Store, SetStoreFunction] { - // const localState = localStorage.getItem(name); - - const [state, setState] = createStore( - init || [] - // localState ? JSON.parse(localState) : init + let footer; + footer = ( +