First commit

Add build directory

Remove build folder contents

Ignore build folder changes
This commit is contained in:
Tim Kächele 2024-02-18 21:40:00 +01:00
commit eb6363e121
21 changed files with 1291 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
build

4
build.sh Normal file
View 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
View File

0
dist/.keep vendored Normal file
View File

12
index.html Normal file
View 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
View 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
View 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
View 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()

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
);
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}