mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2025-07-16 01:28:50 +02:00
added frontend and updated backend with docker, wrote some initial instructions
This commit is contained in:
58
.idea/codeStyles/Project.xml
generated
Normal file
58
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,58 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
<option name="HTML_ENFORCE_QUOTES" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
6
.idea/prettier.xml
generated
Normal file
6
.idea/prettier.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||
</component>
|
||||
</project>
|
49
README.md
49
README.md
@ -1,21 +1,56 @@
|
||||
# HTWK PLANNER
|
||||
# HTWKalender
|
||||
|
||||
Scrape information about Seminar Groups and dates.
|
||||
|
||||
### Run with
|
||||
|
||||
```bash
|
||||
go run . serve
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
### Go to Admin UI
|
||||
|
||||
➜ Webinterface: http://127.0.0.1/
|
||||
|
||||
➜ Admin UI: http://127.0.0.1/_/
|
||||
|
||||
➜ API: http://127.0.0.1/_/
|
||||
|
||||
For first login use the following credentials:
|
||||
|
||||
Email:
|
||||
```
|
||||
demo@htwkalender.de
|
||||
```
|
||||
|
||||
Password:
|
||||
```
|
||||
htwkalender-demo
|
||||
```
|
||||
|
||||
### Go to Admin UI
|
||||
|
||||
` ➜ Admin UI: http://127.0.0.1:8090/_/`
|
||||
|
||||
- create account
|
||||
- import pb_schema.json
|
||||
|
||||
### Fetch Data from HTWK
|
||||
|
||||
`http://127.0.0.1:8090/api/fetchPlans`
|
||||
Execute the following api calls to fetch data manually from HTWK and store it in the database:
|
||||
|
||||
The first command will fetch all groups and store them in the database.
|
||||
This should be done quick in a few seconds (0-5s).
|
||||
When you execute the command again, it will update the groups in the
|
||||
database and only return new added groups.
|
||||
|
||||
http://127.0.0.1/api/fetchGroups
|
||||
|
||||
For fetching the plans, you can use the following command.
|
||||
This will fetch all plans for all groups and store the events in the database.
|
||||
It's done for all current existing events (ws/ss).
|
||||
The whole process takes a while (1-5min), depending on the amount of groups and events.
|
||||
Stay for this time on the page and wait for the response.
|
||||
|
||||
http://127.0.0.1/api/fetchPlans
|
||||
|
||||
|
||||
### View/Filter/Search in Admin UI
|
||||
|
||||
If you want some easy first api endpoints and data views, you can use the Admin UI.
|
||||
|
0
.gitignore → backend/.gitignore
vendored
0
.gitignore → backend/.gitignore
vendored
18
backend/Dockerfile
Normal file
18
backend/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
FROM golang:1.20.5-alpine
|
||||
|
||||
# Set the Current Working Directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod and sum files
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy the source from the current directory to the Working Directory inside the container
|
||||
COPY *.go ./
|
||||
COPY .. .
|
||||
|
||||
# Build the Go app
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /htwkalender
|
||||
|
||||
# Expose port 8080 to the outside world
|
||||
EXPOSE 8080
|
21
backend/README.md
Normal file
21
backend/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# HTWK PLANNER
|
||||
|
||||
Scrape information about Seminar Groups and dates.
|
||||
|
||||
### Run with
|
||||
```bash
|
||||
go run . serve
|
||||
```
|
||||
|
||||
### Go to Admin UI
|
||||
|
||||
` ➜ Admin UI: http://127.0.0.1:8090/_/`
|
||||
|
||||
- create account
|
||||
- import pb_schema.json
|
||||
|
||||
### Fetch Data from HTWK
|
||||
|
||||
`http://127.0.0.1:8090/api/fetchPlans`
|
||||
|
||||
### View/Filter/Search in Admin UI
|
@ -4,7 +4,7 @@ go 1.20
|
||||
|
||||
require (
|
||||
github.com/jordic/goics v0.0.0-20210404174824-5a0337b716a0
|
||||
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
||||
github.com/pocketbase/dbx v1.10.0
|
||||
github.com/pocketbase/pocketbase v0.17.5
|
||||
golang.org/x/net v0.14.0
|
||||
)
|
||||
@ -48,11 +48,11 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/pocketbase/dbx v1.10.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/cobra v1.7.0 // indirect
|
30
backend/main.go
Normal file
30
backend/main.go
Normal file
@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||
_ "htwk-planner/migrations"
|
||||
"htwk-planner/service"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := pocketbase.New()
|
||||
|
||||
// loosely check if it was executed using "go run"
|
||||
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
|
||||
|
||||
migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
|
||||
// enable auto creation of migration files when making collection changes in the Admin UI
|
||||
// (the isGoRun check is to enable it only during development)
|
||||
Automigrate: isGoRun,
|
||||
})
|
||||
|
||||
service.AddRoutes(app)
|
||||
|
||||
if err := app.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
383
backend/migrations/1695150679_collections_snapshot.go
Normal file
383
backend/migrations/1695150679_collections_snapshot.go
Normal file
@ -0,0 +1,383 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
jsonData := `[
|
||||
{
|
||||
"id": "_pb_users_auth_",
|
||||
"created": "2023-09-19 17:30:50.598Z",
|
||||
"updated": "2023-09-19 17:31:15.957Z",
|
||||
"name": "users",
|
||||
"type": "auth",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "users_name",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "users_avatar",
|
||||
"name": "avatar",
|
||||
"type": "file",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"maxSize": 5242880,
|
||||
"mimeTypes": [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"image/gif",
|
||||
"image/webp"
|
||||
],
|
||||
"thumbs": null,
|
||||
"protected": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": "id = @request.auth.id",
|
||||
"viewRule": "id = @request.auth.id",
|
||||
"createRule": "",
|
||||
"updateRule": "id = @request.auth.id",
|
||||
"deleteRule": "id = @request.auth.id",
|
||||
"options": {
|
||||
"allowEmailAuth": true,
|
||||
"allowOAuth2Auth": true,
|
||||
"allowUsernameAuth": true,
|
||||
"exceptEmailDomains": null,
|
||||
"manageRule": null,
|
||||
"minPasswordLength": 8,
|
||||
"onlyEmailDomains": null,
|
||||
"requireEmail": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cfq9mqlmd97v8z5",
|
||||
"created": "2023-09-19 17:31:15.957Z",
|
||||
"updated": "2023-09-19 17:31:15.957Z",
|
||||
"name": "groups",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "85msl21p",
|
||||
"name": "university",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "2sii4dtp",
|
||||
"name": "shortcut",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "uiwgo28f",
|
||||
"name": "groupId",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "y0l1lrzs",
|
||||
"name": "course",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "kr62mhbz",
|
||||
"name": "faculty",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "ya6znpez",
|
||||
"name": "facultyId",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_rcaN2Oq` + "`" + ` ON ` + "`" + `groups` + "`" + ` (` + "`" + `course` + "`" + `)"
|
||||
],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "d65h4wh7zk13gxp",
|
||||
"created": "2023-09-19 17:31:15.957Z",
|
||||
"updated": "2023-09-19 17:31:15.957Z",
|
||||
"name": "feeds",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "cowxjfmc",
|
||||
"name": "modules",
|
||||
"type": "json",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "7her4515qsmrxe8",
|
||||
"created": "2023-09-19 17:31:15.958Z",
|
||||
"updated": "2023-09-19 17:31:15.958Z",
|
||||
"name": "events",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "m8ne8e3m",
|
||||
"name": "Day",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "xnsxqp7j",
|
||||
"name": "Week",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "7vsr9h6p",
|
||||
"name": "Start",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "wwpokofe",
|
||||
"name": "End",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "aeuskrjo",
|
||||
"name": "Name",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "klrzqyw0",
|
||||
"name": "EventType",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "5zltexoy",
|
||||
"name": "Prof",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "gy3nvfmx",
|
||||
"name": "Rooms",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "hn7b8dfy",
|
||||
"name": "Notes",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "axskpwm8",
|
||||
"name": "BookedAt",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "vyyefxp7",
|
||||
"name": "course",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "vlbpm9fz",
|
||||
"name": "semester",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_orp1NWL` + "`" + ` ON ` + "`" + `events` + "`" + ` (\n ` + "`" + `Day` + "`" + `,\n ` + "`" + `Week` + "`" + `,\n ` + "`" + `Start` + "`" + `,\n ` + "`" + `End` + "`" + `,\n ` + "`" + `Name` + "`" + `,\n ` + "`" + `course` + "`" + `,\n ` + "`" + `Prof` + "`" + `,\n ` + "`" + `Rooms` + "`" + `,\n ` + "`" + `EventType` + "`" + `\n)"
|
||||
],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
}
|
||||
]`
|
||||
|
||||
var collections []*models.Collection
|
||||
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return daos.New(db).ImportCollections(collections, true, nil)
|
||||
}, func(db dbx.Builder) error {
|
||||
return nil
|
||||
})
|
||||
}
|
34
backend/migrations/1695151278_add_admin_account.go
Normal file
34
backend/migrations/1695151278_add_admin_account.go
Normal file
@ -0,0 +1,34 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
admin := &models.Admin{}
|
||||
admin.Email = "demo@htwkalender.de"
|
||||
err := admin.SetPassword("htwkalender-demo")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dao.SaveAdmin(admin)
|
||||
}, func(db dbx.Builder) error { // optional revert operation
|
||||
|
||||
dao := daos.New(db)
|
||||
|
||||
admin, _ := dao.FindAdminByEmail("test@example.com")
|
||||
if admin != nil {
|
||||
return dao.DeleteAdmin(admin)
|
||||
}
|
||||
|
||||
// already deleted
|
||||
return nil
|
||||
})
|
||||
}
|
@ -144,6 +144,29 @@
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "d65h4wh7zk13gxp",
|
||||
"name": "feeds",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"id": "cowxjfmc",
|
||||
"name": "modules",
|
||||
"type": "json",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "7her4515qsmrxe8",
|
||||
"name": "events",
|
||||
@ -296,7 +319,7 @@
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX `idx_orp1NWL` ON `events` (\n `Day`,\n `Week`,\n `Start`,\n `End`,\n `Name`,\n `course`\n)"
|
||||
"CREATE UNIQUE INDEX `idx_orp1NWL` ON `events` (\n `Day`,\n `Week`,\n `Start`,\n `End`,\n `Name`,\n `course`,\n `Prof`,\n `Rooms`,\n `EventType`\n)"
|
||||
],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
@ -304,28 +327,5 @@
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "d65h4wh7zk13gxp",
|
||||
"name": "feeds",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"id": "cowxjfmc",
|
||||
"name": "modules",
|
||||
"type": "json",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
}
|
||||
]
|
@ -1,19 +1,19 @@
|
||||
package main
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"htwk-planner/service/events"
|
||||
"htwk-planner/service/fetch"
|
||||
events2 "htwk-planner/service/events"
|
||||
fetch2 "htwk-planner/service/fetch"
|
||||
"htwk-planner/service/ical"
|
||||
"htwk-planner/service/room"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func addRoutes(app *pocketbase.PocketBase) {
|
||||
func AddRoutes(app *pocketbase.PocketBase) {
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public"), false))
|
||||
@ -25,7 +25,7 @@ func addRoutes(app *pocketbase.PocketBase) {
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/fetchPlans",
|
||||
Handler: func(c echo.Context) error {
|
||||
return fetch.GetSeminarEvents(c, app)
|
||||
return fetch2.GetSeminarEvents(c, app)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
@ -42,7 +42,7 @@ func addRoutes(app *pocketbase.PocketBase) {
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/fetchGroups",
|
||||
Handler: func(c echo.Context) error {
|
||||
return fetch.SeminarGroups(c, app)
|
||||
return fetch2.SeminarGroups(c, app)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
@ -113,7 +113,7 @@ func addRoutes(app *pocketbase.PocketBase) {
|
||||
Handler: func(c echo.Context) error {
|
||||
course := c.QueryParam("course")
|
||||
semester := c.QueryParam("semester")
|
||||
return events.GetModulesForCourseDistinct(app, c, course, semester)
|
||||
return events2.GetModulesForCourseDistinct(app, c, course, semester)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
@ -130,7 +130,7 @@ func addRoutes(app *pocketbase.PocketBase) {
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/modules",
|
||||
Handler: func(c echo.Context) error {
|
||||
return events.GetAllModulesDistinct(app, c)
|
||||
return events2.GetAllModulesDistinct(app, c)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
@ -147,7 +147,7 @@ func addRoutes(app *pocketbase.PocketBase) {
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/courses",
|
||||
Handler: func(c echo.Context) error {
|
||||
return events.GetAllCourses(app, c)
|
||||
return events2.GetAllCourses(app, c)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
@ -6,35 +6,78 @@ import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"htwk-planner/model"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SaveEvents(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) error {
|
||||
func SaveEvents(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) ([]*models.Record, error) {
|
||||
var toBeSavedEvents []struct {
|
||||
model.Event
|
||||
string
|
||||
}
|
||||
var savedRecords []*models.Record
|
||||
var insertRecords []*models.Record
|
||||
|
||||
// check if event is already in database and add to toBeSavedEvents if not
|
||||
for _, seminarGroup := range seminarGroup {
|
||||
for _, event := range seminarGroup.Events {
|
||||
var err error = nil
|
||||
record := models.NewRecord(collection)
|
||||
record.Set("Day", event.Day)
|
||||
record.Set("Week", event.Week)
|
||||
record.Set("Start", event.Start)
|
||||
record.Set("End", event.End)
|
||||
record.Set("Name", event.Name)
|
||||
record.Set("EventType", event.EventType)
|
||||
record.Set("Prof", event.Prof)
|
||||
record.Set("Rooms", event.Rooms)
|
||||
record.Set("Notes", event.Notes)
|
||||
record.Set("BookedAt", event.BookedAt)
|
||||
record.Set("course", seminarGroup.Course)
|
||||
record.Set("semester", event.Semester)
|
||||
err = app.Dao().SaveRecord(record)
|
||||
if err != nil {
|
||||
println("Error while saving record: ", err.Error())
|
||||
dbGroup, err := findEventByDayWeekStartEndNameCourse(event, seminarGroup.Course, app)
|
||||
|
||||
if dbGroup == nil && err.Error() == "sql: no rows in result set" {
|
||||
toBeSavedEvents = append(toBeSavedEvents, struct {
|
||||
model.Event
|
||||
string
|
||||
}{event, seminarGroup.Course})
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
// create record for each event that's not already in the database
|
||||
for _, event := range toBeSavedEvents {
|
||||
record := models.NewRecord(collection)
|
||||
record.Set("Day", event.Day)
|
||||
record.Set("Week", event.Week)
|
||||
record.Set("Start", event.Start)
|
||||
record.Set("End", event.End)
|
||||
record.Set("Name", event.Name)
|
||||
record.Set("EventType", event.EventType)
|
||||
record.Set("Prof", event.Prof)
|
||||
record.Set("Rooms", event.Rooms)
|
||||
record.Set("Notes", event.Notes)
|
||||
record.Set("BookedAt", event.BookedAt)
|
||||
record.Set("course", event.string)
|
||||
record.Set("semester", event.Semester)
|
||||
insertRecords = append(insertRecords, record)
|
||||
}
|
||||
|
||||
// save all records
|
||||
for _, record := range insertRecords {
|
||||
if record != nil {
|
||||
err := app.Dao().SaveRecord(record)
|
||||
if err == nil {
|
||||
savedRecords = append(savedRecords, record)
|
||||
} else {
|
||||
log.Println("Error while saving record: ", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return savedRecords, nil
|
||||
}
|
||||
|
||||
func findEventByDayWeekStartEndNameCourse(event model.Event, course string, app *pocketbase.PocketBase) (*model.Event, error) {
|
||||
err := app.Dao().DB().Select("*").From("events").Where(
|
||||
dbx.NewExp("Day = {:day} AND Week = {:week} AND Start = {:start} AND End = {:end} AND Name = {:name} AND course = {:course} AND Prof = {:prof} AND Rooms = {:rooms} AND EventType = {:eventType}",
|
||||
dbx.Params{"day": event.Day, "week": event.Week, "start": event.Start, "end": event.End, "name": event.Name, "course": course, "prof": event.Prof, "rooms": event.Rooms, "eventType": event.EventType}),
|
||||
).One(&event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &event, err
|
||||
}
|
||||
|
||||
func contains(s []string, e string) bool {
|
81
backend/service/db/dbGroups.go
Normal file
81
backend/service/db/dbGroups.go
Normal file
@ -0,0 +1,81 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"htwk-planner/model"
|
||||
)
|
||||
|
||||
func SaveGroups(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) ([]*models.Record, error) {
|
||||
var savedRecords []*models.Record
|
||||
var tobeSavedGroups []model.SeminarGroup
|
||||
var insertRecords []*models.Record
|
||||
|
||||
for _, group := range seminarGroup {
|
||||
dbGroup, err := FindGroupByCourse(group.Course, app)
|
||||
|
||||
if dbGroup == nil && err.Error() == "sql: no rows in result set" {
|
||||
tobeSavedGroups = append(tobeSavedGroups, group)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// create record for each group that's not already in the database
|
||||
for _, group := range tobeSavedGroups {
|
||||
record := models.NewRecord(collection)
|
||||
record.Set("university", group.University)
|
||||
record.Set("shortcut", group.GroupShortcut)
|
||||
record.Set("groupId", group.GroupId)
|
||||
record.Set("course", group.Course)
|
||||
record.Set("faculty", group.Faculty)
|
||||
record.Set("facultyId", group.FacultyId)
|
||||
insertRecords = append(insertRecords, record)
|
||||
}
|
||||
|
||||
// save all records
|
||||
for _, record := range insertRecords {
|
||||
if record != nil {
|
||||
err := app.Dao().SaveRecord(record)
|
||||
if err == nil {
|
||||
savedRecords = append(savedRecords, record)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return savedRecords, nil
|
||||
}
|
||||
|
||||
func FindGroupByCourse(course string, app *pocketbase.PocketBase) (*model.SeminarGroup, error) {
|
||||
var group model.SeminarGroup
|
||||
err := app.Dao().DB().Select("*").From("groups").Where(dbx.NewExp("course = {:course}", dbx.Params{"course": course})).One(&group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &group, err
|
||||
}
|
||||
|
||||
func GetAllCourses(app *pocketbase.PocketBase) []string {
|
||||
|
||||
var courses []struct {
|
||||
CourseShortcut string `db:"course" json:"course"`
|
||||
}
|
||||
|
||||
// get all rooms from event records in the events collection
|
||||
err := app.Dao().DB().Select("course").From("groups").All(&courses)
|
||||
if err != nil {
|
||||
print("Error while getting groups from database: ", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var courseArray []string
|
||||
|
||||
for _, course := range courses {
|
||||
courseArray = append(courseArray, course.CourseShortcut)
|
||||
}
|
||||
|
||||
return courseArray
|
||||
}
|
@ -28,12 +28,25 @@ func GetSeminarEvents(c echo.Context, app *pocketbase.PocketBase) error {
|
||||
return apis.NewNotFoundError("Collection not found", dbError)
|
||||
}
|
||||
|
||||
dbError = db.SaveEvents(seminarGroups, collection, app)
|
||||
seminarGroups = clearEmptySeminarGroups(seminarGroups)
|
||||
|
||||
savedRecords, dbError := db.SaveEvents(seminarGroups, collection, app)
|
||||
|
||||
if dbError != nil {
|
||||
return apis.NewApiError(400, "Could not save Event into database", dbError)
|
||||
return apis.NewNotFoundError("Events could not be saved", dbError)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, seminarGroups)
|
||||
return c.JSON(http.StatusOK, savedRecords)
|
||||
}
|
||||
|
||||
func clearEmptySeminarGroups(seminarGroups []model.SeminarGroup) []model.SeminarGroup {
|
||||
var newSeminarGroups []model.SeminarGroup
|
||||
for _, seminarGroup := range seminarGroups {
|
||||
if len(seminarGroup.Events) > 0 && seminarGroup.Course != "" {
|
||||
newSeminarGroups = append(newSeminarGroups, seminarGroup)
|
||||
}
|
||||
}
|
||||
return newSeminarGroups
|
||||
}
|
||||
|
||||
func GetSeminarGroupsEventsFromHTML(seminarGroupsLabel []string) []model.SeminarGroup {
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"htwk-planner/model"
|
||||
"htwk-planner/service/db"
|
||||
"io"
|
||||
@ -56,13 +57,14 @@ func SeminarGroups(c echo.Context, app *pocketbase.PocketBase) error {
|
||||
if dbError != nil {
|
||||
return apis.NewNotFoundError("Collection not found", dbError)
|
||||
}
|
||||
var insertedGroups []*models.Record
|
||||
|
||||
dbError = db.SaveGroups(groups, collection, app)
|
||||
insertedGroups, dbError = db.SaveGroups(groups, collection, app)
|
||||
if dbError != nil {
|
||||
return apis.NewApiError(400, "Could not save Event into database", dbError)
|
||||
return apis.NewNotFoundError("Records could not be saved", dbError)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, groups)
|
||||
return c.JSON(http.StatusOK, insertedGroups)
|
||||
}
|
||||
|
||||
func removeDuplicates(groups []model.SeminarGroup) []model.SeminarGroup {
|
@ -7,8 +7,8 @@ import (
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"htwk-planner/model"
|
||||
"htwk-planner/service/db"
|
||||
model "htwk-planner/model"
|
||||
db "htwk-planner/service/db"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
@ -0,0 +1,33 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
htwkalender-backend:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: ./backend
|
||||
# open port 8090
|
||||
ports:
|
||||
- "8090:8090"
|
||||
command: "/htwkalender serve --http=0.0.0.0:8090 --dir=/pb_data"
|
||||
volumes:
|
||||
- ./backend/pb_data:/pb_data
|
||||
htwkalender-frontend:
|
||||
volumes:
|
||||
- ./frontend/src:/app/src
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: ./frontend
|
||||
# open port 8000
|
||||
ports:
|
||||
- "8000:8000"
|
||||
command: "npm run dev"
|
||||
|
||||
rproxy:
|
||||
image: nginx:stable
|
||||
volumes:
|
||||
- ./reverseproxy.conf:/etc/nginx/nginx.conf
|
||||
depends_on:
|
||||
- htwkalender-backend
|
||||
- htwkalender-frontend
|
||||
ports:
|
||||
- "80:80"
|
1
frontend/.dockerignore
Normal file
1
frontend/.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
14
frontend/.eslintrc.cjs
Normal file
14
frontend/.eslintrc.cjs
Normal file
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"prettier",
|
||||
"@vue/typescript/recommended",
|
||||
],
|
||||
parserOptions: {},
|
||||
rules: {},
|
||||
};
|
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
1
frontend/.prettierrc.json
Normal file
1
frontend/.prettierrc.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
7
frontend/Dockerfile
Normal file
7
frontend/Dockerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM node:latest
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY ./ ./
|
||||
|
20
frontend/README.md
Normal file
20
frontend/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
# htwk-planner-frontend
|
@ -5,11 +5,9 @@
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HTWKalender</title>
|
||||
<script type="module" crossorigin src="/assets/index-7b40e5f8.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-ae817131.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
2716
frontend/package-lock.json
generated
Normal file
2716
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
frontend/package.json
Normal file
33
frontend/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "htwk-planner",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
|
||||
"format": "prettier . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^2.1.6",
|
||||
"primeflex": "^3.3.1",
|
||||
"primeicons": "^6.0.1",
|
||||
"primevue": "^3.32.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.5.9",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"prettier": "3.0.2",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5",
|
||||
"vue-tsc": "^1.8.5"
|
||||
}
|
||||
}
|
BIN
frontend/public/Browse.plb
Normal file
BIN
frontend/public/Browse.plb
Normal file
Binary file not shown.
35
frontend/public/vite.svg
Normal file
35
frontend/public/vite.svg
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with PhotoLine 24.00 (www.pl32.de) -->
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad0" x1="4.2" y1="16" x2="10.37" y2="16" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.84 0 0 0.84 -27.54 6.47)">
|
||||
<stop offset="0" stop-color="#000000"/>
|
||||
<stop offset="1" stop-color="#ffffff"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grad1" x1="21.63" y1="16" x2="27.8" y2="16" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.84 0 0 0.84 -27.54 6.47)">
|
||||
<stop offset="0" stop-color="#000000"/>
|
||||
<stop offset="1" stop-color="#ffffff"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grad2" x1="12.5" y1="15.61" x2="19.54" y2="15.61" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.84 0 0 0.84 -27.54 6.47)">
|
||||
<stop offset="0" stop-color="#000000"/>
|
||||
<stop offset="1" stop-color="#ffffff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="matrix(1.194506 0 0 1.194506 32.902259 -7.722892)">
|
||||
<g id="Quadrat/Rechteck 3" transform="matrix(1 0 0 1 0 0.111111)">
|
||||
<rect transform="matrix(1 0 0 1 0 -0.111111)" fill="url(#grad0)" fill-rule="evenodd" x="-24.03" y="8.64"
|
||||
width="5.17" height="22.44"/>
|
||||
</g>
|
||||
<g id="Quadrat/Rechteck 3" transform="matrix(1 0 0 1 14.583336 0.111111)">
|
||||
<rect transform="matrix(1 0 0 1 -14.583336 -0.111111)" fill="url(#grad1)" fill-rule="evenodd" x="-9.44"
|
||||
y="8.64" width="5.17" height="22.44"/>
|
||||
</g>
|
||||
<g id="Quadrat/Rechteck 3" transform="matrix(0 0.430105 -0.262376 0 -8.956966 28.751123)">
|
||||
<rect transform="matrix(0 -3.811324 2.325016 0 -66.846832 -34.137889)" fill="url(#grad2)"
|
||||
fill-rule="evenodd" x="-17.08" y="18.42" width="5.89" height="2.22"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
10
frontend/src/App.vue
Normal file
10
frontend/src/App.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import MenuBar from "./components/MenuBar.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenuBar />
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
20
frontend/src/api/createFeed.ts
Normal file
20
frontend/src/api/createFeed.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Module } from "../model/module.ts";
|
||||
|
||||
export async function createIndividualFeed(modules: Module[]): Promise<string> {
|
||||
let token = "";
|
||||
|
||||
await fetch("/api/createFeed", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(modules),
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((response) => {
|
||||
token = response;
|
||||
});
|
||||
return token;
|
||||
}
|
45
frontend/src/api/fetchCourse.ts
Normal file
45
frontend/src/api/fetchCourse.ts
Normal file
@ -0,0 +1,45 @@
|
||||
// function to fetch course data from the API
|
||||
|
||||
import { Module } from "../model/module.ts";
|
||||
|
||||
export async function fetchCourse(): Promise<string[]> {
|
||||
const courses: string[] = [];
|
||||
await fetch("/api/courses")
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((coursesResponse) => {
|
||||
coursesResponse.forEach((course: string) => courses.push(course));
|
||||
});
|
||||
return courses;
|
||||
}
|
||||
|
||||
export async function fetchModulesByCourseAndSemester(
|
||||
course: string,
|
||||
semester: string,
|
||||
): Promise<Module[]> {
|
||||
const modules: Module[] = [];
|
||||
await fetch("/api/course/modules?course=" + course + "&semester=" + semester)
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((modulesResponse) => {
|
||||
modulesResponse.forEach((module: string) =>
|
||||
modules.push(new Module(module, course)),
|
||||
);
|
||||
});
|
||||
return modules;
|
||||
}
|
||||
|
||||
export async function fetchAllModules(): Promise<Module[]> {
|
||||
let modules: Module[] = [];
|
||||
await fetch("/api/modules")
|
||||
.then((response) => {
|
||||
return response.json() as Promise<Module[]>;
|
||||
})
|
||||
.then((responseModules: Module[]) => {
|
||||
modules = responseModules as Module[];
|
||||
});
|
||||
|
||||
return modules;
|
||||
}
|
1
frontend/src/assets/vue.svg
Normal file
1
frontend/src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
106
frontend/src/components/AdditionalModules.vue
Normal file
106
frontend/src/components/AdditionalModules.vue
Normal file
@ -0,0 +1,106 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, Ref } from "vue";
|
||||
import { Module } from "../model/module.ts";
|
||||
import { fetchAllModules } from "../api/fetchCourse.ts";
|
||||
import moduleStore from "../store/moduleStore.ts";
|
||||
import { createIndividualFeed } from "../api/createFeed.ts";
|
||||
import { MultiSelectAllChangeEvent } from "primevue/multiselect";
|
||||
import tokenStore from "../store/tokenStore.ts";
|
||||
import router from "../router";
|
||||
|
||||
const fetchedModules = async () => {
|
||||
return await fetchAllModules();
|
||||
};
|
||||
|
||||
const modules: Ref<Module[]> = ref([]);
|
||||
|
||||
const selectedModules: Ref<Module[]> = ref([] as Module[]);
|
||||
|
||||
fetchedModules().then(
|
||||
(data) =>
|
||||
(modules.value = data.map((module: Module) => {
|
||||
return module;
|
||||
})),
|
||||
);
|
||||
|
||||
async function finalStep() {
|
||||
selectedModules.value.forEach((module: Module) => {
|
||||
moduleStore().addModule(module);
|
||||
});
|
||||
|
||||
const token: string = await createIndividualFeed(moduleStore().modules);
|
||||
tokenStore().setToken(token);
|
||||
await router.push("/calendar-link");
|
||||
}
|
||||
|
||||
const display = (module: Module) => module.Name + " (" + module.Course + ")";
|
||||
|
||||
const selectAll = ref(false);
|
||||
|
||||
const onSelectAllChange = (event: MultiSelectAllChangeEvent) => {
|
||||
selectedModules.value = event.checked
|
||||
? modules.value.map((module) => module)
|
||||
: [];
|
||||
selectAll.value = event.checked;
|
||||
};
|
||||
|
||||
function selectChange() {
|
||||
selectAll.value = selectedModules.value.length === modules.value.length;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-column">
|
||||
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
||||
<h3>
|
||||
Select additional Modules that are not listed in the regular semester
|
||||
for your Course
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card flex align-items-center justify-content-center m-2">
|
||||
<MultiSelect
|
||||
v-model="selectedModules"
|
||||
:max-selected-labels="1"
|
||||
:option-label="display"
|
||||
:options="modules"
|
||||
:select-all="selectAll"
|
||||
:virtual-scroller-options="{ itemSize: 70 }"
|
||||
class="custom-multiselect"
|
||||
filter
|
||||
placeholder="Select additional modules"
|
||||
@change="selectChange()"
|
||||
@selectall-change="onSelectAllChange($event)"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="flex align-items-center">
|
||||
<p class="text-1xl white-space-normal">
|
||||
{{ display(slotProps.option) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="py-2 px-3">
|
||||
<b>{{ selectedModules ? selectedModules.length : 0 }}</b>
|
||||
item{{
|
||||
(selectedModules ? selectedModules.length : 0) > 1 ? "s" : ""
|
||||
}}
|
||||
selected.
|
||||
</div>
|
||||
</template>
|
||||
</MultiSelect>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
||||
<Button @click="finalStep()"> Create Calendar</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.custom-multiselect) {
|
||||
width: 50rem;
|
||||
}
|
||||
|
||||
:deep(.custom-multiselect li) {
|
||||
height: unset;
|
||||
}
|
||||
</style>
|
49
frontend/src/components/CalendarLink.vue
Normal file
49
frontend/src/components/CalendarLink.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import tokenStore from "../store/tokenStore.ts";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { onMounted } from "vue";
|
||||
import router from "../router";
|
||||
const toast = useToast();
|
||||
|
||||
const show = () => {
|
||||
toast.add({
|
||||
severity: "info",
|
||||
summary: "Info",
|
||||
detail: "Link copied to clipboard",
|
||||
life: 3000,
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
rerouteIfTokenIsEmpty();
|
||||
});
|
||||
|
||||
function rerouteIfTokenIsEmpty() {
|
||||
if (tokenStore().token == "") {
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
const text = "http://localhost:8090/api/feed?token=" + tokenStore().token;
|
||||
// Copy the text inside the text field
|
||||
navigator.clipboard.writeText(text);
|
||||
show();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
<div class="flex flex-column">
|
||||
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
||||
<h2>
|
||||
{{ "http://localhost:8090/api/feed?token=" + tokenStore().token }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
||||
<Button @click="copyToClipboard">Copy iCal Link</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
84
frontend/src/components/CourseSelection.vue
Normal file
84
frontend/src/components/CourseSelection.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { Ref, ref } from "vue";
|
||||
import {
|
||||
fetchCourse,
|
||||
fetchModulesByCourseAndSemester,
|
||||
} from "../api/fetchCourse";
|
||||
import ModuleSelection from "./ModuleSelection.vue";
|
||||
import { Module } from "../model/module.ts";
|
||||
|
||||
const courses = async () => {
|
||||
return await fetchCourse();
|
||||
};
|
||||
|
||||
const selectedCourse: Ref<{ name: string }> = ref({ name: "" });
|
||||
const countries: Ref<{ name: string }[]> = ref([]);
|
||||
const semesters: Ref<{ name: string; value: string }[]> = ref([
|
||||
{ name: "Wintersemester", value: "ws" },
|
||||
{ name: "Sommersemester", value: "ss" },
|
||||
]);
|
||||
|
||||
const selectedSemester: Ref<{ name: string; value: string }> = ref(
|
||||
semesters.value[0],
|
||||
);
|
||||
|
||||
courses().then(
|
||||
(data) =>
|
||||
(countries.value = data.map((course) => {
|
||||
return { name: course };
|
||||
})),
|
||||
);
|
||||
|
||||
const modules: Ref<Module[]> = ref([]);
|
||||
|
||||
async function getModules() {
|
||||
modules.value = await fetchModulesByCourseAndSemester(
|
||||
selectedCourse.value.name,
|
||||
selectedSemester.value.value,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-column">
|
||||
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
||||
<h3 class="text-4xl">Welcome to the Course Calendar</h3>
|
||||
</div>
|
||||
<div
|
||||
class="flex align-items-center justify-content-center h-4rem border-round m-2"
|
||||
>
|
||||
<h5 class="text-2xl">Please select a course</h5>
|
||||
</div>
|
||||
<div
|
||||
class="flex align-items-center justify-content-center border-round m-2"
|
||||
>
|
||||
<Dropdown
|
||||
v-model="selectedCourse"
|
||||
:options="countries"
|
||||
class="w-full md:w-25rem mx-2"
|
||||
filter
|
||||
option-label="name"
|
||||
placeholder="Select a Course"
|
||||
@change="getModules()"
|
||||
></Dropdown>
|
||||
<Dropdown
|
||||
v-model="selectedSemester"
|
||||
:options="semesters"
|
||||
class="w-full md:w-25rem mx-2"
|
||||
option-label="name"
|
||||
placeholder="Select a Semester"
|
||||
@change="getModules()"
|
||||
></Dropdown>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex align-items-center justify-content-center border-round m-2"
|
||||
>
|
||||
<div class="flex flex-wrap justify-content-center">
|
||||
<div class="flex align-items-center">
|
||||
<ModuleSelection :modules="modules" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
11
frontend/src/components/FaqPage.vue
Normal file
11
frontend/src/components/FaqPage.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-column">
|
||||
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
||||
<h2>FAQ</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
11
frontend/src/components/Impress.vue
Normal file
11
frontend/src/components/Impress.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div class="flex align-items-center justify-content-center flex-column">
|
||||
<div class="flex align-items-center justify-content-center h-4rem mt-2">
|
||||
<h3 class="text-4xl">Impress</h3>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
39
frontend/src/components/MenuBar.vue
Normal file
39
frontend/src/components/MenuBar.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: "Create Calendar",
|
||||
icon: "pi pi-fw pi-plus",
|
||||
url: "/",
|
||||
},
|
||||
{
|
||||
label: "FAQ",
|
||||
icon: "pi pi-fw pi-book",
|
||||
url: "/faq",
|
||||
},
|
||||
{
|
||||
label: "Impress",
|
||||
icon: "pi pi-fw pi-id-card",
|
||||
url: "/impress",
|
||||
},
|
||||
{
|
||||
label: "Privacy",
|
||||
url: "/privacy-policy",
|
||||
icon: "pi pi-fw pi-exclamation-triangle",
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menubar :model="items" class="menubar justify-content-center">
|
||||
<template #start></template>
|
||||
</Menubar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.menubar {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
138
frontend/src/components/ModuleSelection.vue
Normal file
138
frontend/src/components/ModuleSelection.vue
Normal file
@ -0,0 +1,138 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, PropType, Ref, ref, watch } from "vue";
|
||||
import { Module } from "../model/module.ts";
|
||||
import moduleStore from "../store/moduleStore";
|
||||
import router from "../router";
|
||||
|
||||
const props = defineProps({
|
||||
modules: {
|
||||
type: Array as PropType<Module[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// array of modules with boolean if selected with getter and setter
|
||||
const modulesWithSelection: Ref<{ module: Module; selected: boolean }[]> = ref(
|
||||
props.modules.map((module) => {
|
||||
return { module: module, selected: false };
|
||||
}),
|
||||
);
|
||||
|
||||
//watch for changes in modules prop and update modulesWithSelection
|
||||
|
||||
function selectedModules(): Module[] {
|
||||
return modulesWithSelection.value
|
||||
.filter((module) => module.selected)
|
||||
.map((module) => module.module);
|
||||
}
|
||||
|
||||
const currentModules = computed(() => props.modules);
|
||||
|
||||
function selectAllModules(selection: boolean) {
|
||||
modulesWithSelection.value.forEach((module) => {
|
||||
module.selected = selection;
|
||||
});
|
||||
}
|
||||
|
||||
const allSelected: Ref<boolean> = ref(true);
|
||||
|
||||
computed(() => {
|
||||
return modulesWithSelection.value.every((module) => module.selected);
|
||||
});
|
||||
|
||||
watch(currentModules, (newValue: Module[]) => {
|
||||
modulesWithSelection.value = newValue.map((module) => {
|
||||
return { module: module, selected: false };
|
||||
});
|
||||
});
|
||||
|
||||
function nextStep() {
|
||||
console.log("next step");
|
||||
selectedModules().forEach((module) => {
|
||||
moduleStore().addModule(module);
|
||||
});
|
||||
|
||||
console.debug(moduleStore().modules);
|
||||
|
||||
router.push("/additional-modules");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-column card-container">
|
||||
<div class="flex align-items-center justify-content-center mb-3">
|
||||
<Button
|
||||
:disabled="selectedModules().length < 1"
|
||||
class="col-4 justify-content-center"
|
||||
@click="nextStep()"
|
||||
>Next Step
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center">
|
||||
<DataView :value="modulesWithSelection" data-key="name">
|
||||
<template #header>
|
||||
<div class="flex justify-content-between flex-wrap">
|
||||
<div class="flex align-items-center justify-content-center">
|
||||
<h3>Selected Modules - {{ selectedModules().length }}</h3>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center">
|
||||
<ToggleButton
|
||||
v-model="allSelected"
|
||||
class="w-12rem"
|
||||
off-icon="pi pi-times"
|
||||
off-label="Unselect All"
|
||||
on-icon="pi pi-check"
|
||||
on-label="Select All"
|
||||
@click="selectAllModules(!allSelected)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<p class="p-4 text-2xl font-bold text-900 empty-message">
|
||||
No Modules found for this course
|
||||
</p>
|
||||
</template>
|
||||
<template #list="slotProps">
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="flex flex-column xl:flex-row xl:align-items-start p-4 gap-4"
|
||||
>
|
||||
<div
|
||||
class="flex flex-column sm:flex-row justify-content-between align-items-center xl:align-items-start flex-1 gap-4"
|
||||
>
|
||||
<div
|
||||
class="flex flex-column align-items-center sm:align-items-start gap-3"
|
||||
>
|
||||
<div class="text-2xl">
|
||||
{{ slotProps.data.module.Name }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex sm:flex-column align-items-center sm:align-items-end gap-3 sm:gap-2"
|
||||
>
|
||||
<ToggleButton
|
||||
v-model="modulesWithSelection[slotProps.index].selected"
|
||||
class="w-9rem"
|
||||
off-icon="pi pi-times"
|
||||
off-label="Unselected"
|
||||
on-icon="pi pi-check"
|
||||
on-label="Selected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DataView>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (min-width: 962px) {
|
||||
.empty-message {
|
||||
width: 50rem;
|
||||
}
|
||||
}
|
||||
</style>
|
537
frontend/src/components/PrivacyPolicy.vue
Normal file
537
frontend/src/components/PrivacyPolicy.vue
Normal file
@ -0,0 +1,537 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div class="flex align-items-center justify-content-center flex-column">
|
||||
<div class="flex align-items-center justify-content-center h-4rem mt-2">
|
||||
<h3 class="text-4xl">Privacy policy</h3>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center h-4rem">
|
||||
<p>https://github.com/primefaces/primevue/blob/master/LICENSE.md</p>
|
||||
</div>
|
||||
<div class="flex flex-column col-7">
|
||||
<h1>Datenschutzerklärung</h1>
|
||||
<p>Stand: 19. September 2023</p>
|
||||
<h2>Inhaltsübersicht</h2>
|
||||
<ul class="index">
|
||||
<li><a class="index-link" href="#m3">Verantwortlicher</a></li>
|
||||
<li>
|
||||
<a class="index-link" href="#mOverview"
|
||||
>Übersicht der Verarbeitungen</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a class="index-link" href="#m2427">Maßgebliche Rechtsgrundlagen</a>
|
||||
</li>
|
||||
<li><a class="index-link" href="#m27">Sicherheitsmaßnahmen</a></li>
|
||||
<li>
|
||||
<a class="index-link" href="#m25"
|
||||
>Übermittlung von personenbezogenen Daten</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a class="index-link" href="#m24">Internationale Datentransfers</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="index-link" href="#m10">Rechte der betroffenen Personen</a>
|
||||
</li>
|
||||
<li><a class="index-link" href="#m134">Einsatz von Cookies</a></li>
|
||||
<li>
|
||||
<a class="index-link" href="#m225"
|
||||
>Bereitstellung des Onlineangebotes und Webhosting</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a class="index-link" href="#m182">Kontakt- und Anfragenverwaltung</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 id="m3">Verantwortlicher</h2>
|
||||
<p>
|
||||
Elmar Kresse<br />Philipp-Rosenthal-Straße 33<br />04103, Leipzig,
|
||||
Deutschland
|
||||
</p>
|
||||
E-Mail-Adresse:
|
||||
<p><a href="mailto:support@kresse.dev">support@kresse.dev</a></p>
|
||||
|
||||
<h2 id="mOverview">Übersicht der Verarbeitungen</h2>
|
||||
<p>
|
||||
Die nachfolgende Übersicht fasst die Arten der verarbeiteten Daten und
|
||||
die Zwecke ihrer Verarbeitung zusammen und verweist auf die betroffenen
|
||||
Personen.
|
||||
</p>
|
||||
<h3>Arten der verarbeiteten Daten</h3>
|
||||
<ul>
|
||||
<li>Kontaktdaten.</li>
|
||||
<li>Inhaltsdaten.</li>
|
||||
<li>Nutzungsdaten.</li>
|
||||
<li>Meta-, Kommunikations- und Verfahrensdaten.</li>
|
||||
</ul>
|
||||
<h3>Kategorien betroffener Personen</h3>
|
||||
<ul>
|
||||
<li>Kommunikationspartner.</li>
|
||||
<li>Nutzer.</li>
|
||||
</ul>
|
||||
<h3>Zwecke der Verarbeitung</h3>
|
||||
<ul>
|
||||
<li>Kontaktanfragen und Kommunikation.</li>
|
||||
<li>Sicherheitsmaßnahmen.</li>
|
||||
<li>Verwaltung und Beantwortung von Anfragen.</li>
|
||||
<li>Feedback.</li>
|
||||
<li>
|
||||
Bereitstellung unseres Onlineangebotes und Nutzerfreundlichkeit.
|
||||
</li>
|
||||
<li>Informationstechnische Infrastruktur.</li>
|
||||
</ul>
|
||||
<h2 id="m2427">Maßgebliche Rechtsgrundlagen</h2>
|
||||
<p>
|
||||
<strong>Maßgebliche Rechtsgrundlagen nach der DSGVO: </strong>Im
|
||||
Folgenden erhalten Sie eine Übersicht der Rechtsgrundlagen der DSGVO,
|
||||
auf deren Basis wir personenbezogene Daten verarbeiten. Bitte nehmen Sie
|
||||
zur Kenntnis, dass neben den Regelungen der DSGVO nationale
|
||||
Datenschutzvorgaben in Ihrem bzw. unserem Wohn- oder Sitzland gelten
|
||||
können. Sollten ferner im Einzelfall speziellere Rechtsgrundlagen
|
||||
maßgeblich sein, teilen wir Ihnen diese in der Datenschutzerklärung mit.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Einwilligung (Art. 6 Abs. 1 S. 1 lit. a) DSGVO)</strong> - Die
|
||||
betroffene Person hat ihre Einwilligung in die Verarbeitung der sie
|
||||
betreffenden personenbezogenen Daten für einen spezifischen Zweck oder
|
||||
mehrere bestimmte Zwecke gegeben.
|
||||
</li>
|
||||
<li>
|
||||
<strong
|
||||
>Berechtigte Interessen (Art. 6 Abs. 1 S. 1 lit. f) DSGVO)</strong
|
||||
>
|
||||
- Die Verarbeitung ist zur Wahrung der berechtigten Interessen des
|
||||
Verantwortlichen oder eines Dritten erforderlich, sofern nicht die
|
||||
Interessen oder Grundrechte und Grundfreiheiten der betroffenen
|
||||
Person, die den Schutz personenbezogener Daten erfordern, überwiegen.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>Nationale Datenschutzregelungen in Deutschland: </strong
|
||||
>Zusätzlich zu den Datenschutzregelungen der DSGVO gelten nationale
|
||||
Regelungen zum Datenschutz in Deutschland. Hierzu gehört insbesondere
|
||||
das Gesetz zum Schutz vor Missbrauch personenbezogener Daten bei der
|
||||
Datenverarbeitung (Bundesdatenschutzgesetz – BDSG). Das BDSG enthält
|
||||
insbesondere Spezialregelungen zum Recht auf Auskunft, zum Recht auf
|
||||
Löschung, zum Widerspruchsrecht, zur Verarbeitung besonderer Kategorien
|
||||
personenbezogener Daten, zur Verarbeitung für andere Zwecke und zur
|
||||
Übermittlung sowie automatisierten Entscheidungsfindung im Einzelfall
|
||||
einschließlich Profiling. Ferner können Landesdatenschutzgesetze der
|
||||
einzelnen Bundesländer zur Anwendung gelangen.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Hinweis auf Geltung DSGVO und Schweizer DSG: </strong>Diese
|
||||
Datenschutzhinweise dienen sowohl der Informationserteilung nach dem
|
||||
schweizerischen Bundesgesetz über den Datenschutz (Schweizer DSG) als
|
||||
auch nach der Datenschutzgrundverordnung (DSGVO). Aus diesem Grund
|
||||
bitten wir Sie zu beachten, dass aufgrund der breiteren räumlichen
|
||||
Anwendung und Verständlichkeit die Begriffe der DSGVO verwendet werden.
|
||||
Insbesondere statt der im Schweizer DSG verwendeten Begriffe
|
||||
„Bearbeitung" von „Personendaten", "überwiegendes Interesse" und
|
||||
"besonders schützenswerte Personendaten" werden die in der DSGVO
|
||||
verwendeten Begriffe „Verarbeitung" von „personenbezogenen Daten" sowie
|
||||
"berechtigtes Interesse" und "besondere Kategorien von Daten" verwendet.
|
||||
Die gesetzliche Bedeutung der Begriffe wird jedoch im Rahmen der Geltung
|
||||
des Schweizer DSG weiterhin nach dem Schweizer DSG bestimmt.
|
||||
</p>
|
||||
|
||||
<h2 id="m27">Sicherheitsmaßnahmen</h2>
|
||||
<p>
|
||||
Wir treffen nach Maßgabe der gesetzlichen Vorgaben unter
|
||||
Berücksichtigung des Stands der Technik, der Implementierungskosten und
|
||||
der Art, des Umfangs, der Umstände und der Zwecke der Verarbeitung sowie
|
||||
der unterschiedlichen Eintrittswahrscheinlichkeiten und des Ausmaßes der
|
||||
Bedrohung der Rechte und Freiheiten natürlicher Personen geeignete
|
||||
technische und organisatorische Maßnahmen, um ein dem Risiko
|
||||
angemessenes Schutzniveau zu gewährleisten.
|
||||
</p>
|
||||
<p>
|
||||
Zu den Maßnahmen gehören insbesondere die Sicherung der Vertraulichkeit,
|
||||
Integrität und Verfügbarkeit von Daten durch Kontrolle des physischen
|
||||
und elektronischen Zugangs zu den Daten als auch des sie betreffenden
|
||||
Zugriffs, der Eingabe, der Weitergabe, der Sicherung der Verfügbarkeit
|
||||
und ihrer Trennung. Des Weiteren haben wir Verfahren eingerichtet, die
|
||||
eine Wahrnehmung von Betroffenenrechten, die Löschung von Daten und
|
||||
Reaktionen auf die Gefährdung der Daten gewährleisten. Ferner
|
||||
berücksichtigen wir den Schutz personenbezogener Daten bereits bei der
|
||||
Entwicklung bzw. Auswahl von Hardware, Software sowie Verfahren
|
||||
entsprechend dem Prinzip des Datenschutzes, durch Technikgestaltung und
|
||||
durch datenschutzfreundliche Voreinstellungen.
|
||||
</p>
|
||||
<p>
|
||||
TLS-Verschlüsselung (https): Um Ihre via unserem Online-Angebot
|
||||
übermittelten Daten zu schützen, nutzen wir eine TLS-Verschlüsselung.
|
||||
Sie erkennen derart verschlüsselte Verbindungen an dem Präfix https://
|
||||
in der Adresszeile Ihres Browsers.
|
||||
</p>
|
||||
|
||||
<h2 id="m25">Übermittlung von personenbezogenen Daten</h2>
|
||||
<p>
|
||||
Im Rahmen unserer Verarbeitung von personenbezogenen Daten kommt es vor,
|
||||
dass die Daten an andere Stellen, Unternehmen, rechtlich selbstständige
|
||||
Organisationseinheiten oder Personen übermittelt oder sie ihnen
|
||||
gegenüber offengelegt werden. Zu den Empfängern dieser Daten können
|
||||
z. B. mit IT-Aufgaben beauftragte Dienstleister oder Anbieter von
|
||||
Diensten und Inhalten, die in eine Webseite eingebunden werden, gehören.
|
||||
In solchen Fällen beachten wir die gesetzlichen Vorgaben und schließen
|
||||
insbesondere entsprechende Verträge bzw. Vereinbarungen, die dem Schutz
|
||||
Ihrer Daten dienen, mit den Empfängern Ihrer Daten ab.
|
||||
</p>
|
||||
|
||||
<h2 id="m24">Internationale Datentransfers</h2>
|
||||
<p>
|
||||
Datenverarbeitung in Drittländern: Sofern wir Daten in einem Drittland
|
||||
(d. h., außerhalb der Europäischen Union (EU), des Europäischen
|
||||
Wirtschaftsraums (EWR)) verarbeiten oder die Verarbeitung im Rahmen der
|
||||
Inanspruchnahme von Diensten Dritter oder der Offenlegung bzw.
|
||||
Übermittlung von Daten an andere Personen, Stellen oder Unternehmen
|
||||
stattfindet, erfolgt dies nur im Einklang mit den gesetzlichen Vorgaben.
|
||||
Sofern das Datenschutzniveau in dem Drittland mittels eines
|
||||
Angemessenheitsbeschlusses anerkannt wurde (Art. 45 DSGVO), dient dieser
|
||||
als Grundlage des Datentransfers. Im Übrigen erfolgen Datentransfers nur
|
||||
dann, wenn das Datenschutzniveau anderweitig gesichert ist, insbesondere
|
||||
durch Standardvertragsklauseln (Art. 46 Abs. 2 lit. c) DSGVO),
|
||||
ausdrückliche Einwilligung oder im Fall vertraglicher oder gesetzlich
|
||||
erforderlicher Übermittlung (Art. 49 Abs. 1 DSGVO). Im Übrigen teilen
|
||||
wir Ihnen die Grundlagen der Drittlandübermittlung bei den einzelnen
|
||||
Anbietern aus dem Drittland mit, wobei die Angemesenheitsbeschlüsse als
|
||||
Grundlagen vorrangig gelten. Informationen zu Drittlandtransfers und
|
||||
vorliegenden Angemessenheitsbeschlüssen können dem Informationsangebot
|
||||
der EU-Kommission entnommen werden:
|
||||
<a
|
||||
href="https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection_de"
|
||||
target="_blank"
|
||||
>https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection_de.</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
EU-US Trans-Atlantic Data Privacy Framework: Im Rahmen des sogenannten
|
||||
„Data Privacy Framework" (DPF) hat die EU-Kommission das
|
||||
Datenschutzniveau ebenfalls für bestimmte Unternehmen aus den USA im
|
||||
Rahmen der Angemessenheitsbeschlusses vom 10.07.2023 als sicher
|
||||
anerkannt. Die Liste der zertifizierten Unternehmen als auch weitere
|
||||
Informationen zu dem DPF können Sie der Webseite des Handelsministeriums
|
||||
der USA unter
|
||||
<a href="https://www.dataprivacyframework.gov/" target="_blank"
|
||||
>https://www.dataprivacyframework.gov/</a
|
||||
>
|
||||
(in Englisch) entnehmen. Wir informieren Sie im Rahmen der
|
||||
Datenschutzhinweise welche von uns eingesetzten Diensteanbieter unter
|
||||
dem Data Privacy Framework zertifiziert sind.
|
||||
</p>
|
||||
|
||||
<h2 id="m10">Rechte der betroffenen Personen</h2>
|
||||
<p>
|
||||
Rechte der betroffenen Personen aus der DSGVO: Ihnen stehen als
|
||||
Betroffene nach der DSGVO verschiedene Rechte zu, die sich insbesondere
|
||||
aus Art. 15 bis 21 DSGVO ergeben:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong
|
||||
>Widerspruchsrecht: Sie haben das Recht, aus Gründen, die sich aus
|
||||
Ihrer besonderen Situation ergeben, jederzeit gegen die Verarbeitung
|
||||
der Sie betreffenden personenbezogenen Daten, die aufgrund von Art.
|
||||
6 Abs. 1 lit. e oder f DSGVO erfolgt, Widerspruch einzulegen; dies
|
||||
gilt auch für ein auf diese Bestimmungen gestütztes Profiling.
|
||||
Werden die Sie betreffenden personenbezogenen Daten verarbeitet, um
|
||||
Direktwerbung zu betreiben, haben Sie das Recht, jederzeit
|
||||
Widerspruch gegen die Verarbeitung der Sie betreffenden
|
||||
personenbezogenen Daten zum Zwecke derartiger Werbung einzulegen;
|
||||
dies gilt auch für das Profiling, soweit es mit solcher
|
||||
Direktwerbung in Verbindung steht.</strong
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Widerrufsrecht bei Einwilligungen:</strong> Sie haben das
|
||||
Recht, erteilte Einwilligungen jederzeit zu widerrufen.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Auskunftsrecht:</strong> Sie haben das Recht, eine Bestätigung
|
||||
darüber zu verlangen, ob betreffende Daten verarbeitet werden und auf
|
||||
Auskunft über diese Daten sowie auf weitere Informationen und Kopie
|
||||
der Daten entsprechend den gesetzlichen Vorgaben.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Recht auf Berichtigung:</strong> Sie haben entsprechend den
|
||||
gesetzlichen Vorgaben das Recht, die Vervollständigung der Sie
|
||||
betreffenden Daten oder die Berichtigung der Sie betreffenden
|
||||
unrichtigen Daten zu verlangen.
|
||||
</li>
|
||||
<li>
|
||||
<strong
|
||||
>Recht auf Löschung und Einschränkung der Verarbeitung:</strong
|
||||
>
|
||||
Sie haben nach Maßgabe der gesetzlichen Vorgaben das Recht, zu
|
||||
verlangen, dass Sie betreffende Daten unverzüglich gelöscht werden,
|
||||
bzw. alternativ nach Maßgabe der gesetzlichen Vorgaben eine
|
||||
Einschränkung der Verarbeitung der Daten zu verlangen.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Recht auf Datenübertragbarkeit:</strong> Sie haben das Recht,
|
||||
Sie betreffende Daten, die Sie uns bereitgestellt haben, nach Maßgabe
|
||||
der gesetzlichen Vorgaben in einem strukturierten, gängigen und
|
||||
maschinenlesbaren Format zu erhalten oder deren Übermittlung an einen
|
||||
anderen Verantwortlichen zu fordern.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Beschwerde bei Aufsichtsbehörde:</strong> Sie haben
|
||||
unbeschadet eines anderweitigen verwaltungsrechtlichen oder
|
||||
gerichtlichen Rechtsbehelfs das Recht auf Beschwerde bei einer
|
||||
Aufsichtsbehörde, insbesondere in dem Mitgliedstaat ihres gewöhnlichen
|
||||
Aufenthaltsorts, ihres Arbeitsplatzes oder des Orts des mutmaßlichen
|
||||
Verstoßes, wenn Sie der Ansicht sind, dass die Verarbeitung der Sie
|
||||
betreffenden personenbezogenen Daten gegen die Vorgaben der DSGVO
|
||||
verstößt.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="m134">Einsatz von Cookies</h2>
|
||||
<p>
|
||||
Cookies sind kleine Textdateien, bzw. sonstige Speichervermerke, die
|
||||
Informationen auf Endgeräten speichern und Informationen aus den
|
||||
Endgeräten auslesen. Z. B. um den Login-Status in einem Nutzerkonto,
|
||||
einen Warenkorbinhalt in einem E-Shop, die aufgerufenen Inhalte oder
|
||||
verwendete Funktionen eines Onlineangebotes speichern. Cookies können
|
||||
ferner zu unterschiedlichen Zwecken eingesetzt werden, z. B. zu Zwecken
|
||||
der Funktionsfähigkeit, Sicherheit und Komfort von Onlineangeboten sowie
|
||||
der Erstellung von Analysen der Besucherströme.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Hinweise zur Einwilligung: </strong>Wir setzen Cookies im
|
||||
Einklang mit den gesetzlichen Vorschriften ein. Daher holen wir von den
|
||||
Nutzern eine vorhergehende Einwilligung ein, außer wenn diese gesetzlich
|
||||
nicht gefordert ist. Eine Einwilligung ist insbesondere nicht notwendig,
|
||||
wenn das Speichern und das Auslesen der Informationen, also auch von
|
||||
Cookies, unbedingt erforderlich sind, um dem den Nutzern einen von ihnen
|
||||
ausdrücklich gewünschten Telemediendienst (also unser Onlineangebot) zur
|
||||
Verfügung zu stellen. Zu den unbedingt erforderlichen Cookies gehören in
|
||||
der Regel Cookies mit Funktionen, die der Anzeige und Lauffähigkeit des
|
||||
Onlineangebotes , dem Lastausgleich, der Sicherheit, der Speicherung der
|
||||
Präferenzen und Auswahlmöglichkeiten der Nutzer oder ähnlichen mit der
|
||||
Bereitstellung der Haupt- und Nebenfunktionen des von den Nutzern
|
||||
angeforderten Onlineangebotes zusammenhängenden Zwecken dienen. Die
|
||||
widerrufliche Einwilligung wird gegenüber den Nutzern deutlich
|
||||
kommuniziert und enthält die Informationen zu der jeweiligen
|
||||
Cookie-Nutzung.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Hinweise zu datenschutzrechtlichen Rechtsgrundlagen: </strong
|
||||
>Auf welcher datenschutzrechtlichen Rechtsgrundlage wir die
|
||||
personenbezogenen Daten der Nutzer mit Hilfe von Cookies verarbeiten,
|
||||
hängt davon ab, ob wir Nutzer um eine Einwilligung bitten. Falls die
|
||||
Nutzer einwilligen, ist die Rechtsgrundlage der Verarbeitung Ihrer Daten
|
||||
die erklärte Einwilligung. Andernfalls werden die mithilfe von Cookies
|
||||
verarbeiteten Daten auf Grundlage unserer berechtigten Interessen (z. B.
|
||||
an einem betriebswirtschaftlichen Betrieb unseres Onlineangebotes und
|
||||
Verbesserung seiner Nutzbarkeit) verarbeitet oder, wenn dies im Rahmen
|
||||
der Erfüllung unserer vertraglichen Pflichten erfolgt, wenn der Einsatz
|
||||
von Cookies erforderlich ist, um unsere vertraglichen Verpflichtungen zu
|
||||
erfüllen. Zu welchen Zwecken die Cookies von uns verarbeitet werden,
|
||||
darüber klären wir im Laufe dieser Datenschutzerklärung oder im Rahmen
|
||||
von unseren Einwilligungs- und Verarbeitungsprozessen auf.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Speicherdauer: </strong>Im Hinblick auf die Speicherdauer werden
|
||||
die folgenden Arten von Cookies unterschieden:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong
|
||||
>Temporäre Cookies (auch: Session- oder Sitzungs-Cookies):</strong
|
||||
> Temporäre Cookies werden spätestens gelöscht, nachdem ein Nutzer ein
|
||||
Online-Angebot verlassen und sein Endgerät (z. B. Browser oder mobile
|
||||
Applikation) geschlossen hat.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Permanente Cookies:</strong> Permanente Cookies bleiben auch
|
||||
nach dem Schließen des Endgerätes gespeichert. So können
|
||||
beispielsweise der Login-Status gespeichert oder bevorzugte Inhalte
|
||||
direkt angezeigt werden, wenn der Nutzer eine Website erneut besucht.
|
||||
Ebenso können die mit Hilfe von Cookies erhobenen Daten der Nutzer zur
|
||||
Reichweitenmessung verwendet werden. Sofern wir Nutzern keine
|
||||
expliziten Angaben zur Art und Speicherdauer von Cookies mitteilen
|
||||
(z. B. im Rahmen der Einholung der Einwilligung), sollten Nutzer davon
|
||||
ausgehen, dass Cookies permanent sind und die Speicherdauer bis zu
|
||||
zwei Jahre betragen kann.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong
|
||||
>Allgemeine Hinweise zum Widerruf und Widerspruch (sog. "Opt-Out"): </strong
|
||||
>Nutzer können die von ihnen abgegebenen Einwilligungen jederzeit
|
||||
widerrufen und der Verarbeitung entsprechend den gesetzlichen Vorgaben
|
||||
widersprechen. Hierzu können Nutzer unter anderem die Verwendung von
|
||||
Cookies in den Einstellungen ihres Browsers einschränken (wobei dadurch
|
||||
auch die Funktionalität unseres Onlineangebotes eingeschränkt sein
|
||||
kann). Ein Widerspruch gegen die Verwendung von Cookies zu
|
||||
Online-Marketing-Zwecken kann auch über die Websites
|
||||
<a href="https://optout.aboutads.info/" target="_new"
|
||||
>https://optout.aboutads.info</a
|
||||
>
|
||||
und
|
||||
<a href="https://www.youronlinechoices.com/" target="_new"
|
||||
>https://www.youronlinechoices.com/</a
|
||||
>
|
||||
erklärt werden.
|
||||
</p>
|
||||
<ul class="m-elements">
|
||||
<li class="">
|
||||
<strong>Rechtsgrundlagen:</strong> Berechtigte Interessen (Art. 6 Abs.
|
||||
1 S. 1 lit. f) DSGVO). Einwilligung (Art. 6 Abs. 1 S. 1 lit. a)
|
||||
DSGVO).
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong
|
||||
>Weitere Hinweise zu Verarbeitungsprozessen, Verfahren und
|
||||
Diensten:</strong
|
||||
>
|
||||
</p>
|
||||
<ul class="m-elements">
|
||||
<li>
|
||||
<strong
|
||||
>Verarbeitung von Cookie-Daten auf Grundlage einer Einwilligung: </strong
|
||||
>Wir setzen ein Verfahren zum Cookie-Einwilligungs-Management ein, in
|
||||
dessen Rahmen die Einwilligungen der Nutzer in den Einsatz von
|
||||
Cookies, bzw. der im Rahmen des
|
||||
Cookie-Einwilligungs-Management-Verfahrens genannten Verarbeitungen
|
||||
und Anbieter eingeholt sowie von den Nutzern verwaltet und widerrufen
|
||||
werden können. Hierbei wird die Einwilligungserklärung gespeichert, um
|
||||
deren Abfrage nicht erneut wiederholen zu müssen und die Einwilligung
|
||||
entsprechend der gesetzlichen Verpflichtung nachweisen zu können. Die
|
||||
Speicherung kann serverseitig und/oder in einem Cookie (sogenanntes
|
||||
Opt-In-Cookie, bzw. mithilfe vergleichbarer Technologien) erfolgen, um
|
||||
die Einwilligung einem Nutzer, bzw. dessen Gerät zuordnen zu können.
|
||||
Vorbehaltlich individueller Angaben zu den Anbietern von
|
||||
Cookie-Management-Diensten, gelten die folgenden Hinweise: Die Dauer
|
||||
der Speicherung der Einwilligung kann bis zu zwei Jahren betragen.
|
||||
Hierbei wird ein pseudonymer Nutzer-Identifikator gebildet und mit dem
|
||||
Zeitpunkt der Einwilligung, Angaben zur Reichweite der Einwilligung
|
||||
(z. B. welche Kategorien von Cookies und/oder Diensteanbieter) sowie
|
||||
dem Browser, System und verwendeten Endgerät gespeichert;
|
||||
<span class=""
|
||||
><strong>Rechtsgrundlagen:</strong> Einwilligung (Art. 6 Abs. 1 S. 1
|
||||
lit. a) DSGVO).</span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 id="m225">Bereitstellung des Onlineangebotes und Webhosting</h2>
|
||||
<p>
|
||||
Wir verarbeiten die Daten der Nutzer, um ihnen unsere Online-Dienste zur
|
||||
Verfügung stellen zu können. Zu diesem Zweck verarbeiten wir die
|
||||
IP-Adresse des Nutzers, die notwendig ist, um die Inhalte und Funktionen
|
||||
unserer Online-Dienste an den Browser oder das Endgerät der Nutzer zu
|
||||
übermitteln.
|
||||
</p>
|
||||
<ul class="m-elements">
|
||||
<li>
|
||||
<strong>Verarbeitete Datenarten:</strong> Nutzungsdaten (z. B.
|
||||
besuchte Webseiten, Interesse an Inhalten, Zugriffszeiten); Meta-,
|
||||
Kommunikations- und Verfahrensdaten (z. .B. IP-Adressen, Zeitangaben,
|
||||
Identifikationsnummern, Einwilligungsstatus).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Betroffene Personen:</strong> Nutzer (z. .B.
|
||||
Webseitenbesucher, Nutzer von Onlinediensten).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Zwecke der Verarbeitung:</strong> Bereitstellung unseres
|
||||
Onlineangebotes und Nutzerfreundlichkeit; Informationstechnische
|
||||
Infrastruktur (Betrieb und Bereitstellung von Informationssystemen und
|
||||
technischen Geräten (Computer, Server etc.).). Sicherheitsmaßnahmen.
|
||||
</li>
|
||||
<li class="">
|
||||
<strong>Rechtsgrundlagen:</strong> Berechtigte Interessen (Art. 6 Abs.
|
||||
1 S. 1 lit. f) DSGVO).
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong
|
||||
>Weitere Hinweise zu Verarbeitungsprozessen, Verfahren und
|
||||
Diensten:</strong
|
||||
>
|
||||
</p>
|
||||
<ul class="m-elements">
|
||||
<li>
|
||||
<strong
|
||||
>Bereitstellung Onlineangebot auf eigener/ dedizierter
|
||||
Serverhardware: </strong
|
||||
>Für die Bereitstellung unseres Onlineangebotes nutzen wir von uns
|
||||
betriebene Serverhardware sowie den damit verbundenen Speicherplatz,
|
||||
die Rechenkapazität und die Software;
|
||||
<span class=""
|
||||
><strong>Rechtsgrundlagen:</strong> Berechtigte Interessen (Art. 6
|
||||
Abs. 1 S. 1 lit. f) DSGVO).</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Erhebung von Zugriffsdaten und Logfiles: </strong>Der Zugriff
|
||||
auf unser Onlineangebot wird in Form von so genannten
|
||||
"Server-Logfiles" protokolliert. Zu den Serverlogfiles können die
|
||||
Adresse und Name der abgerufenen Webseiten und Dateien, Datum und
|
||||
Uhrzeit des Abrufs, übertragene Datenmengen, Meldung über
|
||||
erfolgreichen Abruf, Browsertyp nebst Version, das Betriebssystem des
|
||||
Nutzers, Referrer URL (die zuvor besuchte Seite) und im Regelfall
|
||||
IP-Adressen und der anfragende Provider gehören. Die Serverlogfiles
|
||||
können zum einen zu Zwecken der Sicherheit eingesetzt werden, z. B.,
|
||||
um eine Überlastung der Server zu vermeiden (insbesondere im Fall von
|
||||
missbräuchlichen Angriffen, sogenannten DDoS-Attacken) und zum
|
||||
anderen, um die Auslastung der Server und ihre Stabilität
|
||||
sicherzustellen;
|
||||
<span class=""
|
||||
><strong>Rechtsgrundlagen:</strong> Berechtigte Interessen (Art. 6
|
||||
Abs. 1 S. 1 lit. f) DSGVO). </span
|
||||
><strong>Löschung von Daten:</strong> Logfile-Informationen werden für
|
||||
die Dauer von maximal 30 Tagen gespeichert und danach gelöscht oder
|
||||
anonymisiert. Daten, deren weitere Aufbewahrung zu Beweiszwecken
|
||||
erforderlich ist, sind bis zur endgültigen Klärung des jeweiligen
|
||||
Vorfalls von der Löschung ausgenommen.
|
||||
</li>
|
||||
</ul>
|
||||
<h2 id="m182">Kontakt- und Anfragenverwaltung</h2>
|
||||
<p>
|
||||
Bei der Kontaktaufnahme mit uns (z. B. per Post, Kontaktformular,
|
||||
E-Mail, Telefon oder via soziale Medien) sowie im Rahmen bestehender
|
||||
Nutzer- und Geschäftsbeziehungen werden die Angaben der anfragenden
|
||||
Personen verarbeitet soweit dies zur Beantwortung der Kontaktanfragen
|
||||
und etwaiger angefragter Maßnahmen erforderlich ist.
|
||||
</p>
|
||||
<ul class="m-elements">
|
||||
<li>
|
||||
<strong>Verarbeitete Datenarten:</strong> Kontaktdaten (z. B. E-Mail,
|
||||
Telefonnummern); Inhaltsdaten (z. B. Eingaben in Onlineformularen);
|
||||
Nutzungsdaten (z. B. besuchte Webseiten, Interesse an Inhalten,
|
||||
Zugriffszeiten); Meta-, Kommunikations- und Verfahrensdaten (z. .B.
|
||||
IP-Adressen, Zeitangaben, Identifikationsnummern,
|
||||
Einwilligungsstatus).
|
||||
</li>
|
||||
<li><strong>Betroffene Personen:</strong> Kommunikationspartner.</li>
|
||||
<li>
|
||||
<strong>Zwecke der Verarbeitung:</strong> Kontaktanfragen und
|
||||
Kommunikation; Verwaltung und Beantwortung von Anfragen; Feedback
|
||||
(z.B. Sammeln von Feedback via Online-Formular). Bereitstellung
|
||||
unseres Onlineangebotes und Nutzerfreundlichkeit.
|
||||
</li>
|
||||
<li class="">
|
||||
<strong>Rechtsgrundlagen:</strong> Berechtigte Interessen (Art. 6 Abs.
|
||||
1 S. 1 lit. f) DSGVO).
|
||||
</li>
|
||||
</ul>
|
||||
<p class="seal">
|
||||
<a
|
||||
href="https://datenschutz-generator.de/"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
title="Rechtstext von Dr. Schwenke - für weitere Informationen bitte anklicken."
|
||||
>Erstellt mit kostenlosem Datenschutz-Generator.de von Dr. Thomas
|
||||
Schwenke</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
41
frontend/src/main.ts
Normal file
41
frontend/src/main.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
import PrimeVue from "primevue/config";
|
||||
import Button from "primevue/button";
|
||||
import Dropdown from "primevue/dropdown";
|
||||
import Menubar from "primevue/menubar";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Card from "primevue/card";
|
||||
import DataView from "primevue/dataview";
|
||||
import ToggleButton from "primevue/togglebutton";
|
||||
import "primevue/resources/themes/viva-dark/theme.css";
|
||||
import "primeicons/primeicons.css";
|
||||
import "primeflex/primeflex.css";
|
||||
import router from "./router";
|
||||
import TabView from "primevue/tabview";
|
||||
import TabPanel from "primevue/tabpanel";
|
||||
import { createPinia } from "pinia";
|
||||
import MultiSelect from "primevue/multiselect";
|
||||
import ToastService from "primevue/toastservice";
|
||||
import Toast from "primevue/toast";
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
|
||||
app.use(PrimeVue);
|
||||
app.use(router);
|
||||
app.use(ToastService);
|
||||
app.use(pinia);
|
||||
app.component("Button", Button);
|
||||
app.component("Menubar", Menubar);
|
||||
app.component("Dropdown", Dropdown);
|
||||
app.component("InputText", InputText);
|
||||
app.component("Card", Card);
|
||||
app.component("DataView", DataView);
|
||||
app.component("ToggleButton", ToggleButton);
|
||||
app.component("TabView", TabView);
|
||||
app.component("TabPanel", TabPanel);
|
||||
app.component("MultiSelect", MultiSelect);
|
||||
app.component("Toast", Toast);
|
||||
app.mount("#app");
|
6
frontend/src/model/module.ts
Normal file
6
frontend/src/model/module.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export class Module {
|
||||
constructor(
|
||||
public Name: string,
|
||||
public Course: string,
|
||||
) {}
|
||||
}
|
49
frontend/src/router/index.ts
Normal file
49
frontend/src/router/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import Faq from "../components/FaqPage.vue";
|
||||
import CourseSelection from "../components/CourseSelection.vue";
|
||||
import AdditionalModules from "../components/AdditionalModules.vue";
|
||||
import CalendarLink from "../components/CalendarLink.vue";
|
||||
import Impress from "../components/Impress.vue";
|
||||
import PrivacyPolicy from "../components/PrivacyPolicy.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "course-selection",
|
||||
component: CourseSelection,
|
||||
},
|
||||
{
|
||||
path: "/faq",
|
||||
name: "faq",
|
||||
component: Faq,
|
||||
},
|
||||
{
|
||||
path: "/additional-modules",
|
||||
name: "additional-modules",
|
||||
component: AdditionalModules,
|
||||
},
|
||||
{
|
||||
path: "/calendar-link",
|
||||
name: "calendar-link",
|
||||
component: CalendarLink,
|
||||
},
|
||||
{
|
||||
path: "/privacy-policy",
|
||||
name: "privacy-policy",
|
||||
component: PrivacyPolicy,
|
||||
},
|
||||
{
|
||||
path: "/impress",
|
||||
name: "impress",
|
||||
component: Impress,
|
||||
},
|
||||
{
|
||||
path: "/:catchAll(.*)",
|
||||
redirect: "/",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
18
frontend/src/store/moduleStore.ts
Normal file
18
frontend/src/store/moduleStore.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Module } from "../model/module.ts";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
const moduleStore = defineStore("moduleStore", {
|
||||
state: () => ({
|
||||
modules: [] as Module[],
|
||||
}),
|
||||
actions: {
|
||||
addModule(module: Module) {
|
||||
this.modules.push(module);
|
||||
},
|
||||
removeModule(module: Module) {
|
||||
this.modules.splice(this.modules.indexOf(module), 1);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default moduleStore;
|
17
frontend/src/store/tokenStore.ts
Normal file
17
frontend/src/store/tokenStore.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
const tokenStore = defineStore("tokenStore", {
|
||||
state: () => ({
|
||||
token: "",
|
||||
}),
|
||||
actions: {
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
},
|
||||
removeToken() {
|
||||
this.token = "";
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default tokenStore;
|
3
frontend/src/style.css
Normal file
3
frontend/src/style.css
Normal file
@ -0,0 +1,3 @@
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
}
|
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
31
frontend/tsconfig.json
Normal file
31
frontend/tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
16
frontend/tsconfig.node.json
Normal file
16
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"playwright.config.*"
|
||||
]
|
||||
}
|
14
frontend/vite.config.ts
Normal file
14
frontend/vite.config.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
host: true,
|
||||
port: 8000,
|
||||
watch: {
|
||||
usePolling: true,
|
||||
}
|
||||
}
|
||||
});
|
26
main.go
26
main.go
@ -1,26 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := pocketbase.New()
|
||||
|
||||
addRoutes(app)
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
// serves static files from the provided public dir (if exists)
|
||||
e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public"), false))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := app.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 285 KiB |
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
57
reverseproxy.conf
Normal file
57
reverseproxy.conf
Normal file
@ -0,0 +1,57 @@
|
||||
#user nobody;
|
||||
worker_processes 1;
|
||||
|
||||
#error_log logs/error.log;
|
||||
#error_log logs/error.log notice;
|
||||
#error_log logs/error.log info;
|
||||
|
||||
#pid logs/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
# '$status $body_bytes_sent "$http_referer" '
|
||||
# '"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/proxy_access.log;
|
||||
error_log /var/log/nginx/proxy_error.log;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
#keepalive_timeout 0;
|
||||
keepalive_timeout 180s;
|
||||
send_timeout 180s;
|
||||
|
||||
#gzip on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name frontend;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://htwkalender-backend:8090;
|
||||
client_max_body_size 20m;
|
||||
proxy_connect_timeout 600s;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
send_timeout 600s;
|
||||
}
|
||||
|
||||
location /_ {
|
||||
proxy_pass http://htwkalender-backend:8090;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://htwkalender-frontend:8000;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"htwk-planner/model"
|
||||
)
|
||||
|
||||
func SaveGroups(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) error {
|
||||
for _, group := range seminarGroup {
|
||||
record := models.NewRecord(collection)
|
||||
record.Set("university", group.University)
|
||||
record.Set("shortcut", group.GroupShortcut)
|
||||
record.Set("groupId", group.GroupId)
|
||||
record.Set("course", group.Course)
|
||||
record.Set("faculty", group.Faculty)
|
||||
record.Set("facultyId", group.FacultyId)
|
||||
if err := app.Dao().SaveRecord(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAllCourses(app *pocketbase.PocketBase) []string {
|
||||
|
||||
var courses []struct {
|
||||
CourseShortcut string `db:"course" json:"course"`
|
||||
}
|
||||
|
||||
// get all rooms from event records in the events collection
|
||||
err := app.Dao().DB().Select("course").From("groups").All(&courses)
|
||||
if err != nil {
|
||||
print("Error while getting groups from database: ", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var courseArray []string
|
||||
|
||||
for _, course := range courses {
|
||||
courseArray = append(courseArray, course.CourseShortcut)
|
||||
}
|
||||
|
||||
return courseArray
|
||||
}
|
Reference in New Issue
Block a user