Compare commits
61 Commits
167e4831fa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8968e990ec | |||
| 94996fb45c | |||
| 29af08f79d | |||
| a6ef1c71e2 | |||
| 2f4eb05233 | |||
| 34e44d2310 | |||
| f2f096ef67 | |||
|
|
3f13bbdd19 | ||
| 1ca4235283 | |||
| 96753a774c | |||
| 625485d318 | |||
| eeae861353 | |||
| d524a831cd | |||
| 1f947e3200 | |||
| f92b7345ae | |||
| 6f9933f332 | |||
| 3936d1c651 | |||
| e173888692 | |||
| 621e414f95 | |||
| 454973b017 | |||
| 17a29376a9 | |||
| d26044ee3f | |||
| 47e8f0fc11 | |||
| f02083d44c | |||
| 66eaf91521 | |||
| 0704ea59b1 | |||
| 848ee1ccb4 | |||
| 103e3bace3 | |||
| 088252f5bc | |||
| 9b8dc8cde7 | |||
| 5cb6ef0798 | |||
| 9804bd196b | |||
| 88bdfb35d3 | |||
| 52c8ab9eea | |||
| 475c490b16 | |||
| caf853e8a9 | |||
| 3e6ddab88d | |||
| d8050f68cd | |||
| 9a515baf37 | |||
| 9ddea4d53d | |||
|
|
be9d309f4d | ||
| 8287b72e60 | |||
| 36ca8c76d3 | |||
|
|
0667d96de3 | ||
|
|
5bd0e35532 | ||
|
|
905e259af2 | ||
|
|
f17b253ac6 | ||
|
|
5ef0b504e1 | ||
|
|
0ab53963fb | ||
|
|
b87c215525 | ||
|
|
34f95c07ac | ||
|
|
1e0bf3f915 | ||
|
|
d5bdb496d8 | ||
|
|
5bddc03d5c | ||
|
|
3094e30575 | ||
|
|
743bb06c00 | ||
|
|
f36ea7a34b | ||
|
|
d2778cd922 | ||
| b6999ff3ad | |||
| 4601da0ae7 | |||
| 90286fbe13 |
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
.gitignore
|
||||
|
||||
package-lock.json
|
||||
|
||||
build
|
||||
node_modules
|
||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM alpine AS npm
|
||||
|
||||
RUN apk add npm
|
||||
|
||||
|
||||
FROM npm AS npm-installed
|
||||
|
||||
WORKDIR source
|
||||
ADD package.json .
|
||||
RUN npm install
|
||||
|
||||
|
||||
FROM npm-installed AS builder
|
||||
|
||||
ADD . .
|
||||
RUN npm run build --production
|
||||
|
||||
|
||||
FROM nginx:stable-alpine3.17-slim
|
||||
|
||||
COPY entrypoint.sh .
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
COPY --from=builder /source/build /usr/share/nginx/html
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
70
README.md
70
README.md
@@ -1,70 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
8
docker-compose.yaml
Normal file
8
docker-compose.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
version: '3'
|
||||
services:
|
||||
frontend:
|
||||
build: .
|
||||
container_name: SWA-Frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
28
entrypoint.sh
Normal file
28
entrypoint.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# vim:sw=4:ts=4:et
|
||||
|
||||
set -e
|
||||
|
||||
# Set up endpoint for env retrieval
|
||||
echo "window._env_ = {" > /usr/share/nginx/html/env-config.js
|
||||
|
||||
# Collect enviroment variables for react
|
||||
eval enviroment_variables="$(env | grep REACT_APP.*=)"
|
||||
|
||||
# Loop over variables
|
||||
env | grep REACT_APP.*= | while read -r line;
|
||||
do
|
||||
printf "%s',\n" $line | sed "s/=/:'/" >> /usr/share/nginx/html/env-config.js
|
||||
|
||||
# Notify the user
|
||||
printf "Env variable %s' was injected into React App. \n" $line | sed "0,/=/{s//:'/}"
|
||||
|
||||
done
|
||||
|
||||
# End the object creation
|
||||
echo "}" >> /usr/share/nginx/html/env-config.js
|
||||
|
||||
echo "Enviroment Variable Injection Complete."
|
||||
|
||||
# Start Nginx
|
||||
nginx -g "daemon off;"
|
||||
240
package-lock.json
generated
240
package-lock.json
generated
@@ -8,12 +8,21 @@
|
||||
"name": "swa-labor-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@react-keycloak/web": "^3.4.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"keycloak-js": "^25.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"sass": "^1.77.2",
|
||||
"scss": "^0.2.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
}
|
||||
},
|
||||
@@ -2392,6 +2401,51 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz",
|
||||
"integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz",
|
||||
"integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz",
|
||||
"integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
|
||||
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"react": ">=16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||
@@ -3368,6 +3422,54 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-keycloak/core": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-keycloak/core/-/core-3.2.0.tgz",
|
||||
"integrity": "sha512-1yzU7gQzs+6E1v6hGqxy0Q+kpMHg9sEcke2yxZR29WoU8KNE8E50xS6UbI8N7rWsgyYw8r9W1cUPCOF48MYjzw==",
|
||||
"dependencies": {
|
||||
"react-fast-compare": "^3.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/reactkeycloak"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-keycloak/web": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-keycloak/web/-/web-3.4.0.tgz",
|
||||
"integrity": "sha512-yKKSCyqBtn7dt+VckYOW1IM5NW999pPkxDZOXqJ6dfXPXstYhOQCkTZqh8l7UL14PkpsoaHDh7hSJH8whah01g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.0",
|
||||
"@react-keycloak/core": "^3.2.0",
|
||||
"hoist-non-react-statics": "^3.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/reactkeycloak"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"keycloak-js": ">=9.0.2",
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8",
|
||||
"typescript": ">=3.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz",
|
||||
"integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-babel": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
||||
@@ -5447,6 +5549,29 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
|
||||
@@ -9198,6 +9323,19 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"dependencies": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics/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/hoopy": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
|
||||
@@ -9498,6 +9636,11 @@
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz",
|
||||
"integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ=="
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -12163,6 +12306,11 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sha256": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz",
|
||||
"integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q=="
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -12327,6 +12475,23 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/keycloak-js": {
|
||||
"version": "25.0.0",
|
||||
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.0.tgz",
|
||||
"integrity": "sha512-7vNDYWbi9H2LqeNvkpADL/Y/25KgG+3Byc5epd1eNAXM32FNi1DRMsbdiIHpzrTZhYlxZRWeDGhbLYIwmoMonw==",
|
||||
"dependencies": {
|
||||
"js-sha256": "^0.11.0",
|
||||
"jwt-decode": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -13086,6 +13251,14 @@
|
||||
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
|
||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
|
||||
},
|
||||
"node_modules/ometa": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ometa/-/ometa-0.2.2.tgz",
|
||||
"integrity": "sha512-LZuoK/yjU3FvrxPjUXUlZ1bavCfBPqauA7fsNdwi+AVhRdyk2IzgP3JRnevvjzQ6fKHdUw8YISshf53FmpHrng==",
|
||||
"engines": {
|
||||
"node": ">= 0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -14813,6 +14986,11 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||
@@ -15096,6 +15274,11 @@
|
||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
@@ -15109,6 +15292,36 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.23.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz",
|
||||
"integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.16.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.23.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz",
|
||||
"integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.16.1",
|
||||
"react-router": "6.23.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"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",
|
||||
@@ -15671,6 +15884,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.77.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz",
|
||||
"integrity": "sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==",
|
||||
"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": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-loader": {
|
||||
"version": "12.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
||||
@@ -15749,6 +15978,17 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/scss": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/scss/-/scss-0.2.4.tgz",
|
||||
"integrity": "sha512-4u8V87F+Q/upVhUmhPnB4C1R11xojkRkWjExL2v0CX2EXTg18VrKd+9JWoeyCp2VEMdSpJsyAvVU+rVjogh51A==",
|
||||
"dependencies": {
|
||||
"ometa": "0.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/select-hose": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||
|
||||
@@ -3,12 +3,21 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@react-keycloak/web": "^3.4.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"keycloak-js": "^25.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"sass": "^1.77.2",
|
||||
"scss": "^0.2.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
4
public/env-config.js
Normal file
4
public/env-config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
window._env_ = {
|
||||
REACT_APP_API: "https://api.jodel.anxietyprime.de",
|
||||
REACT_APP_KC: "https://keycloak.anxietyprime.de",
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 163 KiB |
@@ -9,22 +9,8 @@
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>SWA-Jodel</title>
|
||||
<script src="%PUBLIC_URL%/env-config.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
38
src/App.css
38
src/App.css
@@ -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);
|
||||
}
|
||||
}
|
||||
137
src/App.js
137
src/App.js
@@ -1,25 +1,118 @@
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import NewPostForm from './components/NewPostForm';
|
||||
import './App.scss';
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faPenToSquare, faRotate, faTimes} from "@fortawesome/free-solid-svg-icons";
|
||||
import {postApi} from "./api/posts.api";
|
||||
import {locationUtils} from "./utils/location";
|
||||
import Post from "./components/Post";
|
||||
import keycloak from "./Authentification/Keycloak";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</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 = () => {
|
||||
const [posts, setPosts] = useState([]);
|
||||
const [selectedPost, selectPost] = useState(null);
|
||||
const [comment, setComment] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if(posts.length === 0) {
|
||||
reload().then(null);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if(selectedPost) {
|
||||
selectPost(posts.find(post => post.id === selectedPost.id));
|
||||
}
|
||||
}, [posts]);
|
||||
|
||||
|
||||
const reload = async () => {
|
||||
await loadPosts().then(posts => {
|
||||
posts = recursiveSort(posts);
|
||||
setPosts(posts);
|
||||
});
|
||||
}
|
||||
|
||||
const recursiveSort = (posts) => {
|
||||
posts.forEach((post, index) => {posts[index].comments = recursiveSort(post.comments)});
|
||||
posts.sort((a, b) => a.date < b.date ? 1 : -1);
|
||||
return posts;
|
||||
}
|
||||
|
||||
const loadPosts = async () => {
|
||||
const location = await locationUtils.getCurrentLocation();
|
||||
return await postApi.getPostsByCoords(location.lat, location.lon);
|
||||
}
|
||||
|
||||
const addPost = async (title, text) => {
|
||||
await postApi.createNewPost(title, text);
|
||||
await reload();
|
||||
};
|
||||
|
||||
const addComment = async (postId, commentText) => {
|
||||
await postApi.createNewPost("", commentText, postId).then(await reload);
|
||||
};
|
||||
|
||||
const handleCommentSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (comment.trim() && selectedPost) {
|
||||
await addComment(selectedPost.id, comment);
|
||||
setComment('');
|
||||
await reload();
|
||||
selectPost(selectedPost);
|
||||
}
|
||||
};
|
||||
|
||||
const closePost = () => {
|
||||
selectPost(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="nav-bar">
|
||||
<button className="nav-button" onClick={() => reload()}>
|
||||
<FontAwesomeIcon className={"close-button"} icon={faRotate}/>
|
||||
</button>
|
||||
<button className="nav-button nav-space" onClick={() => keycloak.accountManagement()}>
|
||||
<FontAwesomeIcon className={"close-button"} icon={faPenToSquare}/>
|
||||
</button>
|
||||
<button className="logout" onClick={() => keycloak.logout()}>Logout</button>
|
||||
</div>
|
||||
<h1>SWA - Jodel</h1>
|
||||
<div style={{height: selectedPost ? 0 : "auto", overflowY: "hidden"}}>
|
||||
<NewPostForm addPost={addPost}/>
|
||||
{posts.map(post => <Post key={post.id} post={post} recursionDepth={5} selectPost={selectPost} reload={reload} />)}
|
||||
</div>
|
||||
{selectedPost && (
|
||||
<div className="single-post-view">
|
||||
<div className="close-post">
|
||||
<FontAwesomeIcon className={"close-button"} onClick={closePost} icon={faTimes}/>
|
||||
</div>
|
||||
<Post key={selectedPost.id} post={selectedPost} recursionDepth={0} selectPost={null} reload={reload}/>
|
||||
<form className="comment-form" onSubmit={handleCommentSubmit}>
|
||||
<div className="textarea-container">
|
||||
<textarea
|
||||
rows={15}
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
placeholder="Kommentieren..."
|
||||
maxLength={500}
|
||||
/>
|
||||
<div className="char-count-container">
|
||||
<span className={comment.length === 500 ? "char-count max-char-count" : "char-count"}>
|
||||
{comment.length} / 500
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Kommentieren</button>
|
||||
</form>
|
||||
<div className="comments">
|
||||
{selectedPost.comments.map(post => <Post key={post.id} post={post} recursionDepth={0} selectPost={null} reload={reload} />)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
313
src/App.scss
Normal file
313
src/App.scss
Normal file
@@ -0,0 +1,313 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f6f7f9;
|
||||
color: #4a4a4a;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #ff9908;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #ff9908;
|
||||
}
|
||||
|
||||
.new-post-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
padding: 15px;
|
||||
background-color: #ff9908;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #e68a00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-list {
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid lightgray;
|
||||
}
|
||||
|
||||
.post {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border: 1px solid #ddd;
|
||||
margin: 10px 0;
|
||||
padding: 5px 5px 15px 15px;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.post-content {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.post-container {
|
||||
margin-right: -5em;
|
||||
}
|
||||
|
||||
.post-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
width: 5em;
|
||||
|
||||
.post-date {
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
|
||||
.votes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: -1em;
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
margin-bottom: 5px;
|
||||
background-color: transparent;
|
||||
color: #ff9908;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.5em;
|
||||
|
||||
&:hover {
|
||||
color: #e68a00;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
|
||||
input[type="text"] {
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
margin-right: 10px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
background-color: #ff9908;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
|
||||
&:hover {
|
||||
background-color: #e68a00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comments {
|
||||
margin-top: 10px;
|
||||
|
||||
.comment {
|
||||
padding: 10px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 10px;
|
||||
background-color: #fafafa;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.votes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
margin-bottom: 5px;
|
||||
background-color: transparent;
|
||||
color: #ff9908;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.5em;
|
||||
|
||||
&:hover {
|
||||
color: #e68a00;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 1em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.single-post-view {
|
||||
.close-post {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #ff9908;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
font-size: 1em;
|
||||
font-family: Arial, sans-serif;
|
||||
|
||||
&.single-row {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.char-count-container {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 12px;
|
||||
font-size: 12px;
|
||||
color: grey;
|
||||
background: white;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.max-char-count {
|
||||
background-color: rgba(255, 0, 0, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 15px;
|
||||
background-color: #ff9908;
|
||||
color: white;
|
||||
border: none;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #e68a00;
|
||||
}
|
||||
}
|
||||
|
||||
.comment {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.post-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nrOfComments {
|
||||
position: relative;
|
||||
right: -3em;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
font-size: 1.5em;
|
||||
padding: 0.35em;
|
||||
* {
|
||||
padding-right: 0.3em;
|
||||
}
|
||||
span {
|
||||
padding-right: 0.6em;
|
||||
}
|
||||
button {
|
||||
padding: 0.3em 0.8em;
|
||||
line-height: 2em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: #ff9908;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
height: 3em;
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
.nav-space {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.logout {
|
||||
margin-left: 0.5em;
|
||||
width: 25%
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 4em;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
14
src/Authentification/Keycloak.js
Normal file
14
src/Authentification/Keycloak.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Keycloak from "keycloak-js";
|
||||
import getConfig from "../config/config";
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
const keycloak = new Keycloak({
|
||||
url: config.REACT_APP_KC || process.env.REACT_APP_KC,
|
||||
realm: "Jodel",
|
||||
clientId: "jodel-frontend",
|
||||
});
|
||||
|
||||
keycloak.init({ onLoad: 'login-required' }).then((authenticated) => {});
|
||||
|
||||
export default keycloak;
|
||||
52
src/api/posts.api.js
Normal file
52
src/api/posts.api.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import axios from "axios"
|
||||
import {locationUtils} from "../utils/location";
|
||||
import keycloak from "../Authentification/Keycloak";
|
||||
import getConfig from "../config/config";
|
||||
|
||||
const config = getConfig();
|
||||
const path = config.REACT_APP_API || process.env.REACT_APP_API;
|
||||
|
||||
export const postApi = {
|
||||
async getPostsByCoords (lat, lon) {
|
||||
if (!keycloak.authenticated) return [];
|
||||
return (await axios.get(`${path}/posts/${lat}/${lon}`)).data;
|
||||
},
|
||||
async getPostByID (id) {
|
||||
if (!keycloak.authenticated) return [];
|
||||
return (await axios.get(`${path}/post/${id}`)).data;
|
||||
},
|
||||
async createNewPost(title, content, parent = null) {
|
||||
const location = await locationUtils.getCurrentLocation();
|
||||
if(location.lon && location.lat) {
|
||||
if(parent !== null) {
|
||||
await axios.post(`${path}/posts`, {
|
||||
"authorID": 1,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"date": Date.now().toString(),
|
||||
"location": {
|
||||
"longitude": location.lon,
|
||||
"latitude": location.lat,
|
||||
},
|
||||
"parent": parent
|
||||
});
|
||||
} else {
|
||||
await axios.post(`${path}/posts`, {
|
||||
"authorID": 1,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"date": Date.now().toString(),
|
||||
"location": {
|
||||
"longitude": location.lon,
|
||||
"latitude": location.lat,
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log("Geolocation is not supported by this browser. Could'nt post without valid location");
|
||||
}
|
||||
},
|
||||
async reactToPost(id, reaction) {
|
||||
await axios.patch(`${path}/post/${id}`, {reaction: reaction});
|
||||
}
|
||||
}
|
||||
45
src/components/NewPostForm.js
Normal file
45
src/components/NewPostForm.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const NewPostForm = ({ addPost }) => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (text.trim() && title.trim()) {
|
||||
addPost(title, text);
|
||||
setText('');
|
||||
setTitle('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="new-post-form" onSubmit={handleSubmit}>
|
||||
<textarea
|
||||
className="single-row"
|
||||
rows={1}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Titel"
|
||||
maxLength={50}
|
||||
/>
|
||||
<div className="textarea-container">
|
||||
<textarea
|
||||
rows={15}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="Schreibe einen neuen Jodel..."
|
||||
maxLength={500}
|
||||
/>
|
||||
<div className="char-count-container">
|
||||
<span className={text.length === 500 ? "char-count max-char-count" : "char-count"}>
|
||||
{text.length} / 500
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Posten</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewPostForm;
|
||||
97
src/components/Post.js
Normal file
97
src/components/Post.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faCaretUp,
|
||||
faCaretDown,
|
||||
faComments,
|
||||
faAngleUp,
|
||||
faAngleDown
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import {postApi} from "../api/posts.api";
|
||||
import keycloak from "../Authentification/Keycloak";
|
||||
|
||||
const Post = ({ post, recursionDepth, selectPost, reload }) => {
|
||||
|
||||
const [showComments, setShowComments] = React.useState(false);
|
||||
|
||||
const upvote = () => {
|
||||
if (post.reaction === true) {
|
||||
revokeVote();
|
||||
return;
|
||||
}
|
||||
postApi.reactToPost(post.id, true).then(reload);
|
||||
}
|
||||
const downVote = () => {
|
||||
if (post.reaction === false) {
|
||||
revokeVote();
|
||||
return;
|
||||
}
|
||||
postApi.reactToPost(post.id, false).then(reload);
|
||||
}
|
||||
const revokeVote = () => {
|
||||
postApi.reactToPost(post.id, null).then(reload);
|
||||
}
|
||||
|
||||
const getComments = (post, recursionDepth) => {
|
||||
let nrOfComments = post.comments.length;
|
||||
if (recursionDepth !== 0) post.comments.forEach(comment => {
|
||||
nrOfComments += getComments(comment, recursionDepth - 1);
|
||||
})
|
||||
return nrOfComments;
|
||||
}
|
||||
|
||||
const getDate = (dateStr) => {
|
||||
let date = new Date(dateStr);
|
||||
return date.toLocaleDateString('de-DE') + "\n" + ("00" + date.getHours()).slice(-2) + ":" + ("00" + date.getMinutes()).slice(-2);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="post" onClick={click => {if (!(click.target instanceof HTMLButtonElement)) setShowComments(!showComments)}}>
|
||||
<div className="post-container">
|
||||
<div className="post-title">
|
||||
<h3>{post.id === post.parent ? post.title : "Kommentar"}<span style={{color: 'gray'}}> von {post.anonymousID === 0 ? keycloak.idTokenParsed.preferred_username : "Anonymous " + post.anonymousID}</span></h3>
|
||||
</div>
|
||||
<div className="post-content">
|
||||
<p>{post.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
{recursionDepth !== 0 && <div className="nrOfComments">
|
||||
<FontAwesomeIcon className="icon" icon={faComments}/>
|
||||
<span className="">{getComments(post, recursionDepth - 1)}</span>
|
||||
<button onClick={() => selectPost(post)}>Antworten</button>
|
||||
</div>}
|
||||
<div className="post-right">
|
||||
<div className="post-date">
|
||||
{getDate(post.date)}
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="votes">
|
||||
<button onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
upvote();
|
||||
}}>
|
||||
{post.reaction === true ? <FontAwesomeIcon icon={faCaretUp}/> : <FontAwesomeIcon icon={faAngleUp} />}
|
||||
</button>
|
||||
<span>{post.reactions.positive - post.reactions.negative}</span>
|
||||
<button onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
downVote();
|
||||
}}>
|
||||
{post.reaction === false ? <FontAwesomeIcon icon={faCaretDown}/> : <FontAwesomeIcon icon={faAngleDown} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{recursionDepth !== 0 && <div className="spacer"></div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="comment">
|
||||
{recursionDepth !== 0 && showComments && post.comments.map(post =>
|
||||
<Post key={post.id} post={post} recursionDepth={recursionDepth - 1} selectPost={selectPost} reload={reload} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Post;
|
||||
5
src/config/config.js
Normal file
5
src/config/config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const getConfig = () => {
|
||||
return window._env_ || {};
|
||||
}
|
||||
|
||||
export default getConfig;
|
||||
27
src/index.js
27
src/index.js
@@ -2,16 +2,25 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import axios from "axios";
|
||||
import keycloak from "./Authentification/Keycloak";
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
if (keycloak.token) {
|
||||
config.headers['Authorization'] = `Bearer ${keycloak.token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,13 +0,0 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
26
src/utils/location.js
Normal file
26
src/utils/location.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export const locationUtils = {
|
||||
async getCurrentLocation () {
|
||||
let location = await this.returnCurrentLocation().catch((error) => {
|
||||
console.error('Error getting current location', error);
|
||||
});
|
||||
return {lon: location.longitude, lat: location.latitude}; // aktuelle Koordinaten
|
||||
//return {lat: 48.71180776236015, lon: 9.388372081644336}; // Koordinaten von Deizisau (Vorführung für regionale Posts)
|
||||
},
|
||||
async returnCurrentLocation() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const { latitude, longitude } = position.coords;
|
||||
resolve({ latitude, longitude });
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
reject(new Error('Geolocation is not supported by this browser.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user