Compare commits

..

61 Commits

Author SHA1 Message Date
8968e990ec fixed comment button moving during further comments 2024-06-19 13:35:54 +02:00
94996fb45c pinned answer Buttons to bottom of post 2024-06-19 13:01:11 +02:00
29af08f79d moved reaction buttons a little up 2024-06-19 12:03:35 +02:00
a6ef1c71e2 added time to post 2024-06-19 11:57:46 +02:00
2f4eb05233 fixed post sorting for chrome and opera 2024-06-19 09:49:50 +02:00
34e44d2310 fix to not being able to reply to comments 2024-06-18 12:27:34 +02:00
f2f096ef67 changed displayed username 2024-06-15 13:09:22 +02:00
Pete-Gerlach
3f13bbdd19 show anonymousID in Posts and Comments 2024-06-14 16:27:02 +02:00
1ca4235283 added linebreak in posts 2024-06-14 09:44:24 +02:00
96753a774c added dev env 2024-06-13 23:40:09 +02:00
625485d318 removed logging 2024-06-13 23:13:20 +02:00
eeae861353 typo 2024-06-13 23:09:54 +02:00
d524a831cd test 2024-06-13 23:07:58 +02:00
1f947e3200 removed unnecessary entrypoint code 2024-06-13 22:50:43 +02:00
f92b7345ae removed unnecessary entrypoint code 2024-06-13 22:49:06 +02:00
6f9933f332 removed unnecessary entrypoint code 2024-06-13 22:48:55 +02:00
3936d1c651 removed unnecessary entrypoint code 2024-06-13 22:42:04 +02:00
e173888692 more tests 2024-06-13 22:36:57 +02:00
621e414f95 more tests 2024-06-13 22:28:01 +02:00
454973b017 test 2024-06-13 22:19:23 +02:00
17a29376a9 typo 2024-06-13 22:05:34 +02:00
d26044ee3f added config file 2024-06-13 22:04:32 +02:00
47e8f0fc11 added config file 2024-06-13 22:04:03 +02:00
f02083d44c added custom entrypoint 2024-06-13 21:55:08 +02:00
66eaf91521 changed to prod 2024-06-13 16:28:03 +02:00
0704ea59b1 moved to env variables 2024-06-13 15:28:13 +02:00
848ee1ccb4 reversed longitude and latitude in api url 2024-06-13 14:53:27 +02:00
103e3bace3 reversed longitude and latitude in api url 2024-06-13 14:51:36 +02:00
088252f5bc reversed longitude and latitude in api url 2024-06-13 14:47:24 +02:00
9b8dc8cde7 changed tab name 2024-06-13 10:55:51 +02:00
5cb6ef0798 added sort for posts by date 2024-06-13 09:54:10 +02:00
9804bd196b added refresh and edit button 2024-06-13 08:59:35 +02:00
88bdfb35d3 added new favicon 2024-06-13 08:29:47 +02:00
52c8ab9eea made text areas not resizable 2024-06-13 08:24:34 +02:00
475c490b16 fixed wrong recursion comment count if there are not displayed comments 2024-06-13 08:21:25 +02:00
caf853e8a9 removed comment from last recursion layer 2024-06-13 08:15:46 +02:00
3e6ddab88d added way for posts to stay open when commenting 2024-06-12 20:05:35 +02:00
d8050f68cd recursive comment count 2024-06-12 19:26:15 +02:00
9a515baf37 recursion 2024-06-12 19:18:37 +02:00
9ddea4d53d removed doblicated authorization 2024-06-12 10:48:41 +02:00
Pete Gerlach
be9d309f4d display date of posts and comments 2024-06-12 01:38:09 +02:00
8287b72e60 fixed error loading posts when not logged in 2024-06-11 20:59:41 +02:00
36ca8c76d3 added Keycloak 2024-06-11 20:43:52 +02:00
Pete Gerlach
0667d96de3 implemented comment for showing posts from other region if neccessary 2024-06-11 18:08:29 +02:00
Pete Gerlach
5bd0e35532 made all available features functional and reloaded components after updating posts successfully 2024-06-11 18:02:18 +02:00
Pete Gerlach
905e259af2 implemented attribute type to differentiate between comments and posts while using the same component for both types 2024-06-11 15:50:43 +02:00
Pete Gerlach
f17b253ac6 restructured code 2024-06-11 15:26:49 +02:00
Pete Gerlach
5ef0b504e1 restructured code (modal => internal view) 2024-06-11 15:26:17 +02:00
Pete Gerlach
0ab53963fb show all posts within a distance of 10km 2024-06-11 14:23:39 +02:00
Pete Gerlach
b87c215525 implemented loadPosts function 2024-06-11 14:15:03 +02:00
Pete Gerlach
34f95c07ac fixed getCurrentLocation 2024-06-11 14:14:49 +02:00
Pete Gerlach
1e0bf3f915 fixed design and functionality of NewPostForm 2024-06-11 13:36:11 +02:00
Pete Gerlach
d5bdb496d8 exported getLocation function for reusability 2024-06-11 13:35:38 +02:00
Pete Gerlach
5bddc03d5c redesigned NewPostForm component for title and textarea 2024-06-11 13:23:13 +02:00
Pete Gerlach
3094e30575 implemented API 2024-06-11 13:22:08 +02:00
Pete Gerlach
743bb06c00 WIP: API with 401 2024-06-10 00:55:13 +02:00
Pete Gerlach
f36ea7a34b WIP: Basic Jodel Application 2024-06-02 13:48:26 +02:00
Pete Gerlach
d2778cd922 WIP: Basic Jodel Application 2024-05-28 20:10:09 +02:00
b6999ff3ad added docker-compose 2024-04-20 13:24:57 +02:00
4601da0ae7 added dockerized build chain 2024-04-20 13:24:08 +02:00
90286fbe13 deleted default README 2024-04-20 13:10:17 +02:00
27 changed files with 1011 additions and 207 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
Dockerfile
.dockerignore
.gitignore
package-lock.json
build
node_modules

26
Dockerfile Normal file
View 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"]

View File

@@ -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
View 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
View 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
View File

@@ -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",

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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"
}

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

View File

@@ -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();
});

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

View 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
View 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
View File

@@ -0,0 +1,5 @@
const getConfig = () => {
return window._env_ || {};
}
export default getConfig;

View File

@@ -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();

View File

@@ -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

View File

@@ -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;

View File

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