added frontend and updated backend with docker, wrote some initial instructions

This commit is contained in:
Elmar Kresse
2023-09-19 21:34:29 +02:00
parent b8a638a5fe
commit c051995823
87 changed files with 4987 additions and 1574 deletions

58
.idea/codeStyles/Project.xml generated Normal file
View 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
View 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
View 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>

View File

@ -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
` ➜ Admin UI: http://127.0.0.1:8090/_/`
➜ 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
```
- 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.

View File

18
backend/Dockerfile Normal file
View 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
View 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

View File

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

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

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

View File

@ -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": {}
}
]

View File

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

View File

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

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

View File

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

View File

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

View File

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

@ -0,0 +1 @@
node_modules

14
frontend/.eslintrc.cjs Normal file
View 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
View 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?

View File

@ -0,0 +1 @@
{}

7
frontend/Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM node:latest
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY ./ ./

20
frontend/README.md Normal file
View 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

View File

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

File diff suppressed because it is too large Load Diff

33
frontend/package.json Normal file
View 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

Binary file not shown.

35
frontend/public/vite.svg Normal file
View 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
View 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>

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

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

View 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

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

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

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

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

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

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

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

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

View File

@ -0,0 +1,6 @@
export class Module {
constructor(
public Name: string,
public Course: string,
) {}
}

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

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

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

@ -0,0 +1,3 @@
body {
font-family: var(--font-family);
}

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

31
frontend/tsconfig.json Normal file
View 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"
}
]
}

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

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 285 KiB

View File

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

View File

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