Implement working prototype

This commit is contained in:
Dominic Zimmer 2025-05-03 12:16:36 +02:00
parent 9979d96022
commit 4fc6f94b0e
6 changed files with 250 additions and 35 deletions

7
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@types/node": "^16.18.126",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"dayjs": "^1.11.13",
"lowdb": "^7.0.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@ -6469,6 +6470,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",

View File

@ -11,6 +11,7 @@
"@types/node": "^16.18.126",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"dayjs": "^1.11.13",
"lowdb": "^7.0.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",

View File

@ -1,4 +1,54 @@
{
"foo": "bar",
"baz": 42
"entries": {
"20250406": {
"items": [
{
"variant": "eats",
"date": "Sat May 03 2025 12:07:11 GMT+0200 (Central European Summer Time)",
"item": "strawberries"
},
{
"variant": "eats",
"date": "Sat May 03 2025 12:08:00 GMT+0200 (Central European Summer Time)",
"item": "nothing"
},
{
"variant": "eats",
"date": "Sat May 03 2025 12:09:03 GMT+0200 (Central European Summer Time)",
"item": "nuttin"
},
{
"variant": "eats",
"date": "Sat May 03 2025 12:09:56 GMT+0200 (Central European Summer Time)",
"item": "foo"
},
{
"variant": "eats",
"date": "Sat May 03 2025 12:12:57 GMT+0200 (Central European Summer Time)",
"item": "swine"
},
{
"variant": "allergy",
"date": "Sat May 03 2025 12:16:05 GMT+0200 (Central European Summer Time)"
},
{
"variant": "eats",
"date": "Sat May 03 2025 12:16:10 GMT+0200 (Central European Summer Time)",
"item": "Nothing much"
},
{
"variant": "eats",
"date": "Sat May 03 2025 12:16:13 GMT+0200 (Central European Summer Time)",
"item": "Mushrooms"
}
]
}
},
"index": [
[
20250406,
"2025-05-03T09:28:49.374Z"
]
],
"version": 0
}

View File

@ -1,45 +1,99 @@
import React from 'react';
import React, { JSX, useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
import { addEntry, ensureToday, getAPI, getToday } from './api';
import { ItemType, Store, StoreEntry } from './types';
import dayjs from 'dayjs';
const defaultStore: Store = {
index: [],
version: 0,
entries: {},
};
const key="c3dpZ2dpdHlzd29vdHkK"
function App() {
export const App : React.FC = () => {
const [API, setAPI] = useState(() => getAPI(key));
const [store, setStore] = useState<Store>(defaultStore);
useEffect(() => {
const f = async () => {
const st = await API.read();
setStore(ensureToday(st));
};
f();
}, [API]);
const [input, setInput] = useState<string>("");
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
onClick={async () => {
const rawResponse = await fetch(`http://localhost:4444/write/${key}/`, {
method: 'POST',
headers: {
},
body: JSON.stringify({a: 1, b: 'Textual content'})
});
await rawResponse.text();
<div className="eats">
<h1>I ate ...</h1>
<form>
<input type="text" placeholder='Stinking Fish' onChange={(e) => setInput(e.target.value)} value={input} />
<button
onClick={ async () => {
const newst = addEntry(store, {variant: "eats", date: new Date().toString(), item: input} );
console.log("newstore is", newst);
await API.write(JSON.stringify(store))
const fromserver = await API.read();
setStore({...fromserver} );
setInput("");
}}
>
Write
</a>
<a
className="App-link"
onClick={async () => {
const rawResponse = await fetch(`http://localhost:4444/read/${key}/`, {
method: 'POST'
});
await rawResponse.text();
disabled={input.trim().length < 2}
>Submit</button>
</form>
</div>
<div className="reacts">
<h1>I have allergy</h1>
<button
onClick={ async () => {
const newst = addEntry(store, {variant: "allergy", date: new Date().toString()} );
await API.write(JSON.stringify(store))
const fromserver = await API.read();
setStore({...fromserver} );
}}
>
Read
</a>
</header>
</div>
>Now!</button>
</div>
{store ?
<RenderTable store={store} />
:null}
<details name='foo'>
<summary>Raw Store</summary>
<pre>
{JSON.stringify(store, null, 4)}
</pre>
</details>
</div>
);
}
};
const DATEFORMAT = "YYYY MM DD HH:mm";
export const RenderTable : React.FC<{store: Store}> = ({store}) => {
const listEntries = Object
.values(store.entries)
.flatMap<ItemType>((entry) => entry.items)
.sort((a,b) => b.date.localeCompare(a.date));
const itemToRow = (item: ItemType, index: number): JSX.Element => {
const datestring = dayjs(new Date(item.date)).format(DATEFORMAT)
if (item.variant === "eats") {
return <tr key={index} className="eats"><td>{datestring}</td><td><small>ate</small></td><td> {item.item}</td></tr>;
} else if (item.variant === "allergy") {
return <tr key={index} className="eats"><td>{datestring}</td><td colSpan={2}><small>Reaction!</small></td></tr>;
}
return <></>;
}
return <table className="entry-table">
<thead>
<tr><th>Time</th><th colSpan={2}>Event</th></tr>
</thead>
<tbody>
{listEntries.map(itemToRow)}
</tbody>
</table>
};
export default App;

77
src/api.ts Normal file
View File

@ -0,0 +1,77 @@
import { ItemType, Store, StoreEntry } from "./types"
const defaultEndpoint = "http://localhost:4444/"
type APIEndPoint = {
write: (json: string) => Promise<void>,
read: () => Promise<Store>,
};
const makeEndpoint = (key: string, method: string, endPointOverride?: string): string => {
return `${endPointOverride ?? defaultEndpoint}\/${method}\/${key}\/`;
}
export const getAPI = (key: string, endPointOverride?: string): APIEndPoint => ({
write: async (json: string) => {
const rawResponse = await fetch(makeEndpoint(key, "write", endPointOverride), {
method: 'POST',
headers: {
},
body: json,
});
const resp = await rawResponse.text();
return;
},
read: async () => {
const rawResponse = await fetch(makeEndpoint(key, "read", endPointOverride), {
method: 'POST'
});
const respText = await rawResponse.text();
return JSON.parse(respText, ) as Store;
}
});
export const ensureToday = (store: Store): Store => {
const today : Date = new Date();
const storeEntry = store.index.find(([key, dateString]) => {
const date = new Date(dateString);
const [y, m, d] = [today.getFullYear(), today.getMonth(), today.getDay()];
return (date.getFullYear() === y && date.getMonth() === m && date.getDay() == d);
});
if (storeEntry !== undefined) return store;
const indexKey = today.getDay() + today.getMonth() * 100 + today.getFullYear() * 10000;
const entries = store.entries;
const newEntry : StoreEntry = {
items: [],
}
entries[indexKey] = newEntry;
return {
entries: entries,
index: [[indexKey, today.toString()], ...store.index ],
version: store.version,
};
}
export const addEntry = (store: Store, entry: ItemType): Store => {
const [storeIndex, dateString] = getToday(ensureToday(store))!;
const today = store.entries[storeIndex];
today.items = [...today.items, entry];
store.entries[storeIndex] = today;
return store;
}
export const getToday = (store: Store) => {
if (!store) return;
const today : Date = new Date();
const storeEntry = store.index.find(([key, dateString]) => {
const date = new Date(dateString);
const [y, m, d] = [today.getFullYear(), today.getMonth(), today.getDay()];
return (date.getFullYear() === y && date.getMonth() === m && date.getDay() == d);
});
return storeEntry;
};

26
src/types.ts Normal file
View File

@ -0,0 +1,26 @@
export type DateString = string;
export type VersionedStore = {
1: {
index: Array<[number, DateString]>,
entries: {[ key: number ]: StoreEntry }
version: 0,
},
}
export type Store = VersionedStore[1];
export type ItemType = ({
date: DateString,
variant: "eats" | "allergy",
} & ({
variant: "eats",
item: string
} | {
variant: "allergy"
}));
export type StoreEntry = {
items: Array<ItemType>
}
export default {};