diff --git a/packages/tx-ui1/Dockerfile b/packages/tx-ui1/Dockerfile new file mode 100644 index 0000000..89e0521 --- /dev/null +++ b/packages/tx-ui1/Dockerfile @@ -0,0 +1,8 @@ +FROM node:18-alpine +WORKDIR /usr/src/app +COPY package*.json ./ +RUN yarn install --production --frozen-lockfile +COPY . . +EXPOSE 8080 +ENV NODE_ENV=production +CMD ["node", "server/server.js"] \ No newline at end of file diff --git a/packages/tx-ui1/package.json b/packages/tx-ui1/package.json new file mode 100644 index 0000000..182f2a4 --- /dev/null +++ b/packages/tx-ui1/package.json @@ -0,0 +1,33 @@ +{ + "scripts": { + "start": "node server/server.js" + }, + "dependencies": {}, + "type": "module", + "devDependencies": { + "@agoric/eslint-config": "^0.3.3", + "@agoric/eslint-plugin": "^0.2.3", + "@endo/eslint-config": "^0.5.1", + "@types/better-sqlite3": "^7.6.1", + "@types/node": "^14.14.7", + "@typescript-eslint/parser": "^4.21.0", + "eslint": "^7.24.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-config-prettier": "^8.1.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsdoc": "^32.3.0", + "eslint-plugin-prettier": "^3.3.1", + "fuse.js": "^6.6.2", + "typescript": "^4.8.4" + }, + "eslintConfig": { + "extends": [ + "@agoric" + ] + }, + "prettier": { + "trailingComma": "all", + "arrowParens": "avoid", + "singleQuote": true + } +} diff --git a/packages/tx-ui1/server/server.js b/packages/tx-ui1/server/server.js new file mode 100644 index 0000000..c2ea57f --- /dev/null +++ b/packages/tx-ui1/server/server.js @@ -0,0 +1,38 @@ +// @ts-check + +const pages = { + // package-relative + '/': { path: './ui/index.html', type: 'text/html' }, + '/txUi.js': { path: './ui/txUi.js', type: 'application/javascript' }, + '/report.css': { path: './ui/report.css', type: 'text/css' }, + '/txs': { path: './data/split_detail.json', type: 'application/json' }, +}; + +/** + * @param {object} io + * @param {typeof import('http')} io.http + * @param {typeof import('fs')} io.fs TODO: narrow to least authority + */ +const main = async ({ http, fs }) => { + const srv = http.createServer((request, response) => { + const { url } = request; + console.log('@@request for', url); + const page = pages[url]; + if (!page) { + response.writeHead(404, { 'content-type': 'text/plain' }); + response.end(); + return; + } + fs.promises.readFile(page.path, 'utf8').then(body => { + response.writeHead(200, { 'content-type': page.type }); + response.write(body); + response.end(); + }); + }); + srv.listen(8080); +}; + +(async () => + main({ http: await import('http'), fs: await import('fs') }))().catch(err => + console.error(err), +); diff --git a/packages/tx-ui1/ui/index.html b/packages/tx-ui1/ui/index.html new file mode 100644 index 0000000..a25c9bd --- /dev/null +++ b/packages/tx-ui1/ui/index.html @@ -0,0 +1,23 @@ + + Transaction Search + + + + + + + diff --git a/packages/tx-ui1/ui/report.css b/packages/tx-ui1/ui/report.css new file mode 100644 index 0000000..43a1e83 --- /dev/null +++ b/packages/tx-ui1/ui/report.css @@ -0,0 +1,49 @@ +.report { + border-collapse: collapse; + font-family: sans-serif; +} +th, +td { + border: 1px solid black; + padding: 4px; +} +.report tr:nth-child(odd) { + background-color: #fff; +} + +.report tr:nth-child(even) { + background-color: #eee; +} + +.report td.amount { + text-align: right; +} +.report td.amount:before { + content: '$'; +} + +.report td.status { + text-align: center; +} +.report td.status span { + visibility: hidden; +} +.report td.uncleared:before { + content: '?'; +} +.report td.cleared:before { + content: '✓'; +} +.report td.recurring:before { + content: '♲'; + color: green; +} +.report td.recurring_suggested:before { + content: '♲?'; + color: grey; +} + +.report td.uncleared { + font-style: italic; + color: red; +} diff --git a/packages/tx-ui1/ui/txUi.js b/packages/tx-ui1/ui/txUi.js new file mode 100644 index 0000000..d05df94 --- /dev/null +++ b/packages/tx-ui1/ui/txUi.js @@ -0,0 +1,132 @@ +// @ts-check +import { + html, + useEffect, + useState, +} from 'https://unpkg.com/htm/preact/standalone.module.js'; +import Fuse from 'https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.esm.js'; + +console.log('@@txUi module'); + +/** + * @param {object} io + * @param { typeof document } io.document + * @param { typeof fetch } io.fetch + * @param { typeof localStorage } io.localStorage + * @param { typeof import('fuse.js').default } io.Fuse + */ +export const main = ({ fetch, document, localStorage, Fuse }) => { + console.log('@@main'); + document.addEventListener('DOMContentLoaded', ev => { + console.log('content ready@@'); + + fetch('/txs').then(resp => { + if (resp.status !== 200) { + throw resp; + } + resp.text().then(txt => { + console.log('got text@@', txt.slice(0, 20)); + const lines = txt.split('\n').filter(line => line > ''); + console.log(lines[0]); + }); + }); + }); +}; + +const opts = { + keys: [ + 'date', + 'num', + 'description', + { name: 'memo', getFn: tx => tx.splits.map(s => s.memo).join(' ') }, + { name: 'value', getFn: tx => tx.splits.map(s => `${s.value}`).join(' ') }, + ], + threshold: 0.3, + minMatchCharLength: 3, +}; + +export const TxRegister = () => { + const [items, setItems] = useState([]); + const [fuse, setFuse] = useState(new Fuse([], opts)); + const [pattern, setPattern] = useState(''); + const [matches, setMatches] = useState([]); + + console.warn('AMBIENT: window.localStorage'); + navigator.storage.estimate().then(est => console.log(est)); + const { localStorage } = window; + + useEffect(() => { + const found = localStorage.getItem('/txs'); + if (!found) return; + const lines = found.split('\n').filter(line => line > ''); + setItems(lines); + }); + + const load = async () => { + const resp = await fetch('/txs'); + if (resp.status !== 200) { + throw resp; + } + const txt = await resp.text(); + console.log('got text@@', txt.length, txt.slice(0, 20)); + const lines = txt.split('\n').filter(line => line > ''); + console.log(lines[0]); + const txs = lines.map(line => JSON.parse(line)); + setItems(txs); + setFuse(new Fuse(txs, opts)); + localStorage.setItem('/txs', txt); + }; + + const search = () => { + console.log('@@search', pattern); + const hits = fuse.search(pattern); + setMatches(hits); + }; + + return html` +
e.preventDefault()}> +
+ Transaction Search + setPattern(e.target.value)} /> + +
+ +
Transactions loaded: ${items.length}
+
+
+ + + + + + + + + + + + ${matches.map(match => { + const tx = match.item; + return html` + + + + + + ${tx.splits.map(s => { + return html` + + + + + + + + `; + })} + `; + })} + +
DateNumDescriptionCodeAccountAmount
${tx.date}${tx.num}${tx.description}
${s.memo}${s.code}${s.account}${s.value}
+ `; +};