From eb6363e12104c83f3e82a9dc5d0c83bd48ffaaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Sun, 18 Feb 2024 21:40:00 +0100 Subject: [PATCH] First commit Add build directory Remove build folder contents Ignore build folder changes --- .gitignore | 3 + build.sh | 4 + build/.keep | 0 dist/.keep | 0 index.html | 12 + package-lock.json | 559 +++++++++++++++++++++++++++++++++ package.json | 22 ++ src/app.jsx | 83 +++++ src/components/app_layout.jsx | 25 ++ src/components/order_book.jsx | 51 +++ src/components/order_form.jsx | 43 +++ src/components/order_view.jsx | 39 +++ src/components/price_chart.jsx | 54 ++++ src/components/trade_list.jsx | 25 ++ src/order_book.js | 37 +++ src/order_store.js | 31 ++ src/price_store.js | 30 ++ src/trade_store.js | 18 ++ src/user_id_store.js | 11 + src/ws.js | 140 +++++++++ styles.css | 104 ++++++ 21 files changed, 1291 insertions(+) create mode 100644 .gitignore create mode 100644 build.sh create mode 100644 build/.keep create mode 100644 dist/.keep create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/app.jsx create mode 100644 src/components/app_layout.jsx create mode 100644 src/components/order_book.jsx create mode 100644 src/components/order_form.jsx create mode 100644 src/components/order_view.jsx create mode 100644 src/components/price_chart.jsx create mode 100644 src/components/trade_list.jsx create mode 100644 src/order_book.js create mode 100644 src/order_store.js create mode 100644 src/price_store.js create mode 100644 src/trade_store.js create mode 100644 src/user_id_store.js create mode 100644 src/ws.js create mode 100644 styles.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..63520da --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +build diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..6715948 --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +npm run build; +cp -r dist build +cp index.html build +cp styles.css build diff --git a/build/.keep b/build/.keep new file mode 100644 index 0000000..e69de29 diff --git a/dist/.keep b/dist/.keep new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..3c50b6b --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + REX + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bb99467 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,559 @@ +{ + "name": "ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chart.js": "^4.4.1", + "handlebars": "^4.7.8", + "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "esbuild": "^0.19.11" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", + "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", + "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", + "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", + "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", + "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", + "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", + "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", + "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", + "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", + "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", + "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", + "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", + "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", + "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", + "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", + "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", + "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", + "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", + "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", + "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", + "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", + "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", + "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, + "node_modules/chart.js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=7" + } + }, + "node_modules/esbuild": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", + "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.11", + "@esbuild/android-arm": "0.19.11", + "@esbuild/android-arm64": "0.19.11", + "@esbuild/android-x64": "0.19.11", + "@esbuild/darwin-arm64": "0.19.11", + "@esbuild/darwin-x64": "0.19.11", + "@esbuild/freebsd-arm64": "0.19.11", + "@esbuild/freebsd-x64": "0.19.11", + "@esbuild/linux-arm": "0.19.11", + "@esbuild/linux-arm64": "0.19.11", + "@esbuild/linux-ia32": "0.19.11", + "@esbuild/linux-loong64": "0.19.11", + "@esbuild/linux-mips64el": "0.19.11", + "@esbuild/linux-ppc64": "0.19.11", + "@esbuild/linux-riscv64": "0.19.11", + "@esbuild/linux-s390x": "0.19.11", + "@esbuild/linux-x64": "0.19.11", + "@esbuild/netbsd-x64": "0.19.11", + "@esbuild/openbsd-x64": "0.19.11", + "@esbuild/sunos-x64": "0.19.11", + "@esbuild/win32-arm64": "0.19.11", + "@esbuild/win32-ia32": "0.19.11", + "@esbuild/win32-x64": "0.19.11" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2a615f3 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "ui", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "esbuild src/app.jsx --outdir=dist --minify --sourcemap --jsx=automatic --bundle", + "server": "esbuild src/app.jsx --outdir=dist --minify --sourcemap --jsx=automatic --bundle --watch --servedir=." + }, + "author": "Tim Kächele", + "license": "AGPL-3.0-only", + "devDependencies": { + "esbuild": "^0.19.11" + }, + "dependencies": { + "chart.js": "^4.4.1", + "handlebars": "^4.7.8", + "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/src/app.jsx b/src/app.jsx new file mode 100644 index 0000000..265dee4 --- /dev/null +++ b/src/app.jsx @@ -0,0 +1,83 @@ +import { createRoot } from 'react-dom/client'; +import AppLayout from './components/app_layout'; +import WebsocketConnection from './ws'; +import { getUserId } from './user_id_store'; +import { OrderStore } from './order_store'; +import { TradeStore } from './trade_store'; +import { PriceStore } from './price_store'; +import OrderBook from './order_book'; + +let socket = new WebsocketConnection({url: "ws://localhost:8080"}) +window.socket = socket; + +socket.connect() +socket.authenticate(getUserId()) +socket.fetchOrderBook() +socket.fetchOrders() +socket.fetchTrades() + +document.body.innerHTML = '
'; + +const root = createRoot(document.getElementById('app')); + +let orderStore = new OrderStore(); +let orderBook = new OrderBook(); +let tradeStore = new TradeStore(30); +let priceStore = new PriceStore(50); + +const render = () => { + root.render(AppLayout({ + trades: tradeStore.getTrades(), + orders: orderStore.getOrders(), + orderBook: { + buy: orderBook.buyEntries(), + sell: orderBook.sellEntries(), + }, + prices: { + labels: priceStore.getLabels(), + prices: priceStore.getPrices() + } + })); +} + + + +const handleOrderCreationEvent = (event) => { + orderStore.handleOrderCreatedEvent(event.data) + render() +} + +socket.setHandler("OrderCreatedEvent", handleOrderCreationEvent) +socket.setHandler("OrderFetchEvent", handleOrderCreationEvent) + +socket.setHandler("OrderFillEvent", (event) => { + orderStore.handleOrderFillEvent(event.data) + render() +}) + +socket.setHandler("OrderCancelledEvent", (event) => { + orderStore.handleOrderCancelledEvent(event.data) + render() +}) + +const handleTradeEvent = (event) => { + priceStore.handleTradeEvent(event.data) + tradeStore.handleTradeEvent(event.data) + render() +} + +socket.setHandler("TradeEvent", handleTradeEvent) +socket.setHandler("TradeFetchEvent", handleTradeEvent) + +const orderBookUpdateHandler = (event) => { + orderBook.processUpdate(event.data) + render() +} + +socket.setHandler("OrderBookUpdateEvent", orderBookUpdateHandler) +socket.setHandler("OrderBookFetchEvent", orderBookUpdateHandler) + +render() + + + diff --git a/src/components/app_layout.jsx b/src/components/app_layout.jsx new file mode 100644 index 0000000..2b3f2c4 --- /dev/null +++ b/src/components/app_layout.jsx @@ -0,0 +1,25 @@ +import TradeList from './trade_list' +import { OrderForm } from './order_form' +import OrderBook from './order_book' +import OrderView from './order_view' +import PriceChart from './price_chart' + +export default function AppLayout(props) { + return( +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ ) +} diff --git a/src/components/order_book.jsx b/src/components/order_book.jsx new file mode 100644 index 0000000..c31d003 --- /dev/null +++ b/src/components/order_book.jsx @@ -0,0 +1,51 @@ +export default function OrderBook(props) { + const buyLimits = props.buyLimits; + const sellLimits = props.sellLimits; + return( +
+

Order book

+
+

Buy

+ + + + + + + + + {buyLimits.map((limit) => { + return ( + + + + + ); + })} + +
PriceQuantity
{limit.price}{limit.quantity}
+
+
+

Sell

+ + + + + + + + + {sellLimits.map((limit) => { + return ( + + + + + ); + })} + +
PriceQuantity
{limit.price}{limit.quantity}
+
+
+ ) +} diff --git a/src/components/order_form.jsx b/src/components/order_form.jsx new file mode 100644 index 0000000..cb49ea2 --- /dev/null +++ b/src/components/order_form.jsx @@ -0,0 +1,43 @@ +export function OrderForm(props) { + const socket = window.socket; + + const submitCallback = ((event) => { + event.preventDefault() + + const formData = new FormData(event.target); + const price = parseInt(formData.get("price")) + const quantity = parseInt(formData.get("quantity")) + const side = formData.get("side") + event.target.reset(); + socket.sendTrade({ + price, + quantity, + side + }) + }).bind(this) + + return( +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ ) +} diff --git a/src/components/order_view.jsx b/src/components/order_view.jsx new file mode 100644 index 0000000..c11b955 --- /dev/null +++ b/src/components/order_view.jsx @@ -0,0 +1,39 @@ +const fillState = (order) => { + return ((order.quantity - order.remaining_quantity) / order.quantity) * 100; +} + +export default function OrderView(props) { + const socket = window.socket; + const orders = props.orders || [] + + const cancelOrder = ((event, orderId) => { + socket.sendCancelOrder(orderId) + }).bind(this) + + return( + + + + + + + + + + + + {orders.map((order) => { + return( + + + + + + + + ) + })} + +
LimitSideQuantityRemaining QuantityCancel
{order.price}{order.side}{order.quantity}{order.remaining_quantity}
+ ) +} diff --git a/src/components/price_chart.jsx b/src/components/price_chart.jsx new file mode 100644 index 0000000..793f6cb --- /dev/null +++ b/src/components/price_chart.jsx @@ -0,0 +1,54 @@ +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + +export default function PriceChart(props) { + const options = { + responsive: true, + maintainAspectRatio: false, + animation: false, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Price History', + }, + }, + }; + + const data = { + labels: props.labels, + datasets: [ + { + label: "Price", + data: props.data, + borderColor: '#000000', + }] + } + + return( +
+ +
+ ); +} diff --git a/src/components/trade_list.jsx b/src/components/trade_list.jsx new file mode 100644 index 0000000..4e023f2 --- /dev/null +++ b/src/components/trade_list.jsx @@ -0,0 +1,25 @@ +export default function TradeList(props) { + return( +
+

Trades

+ + + + + + + + + {props.trades.toReversed().map((trade) => { + return( + + + + + ) + })} + +
QuantityPrice
{trade.quantity}{trade.price} €
+
+ ) +} diff --git a/src/order_book.js b/src/order_book.js new file mode 100644 index 0000000..f541ee3 --- /dev/null +++ b/src/order_book.js @@ -0,0 +1,37 @@ +export default class OrderBook { + constructor() { + this.buySide = new Map() + this.sellSide = new Map() + } + + processUpdate(orderbookUpdate) { + let side = null; + if(orderbookUpdate.side == "buy") { + side = this.buySide; + } else { + side = this.sellSide; + } + + if (orderbookUpdate.quantity == 0) { + side.delete(orderbookUpdate.price) + } else { + side.set(orderbookUpdate.price, orderbookUpdate) + } + } + + buyEntries() { + let keys = Array.from(this.buySide.keys()) + + return keys.sort((a,b) => { return a - b }).map((key) => { + return this.buySide.get(key) + }) + } + + sellEntries() { + let keys = Array.from(this.sellSide.keys()) + + return keys.sort((a,b) => { return a - b }).map((key) => { + return this.sellSide.get(key) + }) + } +} diff --git a/src/order_store.js b/src/order_store.js new file mode 100644 index 0000000..5429f40 --- /dev/null +++ b/src/order_store.js @@ -0,0 +1,31 @@ +export class OrderStore { + constructor() { + this.orders = new Map(); + } + + getOrders() { + return Array.from(this.orders.values()); + } + + handleOrderCreatedEvent(event) { + this.orders.set(event.id, event) + } + + handleOrderCancelledEvent(event) { + this.orders.delete(event.id) + } + + handleOrderFillEvent(event) { + let order = this.orders.get(event.id) + if (!order) { + return + } + + order.remaining_quantity = event.remaining_quantity + if (order.remaining_quantity == 0) { + this.orders.delete(event.id) + } else { + this.orders.set(event.id, order) + } + } +} diff --git a/src/price_store.js b/src/price_store.js new file mode 100644 index 0000000..344d9bc --- /dev/null +++ b/src/price_store.js @@ -0,0 +1,30 @@ +export class PriceStore { + constructor(historyRetention) { + this.historyRetention = historyRetention + this.counter = 0 + this.labels = [] + this.prices = [] + } + + handleTradeEvent(event) { + this.counter = this.counter + 1; + this.labels.push(this.counter) + this.prices.push(event.price) + + if (this.labels.length > this.historyRetention + 1) { + this.labels.shift() + } + + if (this.prices.length > this.historyRetention ) { + this.prices.shift() + } + } + + getLabels() { + return this.labels; + } + + getPrices() { + return this.prices; + } +} diff --git a/src/trade_store.js b/src/trade_store.js new file mode 100644 index 0000000..f553b87 --- /dev/null +++ b/src/trade_store.js @@ -0,0 +1,18 @@ +export class TradeStore { + constructor(maxBacklog) { + this.maxBacklog = maxBacklog + this.trades = []; + } + + getTrades() { + return this.trades + } + + handleTradeEvent(event) { + this.trades.push(event) + + if (this.trades.length > this.maxBacklog) { + this.trades.shift(); + } + } +} diff --git a/src/user_id_store.js b/src/user_id_store.js new file mode 100644 index 0000000..b8b4b3c --- /dev/null +++ b/src/user_id_store.js @@ -0,0 +1,11 @@ +export const getUserId = () => { + let storedUserId = localStorage.getItem("auth.user_id") + if (storedUserId) { + return storedUserId + } + + let userId = crypto.randomUUID(); + localStorage.setItem("auth.user_id", userId); + + return userId; +} diff --git a/src/ws.js b/src/ws.js new file mode 100644 index 0000000..13d4bb3 --- /dev/null +++ b/src/ws.js @@ -0,0 +1,140 @@ +export default class WebsocketConnection { + constructor(config) { + this.config = config + this.requestId = 0 + this.connected = false + this.handleMessage = this.handleMessage.bind(this) + this.messageQueue= [] + this.handlers = {} + } + + handleMessage(message) { + const rawData = message.data + const data = JSON.parse(rawData) + + const handler = this.handlers[data["name"]] + + if (handler !== undefined && handler !== null) { + handler(data) + } + } + + setHandler(eventName, handler) { + this.handlers[eventName] = handler + } + + connect() { + this.websocket = new WebSocket(this.config.url); + this.websocket.onopen = () => { + this.connected = true; + this.flushMessageQueue(); + } + + this.websocket.onmessage = this.handleMessage + + this.onclose = function() { + this.connected = false + } + } + + authenticate(userId) { + let authRequest = { + request_id: this.nextRequestId(), + type: "request", + name: "authenticate", + args: { + user_id: userId + }, + } + + this.sendMessage(authRequest) + } + + sendCancelOrder(orderId) { + const cancelRequest = { + type: "request", + request_id: this.nextRequestId(), + name: "order.cancel", + args: { + id: orderId + } + } + this.sendMessage(cancelRequest) + } + + sendTrade(tradeData) { + const tradeRequest = { + type: "request", + request_id: this.nextRequestId(), + name: "order.create", + args: { + price: tradeData.price, + quantity: tradeData.quantity, + side: tradeData.side + } + } + + this.sendMessage(tradeRequest); + } + + fetchOrders() { + const fetchOrdersRequest = { + type: "request", + request_id: this.nextRequestId(), + name: "orders.fetch", + args: { + } + } + + this.sendMessage(fetchOrdersRequest); + } + + fetchOrderBook() { + const fetchOrderBookRequest = { + type: "request", + request_id: this.nextRequestId(), + name: "orderbook.fetch", + args: { + } + } + + this.sendMessage(fetchOrderBookRequest); + } + + fetchTrades() { + const fetchTradesRequest = { + type: "request", + request_id: this.nextRequestId(), + name: "trades.fetch", + args: { + } + } + + this.sendMessage(fetchTradesRequest); + } + + flushMessageQueue() { + let i = 0 + while(this.messageQueue.length != 0) { + i++; + let message = this.messageQueue.shift() + this.websocket.send(message) + } + } + + sendMessage(obj) { + const serializedMessage = JSON.stringify(obj); + + if (this.connected) { + this.websocket.send(serializedMessage); + } else { + // Hold the message till we are connected again + this.messageQueue.push(serializedMessage) + } + } + + nextRequestId() { + this.requestId += 1; + return this.requestId; + } +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..366de0c --- /dev/null +++ b/styles.css @@ -0,0 +1,104 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + width: 90%; + margin: 0 auto; + padding: 0; + font-family: Helvetica, Arial, sans-serif; + font-size: 12px; +} + +/** + * Grid + */ +.grid { + display: grid; + grid-template-columns: [left] 25% [left-end center] auto [center-end right] 25% [right-end]; + grid-template-rows: [top] 50% [bottom] 50% [bottom-end]; + height: 100vh; +} + +.left-sidebar { + grid-column-start: left; + grid-column-end: span left-end; + grid-row-start: top; + grid-row-end: span bottom-end; + padding: 1rem; +} + +.right-sidebar { + grid-column-start: right; + grid-column-end: right-end; + grid-row-start: top; + grid-row-end: span bottom-end; + padding: 1rem; +} + +.center-top { + grid-column-start: center; + grid-column-end: center-end; + padding: 1rem; +} + +.center-bottom { + grid-column-start: center; + grid-column-end: center-end; + padding: 1rem; +} + +input, select, option, button { + font-size: inherit; + width: 100%; +} + +label { + display: block; + margin-bottom: 0.25rem; +} + +table { + width: 100%; + border: 1px solid black; + border-collapse: collapse; +} + +td, th { + border-bottom: 1px solid black; + border-right: 1px solid black; + padding: 0.25rem; +} + +th { + background: black; + color: white; + border: 1px solid black; +} + +.order-form { + margin-bottom: 1rem; +} + +.row { + width: 100%; + display: flex; + align-items: flex-end; +} + +.field-set { + flex-grow: 1; + margin-right: 1rem; +} + +.field-set:last-child { + margin-right: 0; +} + +.text-center { + text-align: center; +} + +.text-end { + text-align: right; +}