First commit
Add build directory Remove build folder contents Ignore build folder changes
This commit is contained in:
commit
eb6363e121
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
build
|
4
build.sh
Normal file
4
build.sh
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
npm run build;
|
||||||
|
cp -r dist build
|
||||||
|
cp index.html build
|
||||||
|
cp styles.css build
|
0
build/.keep
Normal file
0
build/.keep
Normal file
0
dist/.keep
vendored
Normal file
0
dist/.keep
vendored
Normal file
12
index.html
Normal file
12
index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>REX</title>
|
||||||
|
<link rel="stylesheet" href="./styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="/dist/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
559
package-lock.json
generated
Normal file
559
package-lock.json
generated
Normal file
@ -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=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
package.json
Normal file
22
package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
83
src/app.jsx
Normal file
83
src/app.jsx
Normal file
@ -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 = '<div id="app"></div>';
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
|
25
src/components/app_layout.jsx
Normal file
25
src/components/app_layout.jsx
Normal file
@ -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(
|
||||||
|
<div className="grid">
|
||||||
|
<div className="left-sidebar">
|
||||||
|
<OrderBook buyLimits={props.orderBook.buy} sellLimits={props.orderBook.sell}/>
|
||||||
|
</div>
|
||||||
|
<div className="center-top">
|
||||||
|
<PriceChart labels={props.prices.labels} data={props.prices.prices}/>
|
||||||
|
</div>
|
||||||
|
<div className="center-bottom">
|
||||||
|
<OrderForm />
|
||||||
|
<OrderView orders={props.orders}/>
|
||||||
|
</div>
|
||||||
|
<div className="right-sidebar">
|
||||||
|
<TradeList trades={props.trades}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
51
src/components/order_book.jsx
Normal file
51
src/components/order_book.jsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
export default function OrderBook(props) {
|
||||||
|
const buyLimits = props.buyLimits;
|
||||||
|
const sellLimits = props.sellLimits;
|
||||||
|
return(
|
||||||
|
<article>
|
||||||
|
<h1>Order book</h1>
|
||||||
|
<section>
|
||||||
|
<h1>Buy</h1>
|
||||||
|
<table className="buy-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{buyLimits.map((limit) => {
|
||||||
|
return (
|
||||||
|
<tr key={limit.price}>
|
||||||
|
<td class="text-end">{limit.price}</td>
|
||||||
|
<td class="text-end">{limit.quantity}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h1>Sell</h1>
|
||||||
|
<table className="buy-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sellLimits.map((limit) => {
|
||||||
|
return (
|
||||||
|
<tr key={limit.price}>
|
||||||
|
<td class="text-end">{limit.price}</td>
|
||||||
|
<td class="text-end">{limit.quantity}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
43
src/components/order_form.jsx
Normal file
43
src/components/order_form.jsx
Normal file
@ -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(
|
||||||
|
<form class="order-form" onSubmit={submitCallback}>
|
||||||
|
<div className="row">
|
||||||
|
<div className="field-set">
|
||||||
|
<label htmlFor="price-input">Price</label>
|
||||||
|
<input required id="price-input" name="price" type="number"/>
|
||||||
|
</div>
|
||||||
|
<div className="field-set">
|
||||||
|
<label htmlFor="quantity-input">Quantity</label>
|
||||||
|
<input required id="quantity-input" name="quantity" min="0" type="number"/>
|
||||||
|
</div>
|
||||||
|
<div className="field-set">
|
||||||
|
<label htmlFor="side-input">Side</label>
|
||||||
|
<select name="side" id="side-input">
|
||||||
|
<option value="buy">BUY</option>
|
||||||
|
<option value="sell">SELL</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="field-set">
|
||||||
|
<input type="submit" value="Create Order"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
39
src/components/order_view.jsx
Normal file
39
src/components/order_view.jsx
Normal file
@ -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(
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Limit</th>
|
||||||
|
<th>Side</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Remaining Quantity</th>
|
||||||
|
<th>Cancel</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{orders.map((order) => {
|
||||||
|
return(
|
||||||
|
<tr key={order.id}>
|
||||||
|
<td class="text-end">{order.price}</td>
|
||||||
|
<td class="text-center">{order.side}</td>
|
||||||
|
<td class="text-end">{order.quantity}</td>
|
||||||
|
<td class="text-end">{order.remaining_quantity}</td>
|
||||||
|
<td><button onClick={(event) => { cancelOrder(event, order.id) }}>Cancel</button></td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
}
|
54
src/components/price_chart.jsx
Normal file
54
src/components/price_chart.jsx
Normal file
@ -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(
|
||||||
|
<div style={{ position: "relative", height: "100%" }}>
|
||||||
|
<Line options={options} data={data} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
25
src/components/trade_list.jsx
Normal file
25
src/components/trade_list.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export default function TradeList(props) {
|
||||||
|
return(
|
||||||
|
<article>
|
||||||
|
<h1>Trades</h1>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Price</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{props.trades.toReversed().map((trade) => {
|
||||||
|
return(
|
||||||
|
<tr key={trade.id}>
|
||||||
|
<td class="text-end">{trade.quantity}</td>
|
||||||
|
<td class="text-end">{trade.price} €</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
37
src/order_book.js
Normal file
37
src/order_book.js
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
31
src/order_store.js
Normal file
31
src/order_store.js
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
src/price_store.js
Normal file
30
src/price_store.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
18
src/trade_store.js
Normal file
18
src/trade_store.js
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
src/user_id_store.js
Normal file
11
src/user_id_store.js
Normal file
@ -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;
|
||||||
|
}
|
140
src/ws.js
Normal file
140
src/ws.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
104
styles.css
Normal file
104
styles.css
Normal file
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user