Compare commits

..

2 Commits

Author SHA1 Message Date
william 9dacd1944d Changes... 2023-01-24 22:37:57 -05:00
FyloZ f4fcae38e1
Stuff 2022-12-22 13:32:42 -05:00
29 changed files with 1180 additions and 79 deletions

12
.fleet/run.json Normal file
View File

@ -0,0 +1,12 @@
{
"configurations": [
{
"name": "Start Frontend",
"type": "command",
"program": "npm",
"args": [
"start"
]
}
]
}

0
.fleet/settings.json Normal file
View File

568
package-lock.json generated
View File

@ -15,10 +15,17 @@
"@types/node": "^16.18.10",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@types/react-router-bootstrap": "^0.24.5",
"axios": "^1.2.1",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"react": "^18.2.0",
"react-bootstrap": "^2.7.0",
"react-dom": "^18.2.0",
"react-router-bootstrap": "^0.26.2",
"react-router-dom": "^6.6.0",
"react-scripts": "5.0.1",
"sass": "^1.57.1",
"typescript": "^4.9.4",
"web-vitals": "^2.1.4"
}
@ -3072,6 +3079,65 @@
}
}
},
"node_modules/@popperjs/core": {
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.4.1.tgz",
"integrity": "sha512-NmhoilMDyIfQiOSdQgxpVH2tC2u85Y0mVijtBNbI9kcDYLEiW/r6vKYVKtkyU+C4qobXhGMPfZ70PTc0lysSVA==",
"dependencies": {
"@swc/helpers": "^0.4.14"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@remix-run/router": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.2.0.tgz",
"integrity": "sha512-GO82KYYTWPRCgdNtnheaZG3LcViUlxRFlHM7ykh7N+ufoXi6PVIHoP+9RUG/vuzl2hr9i/h6EA1Eq+2HpqJ0gQ==",
"engines": {
"node": ">=14"
}
},
"node_modules/@restart/hooks": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.7.tgz",
"integrity": "sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A==",
"dependencies": {
"dequal": "^2.0.2"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@restart/ui": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.4.1.tgz",
"integrity": "sha512-J7wFOx2DcmkBqCqiZgDsggLO7faiNh4Nv1/v80FmbRgP+MYpwaVDKKXLC69DA4+ejgNIsBP5ORtC74EZqO1j8A==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@popperjs/core": "^2.11.5",
"@react-aria/ssr": "^3.2.0",
"@restart/hooks": "^0.4.7",
"@types/warning": "^3.0.0",
"dequal": "^2.0.2",
"dom-helpers": "^5.2.0",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -3390,6 +3456,14 @@
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@swc/helpers": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz",
"integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@testing-library/dom": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.0.tgz",
@ -3746,6 +3820,11 @@
"@types/node": "*"
}
},
"node_modules/@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="
},
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@ -3857,6 +3936,42 @@
"@types/react": "*"
}
},
"node_modules/@types/react-router": {
"version": "5.1.20",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*"
}
},
"node_modules/@types/react-router-bootstrap": {
"version": "0.24.5",
"resolved": "https://registry.npmjs.org/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz",
"integrity": "sha512-GRx/8xF/skw4/Pmm6d+xbExi8gobCLOe8Eoz9kXPQGbYo7p5Wbi61tjpOF5AbfJ5XMN+fIzweToTi56odj/LOQ==",
"dependencies": {
"@types/react": "*",
"@types/react-router-dom": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@ -3923,6 +4038,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
},
"node_modules/@types/warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
"integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA=="
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@ -5164,6 +5284,29 @@
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
},
"node_modules/bootstrap": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz",
"integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"peerDependencies": {
"@popperjs/core": "^2.11.6"
}
},
"node_modules/bootstrap-icons": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz",
"integrity": "sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -5414,6 +5557,11 @@
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
"integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
},
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"node_modules/clean-css": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz",
@ -6238,6 +6386,14 @@
"node": ">= 0.8"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"engines": {
"node": ">=6"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@ -6374,6 +6530,15 @@
"utila": "~0.4"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@ -8624,6 +8789,11 @@
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.1.tgz",
"integrity": "sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -8713,6 +8883,14 @@
"node": ">= 0.4"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/ipaddr.js": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
@ -13638,6 +13816,23 @@
"react-is": "^16.13.1"
}
},
"node_modules/prop-types-extra": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
"dependencies": {
"react-is": "^16.3.2",
"warning": "^4.0.0"
},
"peerDependencies": {
"react": ">=0.14.0"
}
},
"node_modules/prop-types-extra/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -13823,6 +14018,35 @@
"node": ">=14"
}
},
"node_modules/react-bootstrap": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.7.0.tgz",
"integrity": "sha512-Jcrn6aUuRVBeSB6dzKODKZU1TONOdhAxu0IDm4Sv74SJUm98dMdhSotF2SNvFEADANoR+stV+7TK6SNX1wWu5w==",
"dependencies": {
"@babel/runtime": "^7.17.2",
"@restart/hooks": "^0.4.6",
"@restart/ui": "^1.4.1",
"@types/react-transition-group": "^4.4.4",
"classnames": "^2.3.1",
"dom-helpers": "^5.2.1",
"invariant": "^2.2.4",
"prop-types": "^15.8.1",
"prop-types-extra": "^1.1.0",
"react-transition-group": "^4.4.2",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"peerDependencies": {
"@types/react": ">=16.14.8",
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@ -13962,6 +14186,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@ -13970,6 +14199,48 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.0.tgz",
"integrity": "sha512-+VPfCIaFbkW7BAiB/2oeprxKAt1KLbl+zXZ10CXOYezKWgBmTKyh8XjI53eLqY5kd7uY+V4rh3UW44FclwUU+Q==",
"dependencies": {
"@remix-run/router": "1.2.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-bootstrap": {
"version": "0.26.2",
"resolved": "https://registry.npmjs.org/react-router-bootstrap/-/react-router-bootstrap-0.26.2.tgz",
"integrity": "sha512-YlpI9Xi+Uqp6zFAUO8D/wu6P8mr1ujqq+0V5MhJG1kx9dr/95fAMoGk4J+/CsysOkwtR3tYSac4DDWmHwXvC8w==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": ">=16.13.1",
"react-router-dom": ">=6.0.0"
}
},
"node_modules/react-router-dom": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.6.0.tgz",
"integrity": "sha512-qC4jnvpfCPKVle1mKLD75IvZLcbVJyFMlSn16WY9ZiOed3dgSmqhslCf/u3tmSccWOujkdsT/OwGq12bELmvjg==",
"dependencies": {
"@remix-run/router": "1.2.0",
"react-router": "6.6.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@ -14042,6 +14313,21 @@
}
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -14502,6 +14788,22 @@
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA=="
},
"node_modules/sass": {
"version": "1.57.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
"integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/sass-loader": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
@ -15668,6 +15970,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/uncontrollable": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@types/react": ">=16.9.11",
"invariant": "^2.2.4",
"react-lifecycles-compat": "^3.0.4"
},
"peerDependencies": {
"react": ">=15.0.0"
}
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@ -15876,6 +16192,14 @@
"makeerror": "1.0.12"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@ -18843,6 +19167,48 @@
"source-map": "^0.7.3"
}
},
"@popperjs/core": {
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
},
"@react-aria/ssr": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.4.1.tgz",
"integrity": "sha512-NmhoilMDyIfQiOSdQgxpVH2tC2u85Y0mVijtBNbI9kcDYLEiW/r6vKYVKtkyU+C4qobXhGMPfZ70PTc0lysSVA==",
"requires": {
"@swc/helpers": "^0.4.14"
}
},
"@remix-run/router": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.2.0.tgz",
"integrity": "sha512-GO82KYYTWPRCgdNtnheaZG3LcViUlxRFlHM7ykh7N+ufoXi6PVIHoP+9RUG/vuzl2hr9i/h6EA1Eq+2HpqJ0gQ=="
},
"@restart/hooks": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.7.tgz",
"integrity": "sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A==",
"requires": {
"dequal": "^2.0.2"
}
},
"@restart/ui": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.4.1.tgz",
"integrity": "sha512-J7wFOx2DcmkBqCqiZgDsggLO7faiNh4Nv1/v80FmbRgP+MYpwaVDKKXLC69DA4+ejgNIsBP5ORtC74EZqO1j8A==",
"requires": {
"@babel/runtime": "^7.18.3",
"@popperjs/core": "^2.11.5",
"@react-aria/ssr": "^3.2.0",
"@restart/hooks": "^0.4.7",
"@types/warning": "^3.0.0",
"dequal": "^2.0.2",
"dom-helpers": "^5.2.0",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
}
},
"@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -19037,6 +19403,14 @@
"loader-utils": "^2.0.0"
}
},
"@swc/helpers": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz",
"integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==",
"requires": {
"tslib": "^2.4.0"
}
},
"@testing-library/dom": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.0.tgz",
@ -19330,6 +19704,11 @@
"@types/node": "*"
}
},
"@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="
},
"@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@ -19441,6 +19820,42 @@
"@types/react": "*"
}
},
"@types/react-router": {
"version": "5.1.20",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
"requires": {
"@types/history": "^4.7.11",
"@types/react": "*"
}
},
"@types/react-router-bootstrap": {
"version": "0.24.5",
"resolved": "https://registry.npmjs.org/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz",
"integrity": "sha512-GRx/8xF/skw4/Pmm6d+xbExi8gobCLOe8Eoz9kXPQGbYo7p5Wbi61tjpOF5AbfJ5XMN+fIzweToTi56odj/LOQ==",
"requires": {
"@types/react": "*",
"@types/react-router-dom": "*"
}
},
"@types/react-router-dom": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
"requires": {
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router": "*"
}
},
"@types/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
"requires": {
"@types/react": "*"
}
},
"@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@ -19507,6 +19922,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
},
"@types/warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
"integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA=="
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@ -20428,6 +20848,17 @@
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
},
"bootstrap": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz",
"integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==",
"requires": {}
},
"bootstrap-icons": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz",
"integrity": "sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -20598,6 +21029,11 @@
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
"integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
},
"classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"clean-css": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz",
@ -21195,6 +21631,11 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="
},
"destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@ -21301,6 +21742,15 @@
"utila": "~0.4"
}
},
"dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@ -22941,6 +23391,11 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz",
"integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ=="
},
"immutable": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.1.tgz",
"integrity": "sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ=="
},
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -23005,6 +23460,14 @@
"side-channel": "^1.0.4"
}
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"ipaddr.js": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
@ -26373,6 +26836,22 @@
}
}
},
"prop-types-extra": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
"requires": {
"react-is": "^16.3.2",
"warning": "^4.0.0"
},
"dependencies": {
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}
}
},
"proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -26500,6 +26979,25 @@
"whatwg-fetch": "^3.6.2"
}
},
"react-bootstrap": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.7.0.tgz",
"integrity": "sha512-Jcrn6aUuRVBeSB6dzKODKZU1TONOdhAxu0IDm4Sv74SJUm98dMdhSotF2SNvFEADANoR+stV+7TK6SNX1wWu5w==",
"requires": {
"@babel/runtime": "^7.17.2",
"@restart/hooks": "^0.4.6",
"@restart/ui": "^1.4.1",
"@types/react-transition-group": "^4.4.4",
"classnames": "^2.3.1",
"dom-helpers": "^5.2.1",
"invariant": "^2.2.4",
"prop-types": "^15.8.1",
"prop-types-extra": "^1.1.0",
"react-transition-group": "^4.4.2",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
}
},
"react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@ -26605,11 +27103,41 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
},
"react-router": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.0.tgz",
"integrity": "sha512-+VPfCIaFbkW7BAiB/2oeprxKAt1KLbl+zXZ10CXOYezKWgBmTKyh8XjI53eLqY5kd7uY+V4rh3UW44FclwUU+Q==",
"requires": {
"@remix-run/router": "1.2.0"
}
},
"react-router-bootstrap": {
"version": "0.26.2",
"resolved": "https://registry.npmjs.org/react-router-bootstrap/-/react-router-bootstrap-0.26.2.tgz",
"integrity": "sha512-YlpI9Xi+Uqp6zFAUO8D/wu6P8mr1ujqq+0V5MhJG1kx9dr/95fAMoGk4J+/CsysOkwtR3tYSac4DDWmHwXvC8w==",
"requires": {
"prop-types": "^15.7.2"
}
},
"react-router-dom": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.6.0.tgz",
"integrity": "sha512-qC4jnvpfCPKVle1mKLD75IvZLcbVJyFMlSn16WY9ZiOed3dgSmqhslCf/u3tmSccWOujkdsT/OwGq12bELmvjg==",
"requires": {
"@remix-run/router": "1.2.0",
"react-router": "6.6.0"
}
},
"react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@ -26665,6 +27193,17 @@
"workbox-webpack-plugin": "^6.4.1"
}
},
"react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -26983,6 +27522,16 @@
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA=="
},
"sass": {
"version": "1.57.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
"integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
}
},
"sass-loader": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
@ -27862,6 +28411,17 @@
"which-boxed-primitive": "^1.0.2"
}
},
"uncontrollable": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
"requires": {
"@babel/runtime": "^7.6.3",
"@types/react": ">=16.9.11",
"invariant": "^2.2.4",
"react-lifecycles-compat": "^3.0.4"
}
},
"unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@ -28010,6 +28570,14 @@
"makeerror": "1.0.12"
}
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",

View File

@ -10,10 +10,17 @@
"@types/node": "^16.18.10",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@types/react-router-bootstrap": "^0.24.5",
"axios": "^1.2.1",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"react": "^18.2.0",
"react-bootstrap": "^2.7.0",
"react-dom": "^18.2.0",
"react-router-bootstrap": "^0.26.2",
"react-router-dom": "^6.6.0",
"react-scripts": "5.0.1",
"sass": "^1.57.1",
"typescript": "^4.9.4",
"web-vitals": "^2.1.4"
},

View File

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -1,26 +1,23 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
import React, {useEffect, useState} from 'react';
import TrackDisplay from './components/TrackDisplay';
import api from "./api";
import Track from './model/track.model';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
const App: React.FC = () => {
const [tracks, setTracks] = useState(new Array<Track>());
useEffect(() => {
api.get("/tracks/")
.then(response => setTracks(response.data));
}, []);
return (
<div className="App">
{tracks.map(t => (
<TrackDisplay key={t.id} track={t}/>
))}
</div>
);
}
export default App;

8
src/api.ts Normal file
View File

@ -0,0 +1,8 @@
import axios from "axios";
const api = axios.create({
baseURL: "http://localhost:8080/api/v1",
withCredentials: true
});
export default api;

View File

@ -0,0 +1,43 @@
import React, {FC} from "react";
import ExternalTrack from "../model/externalTrack.model";
import Icon from "./Icon";
const ExternalTrackCard: FC<ExternalTrackCardProps> = ({
track,
source,
imported,
previewing,
onImportClick,
onPreviewClick,
onSpotifyIconClick
}) => (
<div className="external-track-card">
<img className="external-track-thumbnail" src={track.thumbnailUrl} alt={'Thumbnail - ' + track.name}/>
<div className="external-track-description">
<p className="external-track-title" onClick={onSpotifyIconClick}
title={'Open on ' + source}>{track.name}</p>
<p className="external-track-subtitle">{track.authors.join(', ') + ' - ' + track.albumName}</p>
</div>
<div className="external-track-actions">
{previewing ?
<Icon name="pause" hover="pause-fill" onClick={onPreviewClick} title="Stop preview"/> :
<Icon name="play" hover="play-fill" onClick={onPreviewClick} title="Preview track"/>}
{imported ?
<Icon name="check" hover="check-circle-fill" onClick={onImportClick} title="Remove from library"/> :
<Icon name="plus" hover="plus-circle-fill" onClick={onImportClick} title="Import in library"/>}
</div>
</div>
);
interface ExternalTrackCardProps {
track: ExternalTrack,
source: string,
imported: boolean,
previewing: boolean,
onImportClick: () => void,
onPreviewClick: () => void,
onSpotifyIconClick: () => void
}
export default ExternalTrackCard;

35
src/components/Icon.tsx Normal file
View File

@ -0,0 +1,35 @@
import {FC, useEffect, useState} from "react";
const Icon: FC<IconProps> = ({name, hover, title, onClick}) => {
const [hovering, setHovering] = useState(false);
const [className, setClassName] = useState("bi bi-" + name);
useEffect(() => {
let icon;
if (hover && hovering) {
icon = hover;
} else {
icon = name;
}
setClassName("bi bi-" + icon);
}, [hovering, name, hover]);
return (
<i className={className}
title={title}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
onClick={onClick}></i>
);
};
interface IconProps {
name: string
hover?: string
title?: string
onClick?: () => void
}
export default Icon;

27
src/components/NavTab.tsx Normal file
View File

@ -0,0 +1,27 @@
import React, {FC} from "react";
import {Nav} from "react-bootstrap";
import {Link} from "react-router-dom";
const NavTab: FC<NavTabProps> = ({eventKey, label, icon, activeIcon, activeKey, onTabClick}) => {
const isActive = () => activeKey === eventKey;
return (
<Nav.Link as={Link} eventKey={eventKey} to={'/' + eventKey} onClick={() => onTabClick(eventKey)}>
{!isActive() || activeIcon == null ?
<i className={'bi bi-' + icon}></i> :
<i className={'bi bi-' + activeIcon}></i>}
<span>{label}</span>
</Nav.Link>
);
};
interface NavTabProps {
eventKey: string,
label: string,
icon: string,
activeIcon?: string,
activeKey: string,
onTabClick: (key: string) => void
}
export default NavTab;

View File

@ -0,0 +1,29 @@
import {FC, FormEvent, useState} from "react";
const SearchSongForm: FC<SearchSongFormProps> = ({submitSearchHandler}) => {
const [searchQuery, setSearchQuery] = useState("");
const submitFormHandler = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
submitSearchHandler(searchQuery);
};
return (
<form onSubmit={submitFormHandler}>
<input
name="query"
type="string"
placeholder="Search query"
required
value={searchQuery}
onChange={e => setSearchQuery(e.currentTarget.value)}/>
<button>Search</button>
</form>
);
};
interface SearchSongFormProps {
submitSearchHandler: (query: string) => void
}
export default SearchSongForm;

View File

@ -0,0 +1,16 @@
import React from "react";
import Track from "../model/track.model";
const TrackDisplay: React.FC<TrackDisplayProps> = ({track}) => {
return (
<div>
<p>{track.name} - {track.authors.join(" and ")}</p>
</div>
);
};
export interface TrackDisplayProps {
track: Track
}
export default TrackDisplay;

View File

@ -0,0 +1,10 @@
import React from "react";
import App from "../App";
const MainContainer: React.FC = () => {
return (
<App/>
);
};
export default MainContainer;

View File

@ -0,0 +1,28 @@
import {FC, useState} from "react";
import {Container, Nav, Navbar} from "react-bootstrap";
import NavTab from "../components/NavTab";
import "../style/navbar.scss";
const NavbarContainer: FC = () => {
const [activeKey, setActiveKey] = useState("library");
return (
<Navbar bg="dark">
<Container>
<Nav className="me-auto" activeKey={activeKey}>
<NavTab eventKey="library" label="Library" icon="vinyl" activeIcon="vinyl-fill"
activeKey={activeKey} onTabClick={setActiveKey}/>
<NavTab eventKey="search" label="Search" icon="search" activeKey={activeKey}
onTabClick={setActiveKey}/>
<NavTab eventKey="playlists" label="Playlists" icon="bookmarks" activeIcon="bookmarks-fill"
activeKey={activeKey} onTabClick={setActiveKey}/>
<NavTab eventKey="spotify-login" label="Spotify Login" icon="spotify" activeIcon="spotify"
activeKey={activeKey}
onTabClick={() => window.open("http://localhost:8080/module/spotify/login", "_blank")}/>
</Nav>
</Container>
</Navbar>
);
};
export default NavbarContainer;

View File

@ -0,0 +1,72 @@
import React, {useEffect, useState} from "react";
import SearchSongForm from "../components/SearchSongForm";
import api from "../api";
import SearchResultContainer from "./SearchResult.container";
import ExternalTrack from "../model/externalTrack.model";
const audio = new Audio();
const SearchContainer: React.FC = () => {
const [results, setResults] = useState({});
const [previewTrackId, setPreviewTrackId] = useState("");
const [importedTracksIds, setImportedTracksIds] = useState(new Array<string>());
const submitSearchHandler = async (query: string) => {
const response = await api.get("/tracks/search?q=" + query, {withCredentials: true});
setResults(response.data);
};
const onPreviewClick = (trackId: string, previewUrl: string) => {
audio.pause();
if (trackId !== previewTrackId) {
audio.src = previewUrl;
audio.play();
setPreviewTrackId(trackId);
} else {
setPreviewTrackId("");
}
};
const onImportClick = async (trackId: string) => {
if (importedTracksIds.some(id => trackId === id)) {
await api.delete(`/tracks/trackId/spotify/${trackId}`);
setImportedTracksIds(importedTracksIds.filter(id => trackId !== id));
} else {
await api.post("/tracks/", {source: "spotify", trackId}, {withCredentials: true});
setImportedTracksIds([...importedTracksIds, trackId]);
}
};
const onSpotifyIconClick = (trackId: string) => {
const spotifyTrackUrl = "https://open.spotify.com/track/" + trackId;
window.open(spotifyTrackUrl, "_blank");
};
useEffect(() => {
const getLibraryTrackIds = async () => {
const response = await api.get("/tracks/trackIds/");
if (response.data.spotify) {
setImportedTracksIds(response.data.spotify);
}
};
getLibraryTrackIds();
}, []);
return (
<>
<SearchSongForm submitSearchHandler={submitSearchHandler}/>
{Object.entries(results).map(([source, tracks]) => (
<SearchResultContainer key={source} source={source} importedTracksIds={importedTracksIds}
previewTrackId={previewTrackId}
tracks={tracks as ExternalTrack[]}
onImportClick={onImportClick}
onPreviewClick={onPreviewClick}
onSpotifyIconClick={onSpotifyIconClick}/>
))}
</>
)
}
export default SearchContainer;

View File

@ -0,0 +1,44 @@
import {FC} from "react";
import ExternalTrackCard from "../components/ExternalTrackCard";
import ExternalTrack from "../model/externalTrack.model";
import SpotifyLogo from "../images/Spotify_Logo_RGB_White.png";
import "../style/search-results.scss";
const SearchResultContainer: FC<SearchResultContainerProps> = ({
source,
importedTracksIds,
previewTrackId,
tracks,
onImportClick,
onPreviewClick,
onSpotifyIconClick
}) => {
return (
<div className="d-flex flex-column">
<img className="search-results-source-logo" src={SpotifyLogo} alt="Spotify logo"/>
<div className="search-results-wrapper d-flex flex-row">
{tracks.map(s => (
<ExternalTrackCard key={s.trackId} track={s}
source={source}
imported={importedTracksIds.some(id => s.trackId === id)}
previewing={previewTrackId === s.trackId}
onImportClick={() => onImportClick(s.trackId)}
onPreviewClick={() => onPreviewClick(s.trackId, s.previewUrl)}
onSpotifyIconClick={() => onSpotifyIconClick(s.trackId)}/>
))}
</div>
</div>
);
};
interface SearchResultContainerProps {
source: string,
importedTracksIds: string[],
previewTrackId: string,
tracks: ExternalTrack[],
onImportClick: (trackId: string) => void,
onPreviewClick: (trackId: string, previewUrl: string) => void,
onSpotifyIconClick: (trackId: string) => void
}
export default SearchResultContainer;

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,16 +1,33 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {BrowserRouter, Route, Routes} from 'react-router-dom';
import './style/index.scss';
import NavbarContainer from "./containers/Navbar.container";
import SearchContainer from "./containers/Search.container";
import MainContainer from './containers/Main.container';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
<React.StrictMode>
<BrowserRouter>
<div className="d-flex flex-row">
<div className="sidebar">
<NavbarContainer/>
</div>
<div className="p-3">
<Routes>
<Route path="/" element={<MainContainer/>}/>
<Route path="/library" element={<MainContainer/>}/>
<Route path="/search" element={<SearchContainer/>}/>
</Routes>
</div>
</div>
</BrowserRouter>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function

View File

@ -0,0 +1,41 @@
export default class ExternalTrack {
private readonly _trackId: string;
private readonly _name: string;
private readonly _albumName: string;
private readonly _authors: string[];
private readonly _thumbnailUrl: string;
private readonly _previewUrl: string;
public constructor(trackId: string, name: string, albumName: string, authors: string[], thumbnailUrl: string, previewUrl: string) {
this._trackId = trackId;
this._name = name;
this._albumName = albumName;
this._authors = authors;
this._thumbnailUrl = thumbnailUrl;
this._previewUrl = previewUrl;
}
public get trackId(): string {
return this._trackId;
}
public get name(): string {
return this._name;
}
public get albumName(): string {
return this._albumName;
}
public get authors(): string[] {
return this._authors;
}
public get thumbnailUrl(): string {
return this._thumbnailUrl;
}
public get previewUrl(): string {
return this._previewUrl;
}
}

29
src/model/track.model.ts Normal file
View File

@ -0,0 +1,29 @@
export default class Track {
private readonly _id: string;
private readonly _trackId: string;
private readonly _name: string;
private readonly _authors: string[];
public constructor(id: string, trackId: string, name: string, authors: string[]) {
this._id = id;
this._trackId = trackId;
this._name = name;
this._authors = authors;
}
public get id(): string {
return this._id;
}
public get trackId(): string {
return this._trackId;
}
public get name(): string {
return this._name;
}
public get authors(): string[] {
return this._authors;
}
}

16
src/router.tsx Normal file
View File

@ -0,0 +1,16 @@
import SearchContainer from "./containers/Search.container";
import {createBrowserRouter} from "react-router-dom";
import MainContainer from "./containers/Main.container";
const router = createBrowserRouter([
{
path: "/",
element: <MainContainer/>
},
{
path: "/search",
element: <SearchContainer/>
}
]);
export default router;

32
src/style/_theme.scss Normal file
View File

@ -0,0 +1,32 @@
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
/* https://coolors.co/fc8607-8d99ae-edf2f4-29262c-1f1c21 */
$color-primary: #fc8607;
$color-background: #191919;
$color-background-2: darken($color-background, 5%);
$font-color-background: white;
$body-bg: $color-background !default;
$body-color: white;
$navbar-width: 15rem;
$theme-colors: (
"primary": $color-primary,
"secondary": #6c757d,
"success": #28a745,
"danger": #dc3545,
"warning": #ffc107,
"info": #17a2b8,
"light": #f8f9fa,
"dark": $color-background-2
);
//$theme-colors-rgb: map-loop($theme-colors, to-rgb, "$value");
//$utilities-colors: map-merge($utilities-colors, $theme-colors-rgb);
//$utilities-text-colors: map-loop($utilities-colors, rgba-css-var, "$key", "text");
//$utilities-bg-colors: map-loop($utilities-colors, rgba-css-var, "$key", "bg");
@import "~bootstrap/scss/bootstrap";

28
src/style/index.scss Normal file
View File

@ -0,0 +1,28 @@
@import "~bootstrap/scss/bootstrap";
@import "~bootstrap-icons/font/bootstrap-icons";
@import "theme";
body {
margin: 0;
padding: 0;
// font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
// 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
// sans-serif;
font-family: 'Helvetica Neue', sans-serif;
background-color: $color-background !important;
color: $font-color-background;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.sidebar {
background-color: $color-background-2;
min-width: 15rem;
height: 100vh;
}

34
src/style/navbar.scss Normal file
View File

@ -0,0 +1,34 @@
@import "theme";
nav {
background-color: $color-background-2;
width: $navbar-width;
.navbar-nav {
width: $navbar-width;
flex-direction: column !important;
color: white;
}
.nav-link {
color: rgba(255, 255, 255, 0.7);
font-weight: bold;
font-size: 1.2em;
display: flex;
flex-direction: row;
align-items: center;
i, span {
color: rgba(255, 255, 255, 0.7);
margin-right: 1em;
}
&:hover i, &:hover span, &.active span {
color: white;
}
&.active i {
color: $color-primary !important;
}
}
}

View File

@ -0,0 +1,59 @@
@import "theme";
$thumbnail-size: 15em;
$wrapper-margin: 1em;
$wrapper-width: calc(100vw - $navbar-width - (2 * $wrapper-margin));
.search-results-wrapper {
overflow-x: scroll;
width: $wrapper-width;
}
.search-results-source-logo {
width: 10em;
margin-top: $wrapper-margin;
margin-left: $wrapper-margin;
}
.external-track-card {
width: $thumbnail-size;
margin: 1em;
img.external-track-thumbnail {
width: $thumbnail-size;
height: $thumbnail-size;
margin-bottom: .5em;
}
.external-track-description {
p {
margin: 0;
}
.external-track-title {
font-weight: bold;
&:hover {
text-decoration: underline;
}
.bi {
margin-left: .25em;
}
}
.external-track-subtitle {
color: rgba(255, 255, 255, 0.7);
line-height: 1.2em;
}
}
.external-track-actions {
font-size: 1.2em;
margin-bottom: 1em;
}
.bi, .external-track-title {
cursor: pointer;
}
}