Initial commit
|
@ -0,0 +1,16 @@
|
||||||
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# For the full list of supported browsers by the Angular framework, please see:
|
||||||
|
# https://angular.io/guide/browser-support
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
last 2 Chrome versions
|
||||||
|
last 1 Firefox version
|
||||||
|
last 2 Edge major versions
|
||||||
|
last 2 Safari major versions
|
||||||
|
last 2 iOS major versions
|
||||||
|
Firefox ESR
|
|
@ -0,0 +1,86 @@
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
clone:
|
||||||
|
skip_verify: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: restore-cache
|
||||||
|
privileged: true
|
||||||
|
pull: always
|
||||||
|
image: 2martens/drone-volume-cache
|
||||||
|
settings:
|
||||||
|
restore: true
|
||||||
|
mount:
|
||||||
|
- ./node_modules
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
- name: build
|
||||||
|
pull: always
|
||||||
|
image: node:18-alpine
|
||||||
|
commands:
|
||||||
|
- npm install
|
||||||
|
- npm run build:production
|
||||||
|
- cp /drone/src/.htaccess /drone/src/dist/tsw-timetable-frontend/
|
||||||
|
- name: rebuild-cache
|
||||||
|
privileged: true
|
||||||
|
image: 2martens/drone-volume-cache
|
||||||
|
settings:
|
||||||
|
rebuild: true
|
||||||
|
mount:
|
||||||
|
- ./node_modules
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
- name: deploy
|
||||||
|
pull: always
|
||||||
|
image: 2martens/drone-rsync
|
||||||
|
settings:
|
||||||
|
hosts: [ "gienah.uberspace.de" ]
|
||||||
|
user: wahlfron
|
||||||
|
source: /drone/src/dist/tsw-timetable-frontend/.
|
||||||
|
target: ~/tmp/build
|
||||||
|
recursive: true
|
||||||
|
delete: true
|
||||||
|
port: 22
|
||||||
|
key:
|
||||||
|
from_secret: rsync_key
|
||||||
|
script:
|
||||||
|
- shopt -s dotglob
|
||||||
|
- rm -rf tmp/old.build
|
||||||
|
- mkdir tmp/old.build
|
||||||
|
- cp -r html/* tmp/old.build/
|
||||||
|
- rm -rf html/*
|
||||||
|
- cp -r tmp/build/* html/
|
||||||
|
- rm -rf tmp/build
|
||||||
|
- name: notify
|
||||||
|
pull: always
|
||||||
|
image: 2martens/drone-email
|
||||||
|
environment:
|
||||||
|
EMAIL_USERNAME:
|
||||||
|
from_secret: email_username
|
||||||
|
EMAIL_PASSWORD:
|
||||||
|
from_secret: email_password
|
||||||
|
settings:
|
||||||
|
host: howell.uberspace.de
|
||||||
|
port: 587
|
||||||
|
from: Drone <drone@2martens.de>
|
||||||
|
secrets: [email_username, email_password]
|
||||||
|
when:
|
||||||
|
status: [ failure ]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
host:
|
||||||
|
path: /var/lib/drone/cache
|
||||||
|
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
|
@ -0,0 +1,42 @@
|
||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
|
@ -0,0 +1,17 @@
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteBase /
|
||||||
|
|
||||||
|
RewriteCond %{HTTP:Accept-Language} ^de [NC]
|
||||||
|
RewriteRule ^$ /de/ [L,R=302]
|
||||||
|
|
||||||
|
RewriteCond %{HTTP:Accept-Language} !^de [NC]
|
||||||
|
RewriteRule ^$ /en/ [L,R=302]
|
||||||
|
|
||||||
|
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} -d
|
||||||
|
RewriteRule ^.*$ - [NC,L]
|
||||||
|
|
||||||
|
RewriteRule ^de de/index.html [L]
|
||||||
|
RewriteRule ^en en/index.html [L]
|
||||||
|
</IfModule>
|
|
@ -0,0 +1,27 @@
|
||||||
|
# ModuleFrontend
|
||||||
|
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.1.4.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
|
@ -0,0 +1,172 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"tsw-timetable-frontend": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"i18n": {
|
||||||
|
"sourceLocale": "en",
|
||||||
|
"locales": {
|
||||||
|
"de": {
|
||||||
|
"translation": "src/locale/messages.de.xlf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/tsw-timetable-frontend",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/deeppurple-amber.css",
|
||||||
|
"src/sass/styles.scss"
|
||||||
|
],
|
||||||
|
"stylePreprocessorOptions": {
|
||||||
|
"includePaths": [
|
||||||
|
"src/sass"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": [],
|
||||||
|
"allowedCommonJsDependencies": [
|
||||||
|
"base64-js",
|
||||||
|
"js-sha256"
|
||||||
|
],
|
||||||
|
"localize": true,
|
||||||
|
"serviceWorker": true,
|
||||||
|
"ngswConfigPath": "ngsw-config.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"productionDebug": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": true,
|
||||||
|
"localize": ["de"]
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"namedChunks": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.development.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"localize": ["de"]
|
||||||
|
},
|
||||||
|
"developmentRemote": {
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"namedChunks": true,
|
||||||
|
"localize": ["de"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "tsw-timetable-frontend:build:production"
|
||||||
|
},
|
||||||
|
"productionDebug": {
|
||||||
|
"browserTarget": "tsw-timetable-frontend:build:productionDebug"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"browserTarget": "tsw-timetable-frontend:build:development"
|
||||||
|
},
|
||||||
|
"developmentRemote": {
|
||||||
|
"browserTarget": "tsw-timetable-frontend:build:developmentRemote"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "tsw-timetable-frontend:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/deeppurple-amber.css",
|
||||||
|
"src/sass/styles.scss"
|
||||||
|
],
|
||||||
|
"stylePreprocessorOptions": {
|
||||||
|
"includePaths": [
|
||||||
|
"src/sass"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
||||||
|
"index": "/index.html",
|
||||||
|
"assetGroups": [
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"installMode": "prefetch",
|
||||||
|
"resources": {
|
||||||
|
"files": [
|
||||||
|
"/favicon.ico",
|
||||||
|
"/index.html",
|
||||||
|
"/manifest.webmanifest",
|
||||||
|
"/*.css",
|
||||||
|
"/*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "assets",
|
||||||
|
"installMode": "lazy",
|
||||||
|
"updateMode": "prefetch",
|
||||||
|
"resources": {
|
||||||
|
"files": [
|
||||||
|
"/assets/**",
|
||||||
|
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"name": "tsw-timetable-frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"startRemote": "ng serve -c developmentRemote",
|
||||||
|
"build": "ng build",
|
||||||
|
"build:development": "ng build -c development",
|
||||||
|
"build:development:stats": "ng build -c development --stats-json",
|
||||||
|
"build:production": "ng build -c production",
|
||||||
|
"build:production:sourcemap": "ng build -c productionDebug",
|
||||||
|
"build:production:stats": "ng build -c production --stats-json",
|
||||||
|
"analyze": "webpack-bundle-analyzer dist/tsw-timetable-frontend/de/stats.json",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test",
|
||||||
|
"extract-i18n": "ng extract-i18n --output-path src/locale"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^16.2.1",
|
||||||
|
"@angular/cdk": "^16.2.1",
|
||||||
|
"@angular/common": "^16.2.1",
|
||||||
|
"@angular/compiler": "^16.2.1",
|
||||||
|
"@angular/core": "^16.2.1",
|
||||||
|
"@angular/forms": "^16.2.1",
|
||||||
|
"@angular/material": "^16.2.1",
|
||||||
|
"@angular/platform-browser": "^16.2.1",
|
||||||
|
"@angular/platform-browser-dynamic": "^16.2.1",
|
||||||
|
"@angular/router": "^16.2.1",
|
||||||
|
"@angular/service-worker": "^16.2.1",
|
||||||
|
"@ngrx/effects": "^16.2.0",
|
||||||
|
"@ngrx/store": "^16.2.0",
|
||||||
|
"keycloak-angular": "^14.0.0",
|
||||||
|
"rxjs": "~7.8.1",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"zone.js": "~0.13.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^16.2.0",
|
||||||
|
"@angular/cli": "~16.2.0",
|
||||||
|
"@angular/compiler-cli": "^16.2.1",
|
||||||
|
"@angular/localize": "^16.2.1",
|
||||||
|
"@types/jasmine": "~4.3.0",
|
||||||
|
"jasmine-core": "~4.6.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.1.6",
|
||||||
|
"webpack-bundle-analyzer": "^4.9.0",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<app-navigation>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</app-navigation>
|
|
@ -0,0 +1,24 @@
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {AppComponent} from './app.component';
|
||||||
|
import {NavigationComponent} from "./navigation/navigation.component";
|
||||||
|
import {KeycloakAngularModule} from "keycloak-angular";
|
||||||
|
import {MatSidenavModule} from "@angular/material/sidenav";
|
||||||
|
import {NoopAnimationsModule} from "@angular/platform-browser/animations";
|
||||||
|
import {MatToolbarModule} from "@angular/material/toolbar";
|
||||||
|
import {MatListModule} from "@angular/material/list";
|
||||||
|
import {MatIconModule} from "@angular/material/icon";
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({
|
||||||
|
imports: [RouterTestingModule, KeycloakAngularModule, MatSidenavModule, NoopAnimationsModule,
|
||||||
|
MatToolbarModule, MatListModule, MatIconModule],
|
||||||
|
declarations: [AppComponent, NavigationComponent]
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {NavigationComponent} from "./navigation/navigation.component";
|
||||||
|
import {RouterOutlet} from "@angular/router";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [NavigationComponent, RouterOutlet]
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import {APP_INITIALIZER, ApplicationConfig, isDevMode} from "@angular/core";
|
||||||
|
import {KeycloakBearerInterceptor, KeycloakService} from "keycloak-angular";
|
||||||
|
import {Location} from "@angular/common";
|
||||||
|
import {provideRouter, withComponentInputBinding} from "@angular/router";
|
||||||
|
import {ROOT_ROUTES} from "./app.routes";
|
||||||
|
import {provideStore} from "@ngrx/store";
|
||||||
|
import {provideEffects} from "@ngrx/effects";
|
||||||
|
import {provideAnimations} from "@angular/platform-browser/animations";
|
||||||
|
import {HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi} from "@angular/common/http";
|
||||||
|
import {provideServiceWorker} from "@angular/service-worker";
|
||||||
|
import {environment} from "../environments/environment";
|
||||||
|
|
||||||
|
function initializeKeycloak(keycloak: KeycloakService, locationService: Location) {
|
||||||
|
return () =>
|
||||||
|
keycloak.init({
|
||||||
|
config: {
|
||||||
|
url: environment.keycloakURL,
|
||||||
|
realm: environment.realm,
|
||||||
|
clientId: environment.clientId,
|
||||||
|
},
|
||||||
|
initOptions: {
|
||||||
|
onLoad: 'check-sso',
|
||||||
|
silentCheckSsoRedirectUri: `${window.location.origin}${locationService.prepareExternalUrl('/assets/silent-check-sso.html')}`,
|
||||||
|
flow: "standard"
|
||||||
|
},
|
||||||
|
shouldAddToken: (request) => {
|
||||||
|
const {url} = request;
|
||||||
|
return url.startsWith(environment.backendURL);
|
||||||
|
},
|
||||||
|
loadUserProfileAtStartUp: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: initializeKeycloak,
|
||||||
|
multi: true,
|
||||||
|
deps: [KeycloakService, Location],
|
||||||
|
},
|
||||||
|
provideRouter(ROOT_ROUTES, withComponentInputBinding()),
|
||||||
|
provideStore(),
|
||||||
|
provideEffects(),
|
||||||
|
provideAnimations(),
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
KeycloakService,
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: KeycloakBearerInterceptor,
|
||||||
|
multi: true
|
||||||
|
},
|
||||||
|
provideServiceWorker('ngsw-worker.js', {
|
||||||
|
enabled: !isDevMode(),
|
||||||
|
registrationStrategy: 'registerWhenStable:30000'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import {Route} from "@angular/router";
|
||||||
|
|
||||||
|
export const ROOT_ROUTES: Route[] = [
|
||||||
|
{
|
||||||
|
path: 'permission-denied',
|
||||||
|
loadComponent: () => import("./permission-denied/permission-denied.component")
|
||||||
|
.then(mod => mod.PermissionDeniedComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'legal-notice',
|
||||||
|
loadComponent: () => import("./legal-notice/legal-notice.component").then(mod => mod.LegalNoticeComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'privacy-policy',
|
||||||
|
loadComponent: () => import("./privacy-policy/privacy-policy.component").then(mod => mod.PrivacyPolicyComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
loadComponent: () => import("./dashboard/dashboard.component").then(mod => mod.DashboardComponent),
|
||||||
|
pathMatch: 'full'
|
||||||
|
}
|
||||||
|
];
|
|
@ -0,0 +1,48 @@
|
||||||
|
import {ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
|
||||||
|
|
||||||
|
import {KeycloakAuthGuard, KeycloakService} from 'keycloak-angular';
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {Location} from "@angular/common";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AppAuthGuard extends KeycloakAuthGuard {
|
||||||
|
|
||||||
|
constructor(protected override readonly router: Router,
|
||||||
|
protected readonly keycloak: KeycloakService,
|
||||||
|
private readonly location: Location) {
|
||||||
|
super(router, keycloak);
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean | UrlTree> {
|
||||||
|
// Force the user to log in if currently unauthenticated.
|
||||||
|
if (!this.authenticated || this.keycloak.isTokenExpired()) {
|
||||||
|
await this.keycloak.login({
|
||||||
|
redirectUri: `${window.location.origin}${this.location.prepareExternalUrl(state.url)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the roles required from the route.
|
||||||
|
const requiredRoles = route.data['roles'];
|
||||||
|
|
||||||
|
let granted: boolean;
|
||||||
|
|
||||||
|
// Allow the user to proceed if no additional roles are required to access the route.
|
||||||
|
if (!(requiredRoles instanceof Array) || requiredRoles.length === 0) {
|
||||||
|
granted = true;
|
||||||
|
return granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the user to proceed if all the required roles are present.
|
||||||
|
granted = requiredRoles.every((role) => this.roles.includes(role));
|
||||||
|
|
||||||
|
// Routing user into permission denied view if they don't have necessary roles.
|
||||||
|
if (!granted) {
|
||||||
|
await this.router.navigate(['permission-denied']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<div class="row">
|
||||||
|
<h1 class="mat-headline-5 mainContentColumn" i18n="page title">Timetable</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="mainContentColumn">
|
||||||
|
<p class="mat-body-1" i18n="welcome text|A welcome to users">
|
||||||
|
TODO
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,7 @@
|
||||||
|
@use 'mixins';
|
||||||
|
|
||||||
|
@include mixins.centralColumnLayout();
|
||||||
|
|
||||||
|
p {
|
||||||
|
@include mixins.justifiedText();
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {DashboardComponent} from './dashboard.component';
|
||||||
|
|
||||||
|
describe('DashboardComponent', () => {
|
||||||
|
let component: DashboardComponent;
|
||||||
|
let fixture: ComponentFixture<DashboardComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [DashboardComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(DashboardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard',
|
||||||
|
templateUrl: './dashboard.component.html',
|
||||||
|
styleUrls: ['./dashboard.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class DashboardComponent {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
<div class="row">
|
||||||
|
<div class="mainContentColumn">
|
||||||
|
<h2 i18n>Information provided according to Sec. 5 German Telemedia Act (TMG):</h2>
|
||||||
|
<p i18n>Jim Richard Martens<br />
|
||||||
|
Flaßheide 45<br />
|
||||||
|
22525 Hamburg</p>
|
||||||
|
|
||||||
|
<h2 i18n>Contact:</h2>
|
||||||
|
<p i18n>Email: admin@2martens.de</p>
|
||||||
|
|
||||||
|
<h2 i18n>Responsible for contents acc. to Sec. 18, para. 2 German Federal Media Agreement
|
||||||
|
(MStV):</h2>
|
||||||
|
<p i18n>Jim Martens<br />
|
||||||
|
Flaßheide 45<br />
|
||||||
|
22525 Hamburg</p>
|
||||||
|
|
||||||
|
<h2 i18n>Liability for Contents</h2>
|
||||||
|
<p i18n>As service providers, we are liable for own contents of these websites according to Sec. 7, paragraph 1 German
|
||||||
|
Telemedia Act (TMG). However, according to Sec. 8 to 10 German Telemedia Act (TMG), service providers are not
|
||||||
|
obligated to permanently monitor submitted or stored information or to search for evidences that indicate illegal
|
||||||
|
activities.</p> <p i18n>Legal obligations to removing information or to blocking the use of information remain unchallenged.
|
||||||
|
In this case, liability is only possible at the time of knowledge about a specific violation of law. Illegal contents
|
||||||
|
will be removed immediately at the time we get knowledge of them.</p>
|
||||||
|
<h2 i18n>Liability for Links</h2>
|
||||||
|
<p i18n>Our offer includes links to external third party websites. We have no influence on the contents of those websites,
|
||||||
|
therefore we cannot guarantee for those contents. Providers or administrators of linked websites are always responsible
|
||||||
|
for their own contents.</p>
|
||||||
|
<p i18n>The linked websites had been checked for possible violations of law at the time of the establishment of the link.
|
||||||
|
Illegal contents were not detected at the time of the linking. A permanent monitoring of the contents of linked
|
||||||
|
websites cannot be imposed without reasonable indications that there has been a violation of law. Illegal links
|
||||||
|
will be removed immediately at the time we get knowledge of them.</p>
|
||||||
|
<h2 i18n>Copyright</h2>
|
||||||
|
<p i18n>Contents and compilations published on these websites by the providers are subject to German copyright laws.
|
||||||
|
Reproduction, editing, distribution as well as the use of any kind outside the scope of the copyright law require a
|
||||||
|
written permission of the author or originator. Downloads and copies of these websites are permitted for private use
|
||||||
|
only.<br /> The commercial use of our contents without permission of the originator is prohibited.</p>
|
||||||
|
<p i18n>Copyright laws of third parties are respected as long as the contents on these websites do not originate from the
|
||||||
|
provider. Contributions of third parties on this site are indicated as such. However, if you notice any violations of
|
||||||
|
copyright law, please inform us. Such contents will be removed immediately.</p>
|
||||||
|
<p> </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,7 @@
|
||||||
|
@use 'mixins';
|
||||||
|
|
||||||
|
@include mixins.centralColumnLayout();
|
||||||
|
|
||||||
|
p {
|
||||||
|
@include mixins.justifiedText();
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LegalNoticeComponent } from './legal-notice.component';
|
||||||
|
|
||||||
|
describe('LegalNoticeComponent', () => {
|
||||||
|
let component: LegalNoticeComponent;
|
||||||
|
let fixture: ComponentFixture<LegalNoticeComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [LegalNoticeComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(LegalNoticeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-legal-notice',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './legal-notice.component.html',
|
||||||
|
styleUrls: ['./legal-notice.component.scss']
|
||||||
|
})
|
||||||
|
export class LegalNoticeComponent {
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {MessagesService} from './messages.service';
|
||||||
|
import {provideMockStore} from "@ngrx/store/testing";
|
||||||
|
|
||||||
|
describe('MessagesService', () => {
|
||||||
|
let service: MessagesService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
provideMockStore()
|
||||||
|
]
|
||||||
|
});
|
||||||
|
service = TestBed.inject(MessagesService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {Store} from "@ngrx/store";
|
||||||
|
import {addMessageAction} from "./store/messages.actions";
|
||||||
|
import {MessageType} from "./model/message-type";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MessagesService {
|
||||||
|
private static UNAUTHENTICATED = $localize`You are not logged in which prevents data from loading`;
|
||||||
|
private static UNAUTHORIZED = $localize`You don't have sufficient authorization to view the data`;
|
||||||
|
private static INTERNAL_SERVER_ERROR = $localize`An internal server error occurred. Sorry for the inconvenience.`;
|
||||||
|
private static UNKNOWN_ERROR = $localize`An unknown error occurred. Sorry for the inconvenience.`;
|
||||||
|
private static SERVICE_WORKER_ERROR = $localize`An error with the service worker occurred. Please reload the page.`
|
||||||
|
|
||||||
|
constructor(private store: Store) {
|
||||||
|
}
|
||||||
|
|
||||||
|
logMessage(component: string, type: MessageType, details?: string) {
|
||||||
|
let text = component + ": ";
|
||||||
|
switch (type) {
|
||||||
|
case MessageType.UNAUTHENTICATED:
|
||||||
|
text += MessagesService.UNAUTHENTICATED;
|
||||||
|
break;
|
||||||
|
case MessageType.UNAUTHORIZED:
|
||||||
|
text += MessagesService.UNAUTHORIZED;
|
||||||
|
break;
|
||||||
|
case MessageType.INTERNAL_SERVER_ERROR:
|
||||||
|
text += MessagesService.INTERNAL_SERVER_ERROR;
|
||||||
|
break;
|
||||||
|
case MessageType.SERVICE_WORKER_ERROR:
|
||||||
|
text += MessagesService.SERVICE_WORKER_ERROR;
|
||||||
|
if (details != undefined) {
|
||||||
|
text += "Details: " + details;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text += MessagesService.UNKNOWN_ERROR;
|
||||||
|
}
|
||||||
|
this.store.dispatch(addMessageAction({message: {text, type}}))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export enum MessageType {
|
||||||
|
UNAUTHENTICATED,
|
||||||
|
UNAUTHORIZED,
|
||||||
|
INTERNAL_SERVER_ERROR,
|
||||||
|
SERVICE_WORKER_ERROR
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import {MessageType} from "./message-type";
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
text: string;
|
||||||
|
type: MessageType;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {messagesReducer, ReducerMessagesState} from "./messages.reducer";
|
||||||
|
import {ActionReducerMap, createFeatureSelector} from "@ngrx/store";
|
||||||
|
|
||||||
|
export const featureStateName = 'messagesFeature';
|
||||||
|
|
||||||
|
export interface MessagesState {
|
||||||
|
messages: ReducerMessagesState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messagesReducers: ActionReducerMap<MessagesState> = {
|
||||||
|
messages: messagesReducer,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMessagesFeatureState = createFeatureSelector<MessagesState>(
|
||||||
|
featureStateName
|
||||||
|
);
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {createAction, props} from "@ngrx/store";
|
||||||
|
import {Message} from "../model/message";
|
||||||
|
|
||||||
|
export enum ActionTypes {
|
||||||
|
AddMessage = '[Messages] Add Message',
|
||||||
|
AddMessageFinished = '[Messages] Add Message Finished',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addMessageAction = createAction(
|
||||||
|
ActionTypes.AddMessage,
|
||||||
|
props<{message: Message}>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const addMessageFinishedAction = createAction(
|
||||||
|
ActionTypes.AddMessageFinished
|
||||||
|
);
|
|
@ -0,0 +1,22 @@
|
||||||
|
import {MatSnackBar} from "@angular/material/snack-bar";
|
||||||
|
import {Actions, createEffect, ofType} from "@ngrx/effects";
|
||||||
|
import {addMessageAction, addMessageFinishedAction} from "./messages.actions";
|
||||||
|
import {map} from "rxjs";
|
||||||
|
import {inject, Injectable} from "@angular/core";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MessagesEffects {
|
||||||
|
constructor(private snackBar: MatSnackBar) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private actions$ = inject(Actions);
|
||||||
|
|
||||||
|
showSnackbar$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(addMessageAction),
|
||||||
|
map((action) => {
|
||||||
|
this.snackBar.open(action.message.text, "OK");
|
||||||
|
return addMessageFinishedAction();
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import {Message} from "../model/message";
|
||||||
|
import {createReducer, on} from "@ngrx/store";
|
||||||
|
import {addMessageAction, addMessageFinishedAction} from "./messages.actions";
|
||||||
|
|
||||||
|
export interface ReducerMessagesState {
|
||||||
|
displayedMessage: Message|undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ReducerMessagesState = {
|
||||||
|
displayedMessage: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messagesReducer = createReducer(
|
||||||
|
initialState,
|
||||||
|
on(addMessageAction, (state,
|
||||||
|
action) => ({
|
||||||
|
...state,
|
||||||
|
displayedMessage: action.message
|
||||||
|
})),
|
||||||
|
on(addMessageFinishedAction, (state) => ({
|
||||||
|
...state,
|
||||||
|
displayedMessage: undefined
|
||||||
|
}))
|
||||||
|
);
|
|
@ -0,0 +1,39 @@
|
||||||
|
<mat-sidenav-container class="sidenav-container">
|
||||||
|
<mat-sidenav #drawer class="sidenav"
|
||||||
|
[attr.role]="'dialog'"
|
||||||
|
[mode]="'side'">
|
||||||
|
<mat-toolbar i18n="title|Title of the sidebar menu">Menu</mat-toolbar>
|
||||||
|
<mat-nav-list>
|
||||||
|
<a mat-list-item routerLink="legal-notice" i18n="link name|The name of the legal notice page">Legal Notice</a>
|
||||||
|
<a mat-list-item routerLink="privacy-policy" i18n="link name|The name of the privacy policy page">Privacy
|
||||||
|
Policy</a>
|
||||||
|
</mat-nav-list>
|
||||||
|
</mat-sidenav>
|
||||||
|
<mat-sidenav-content>
|
||||||
|
<mat-toolbar color="primary">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Toggle sidenav"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="drawer.toggle()">
|
||||||
|
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
mat-icon-button
|
||||||
|
[routerLink]="''">
|
||||||
|
<mat-icon>home</mat-icon>
|
||||||
|
</button>
|
||||||
|
<span i18n="application title|The application title in the toolbar">Timetable</span>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<ng-container *ngIf="isLoggedIn$ | async; else loginButton">
|
||||||
|
<span class="logged-user" i18n>Logged in as {{loggedUserName$ | async}}</span>
|
||||||
|
<button mat-raised-button class="app-nav-icon" (click)="logout()" i18n>Logout</button>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #loginButton>
|
||||||
|
<button mat-raised-button class="app-nav-icon" (click)="login()" i18n>Login</button>
|
||||||
|
</ng-template>
|
||||||
|
</mat-toolbar>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</mat-sidenav-content>
|
||||||
|
</mat-sidenav-container>
|
|
@ -0,0 +1,25 @@
|
||||||
|
.sidenav-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav .mat-toolbar {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-toolbar.mat-primary {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logged-user {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||||
|
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {MatListModule} from '@angular/material/list';
|
||||||
|
import {MatSidenavModule} from '@angular/material/sidenav';
|
||||||
|
import {MatToolbarModule} from '@angular/material/toolbar';
|
||||||
|
|
||||||
|
import {NavigationComponent} from './navigation.component';
|
||||||
|
import {KeycloakAngularModule} from "keycloak-angular";
|
||||||
|
import {RouterTestingModule} from "@angular/router/testing";
|
||||||
|
|
||||||
|
describe('NavigationComponent', () => {
|
||||||
|
let component: NavigationComponent;
|
||||||
|
let fixture: ComponentFixture<NavigationComponent>;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [NavigationComponent],
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatListModule,
|
||||||
|
MatSidenavModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
KeycloakAngularModule,
|
||||||
|
RouterTestingModule
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NavigationComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compile', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,57 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {KeycloakService} from "keycloak-angular";
|
||||||
|
import {from, of, switchMap} from "rxjs";
|
||||||
|
import {ActivatedRoute, RouterLink} from "@angular/router";
|
||||||
|
import {MatSidenavModule} from "@angular/material/sidenav";
|
||||||
|
import {MatToolbarModule} from "@angular/material/toolbar";
|
||||||
|
import {MatListModule} from "@angular/material/list";
|
||||||
|
import {MatButtonModule} from "@angular/material/button";
|
||||||
|
import {MatIconModule} from "@angular/material/icon";
|
||||||
|
import {AsyncPipe, NgIf} from "@angular/common";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-navigation',
|
||||||
|
templateUrl: './navigation.component.html',
|
||||||
|
styleUrls: ['./navigation.component.scss'],
|
||||||
|
imports: [
|
||||||
|
MatSidenavModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
MatListModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
NgIf,
|
||||||
|
AsyncPipe,
|
||||||
|
RouterLink
|
||||||
|
],
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class NavigationComponent {
|
||||||
|
loggedUserName$;
|
||||||
|
isLoggedIn$;
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
constructor(private keycloakService: KeycloakService,
|
||||||
|
route: ActivatedRoute) {
|
||||||
|
this.url = route.snapshot.url.join('');
|
||||||
|
this.isLoggedIn$ = from(this.keycloakService.isLoggedIn());
|
||||||
|
this.loggedUserName$ = this.isLoggedIn$.pipe(
|
||||||
|
switchMap(loggedIn => {
|
||||||
|
if (loggedIn) {
|
||||||
|
return of(this.keycloakService.getUsername());
|
||||||
|
} else {
|
||||||
|
return of('');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
login(): void {
|
||||||
|
this.keycloakService.login({
|
||||||
|
redirectUri: window.location.origin + this.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
this.keycloakService.logout(window.location.origin);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="container">
|
||||||
|
<h1>Permission Denied to Access this Page!!!!</h1>
|
||||||
|
</div>
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {PermissionDeniedComponent} from './permission-denied.component';
|
||||||
|
|
||||||
|
describe('PermissionDeniedComponent', () => {
|
||||||
|
let component: PermissionDeniedComponent;
|
||||||
|
let fixture: ComponentFixture<PermissionDeniedComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [PermissionDeniedComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(PermissionDeniedComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-permission-denied',
|
||||||
|
templateUrl: './permission-denied.component.html',
|
||||||
|
styleUrls: ['./permission-denied.component.scss'],
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class PermissionDeniedComponent {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
<div class="row">
|
||||||
|
<div class="mainContentColumn">
|
||||||
|
<h2 i18n>1. An overview of data protection</h2>
|
||||||
|
<h3 i18n>General</h3>
|
||||||
|
<p i18n>The following gives a simple overview of what happens to your personal information when you visit our website.
|
||||||
|
Personal information is any data with which you could be personally identified. Detailed information on the subject
|
||||||
|
of data protection can be found in our privacy policy found below.</p>
|
||||||
|
<h3 i18n>Data collection on our website</h3>
|
||||||
|
<p><strong i18n>Who is responsible for the data collection on this website?</strong></p>
|
||||||
|
<p i18n>The data collected on this website are processed by the website operator. The operator's contact details can be found
|
||||||
|
in the website's required legal notice.</p>
|
||||||
|
<p><strong i18n>How do we collect your data?</strong></p>
|
||||||
|
<p i18n>Some data are collected when you provide it to us. This could, for example, be data you enter on a contact form.</p>
|
||||||
|
<p i18n>Other data are collected automatically by our IT systems when you visit the website. These data are primarily technical
|
||||||
|
data such as the browser and operating system you are using or when you accessed the page. These data are collected
|
||||||
|
automatically as soon as you enter our website.</p>
|
||||||
|
<p><strong i18n>What do we use your data for?</strong></p>
|
||||||
|
<p i18n>Part of the data is collected to ensure the proper functioning of the website. Other data can be used to analyze how
|
||||||
|
visitors use the site.</p>
|
||||||
|
<p><strong i18n>What rights do you have regarding your data?</strong></p>
|
||||||
|
<p i18n>You always have the right to request information about your stored data, its origin, its recipients, and the purpose
|
||||||
|
of its collection at no charge. You also have the right to request that it be corrected, blocked, or deleted. You can
|
||||||
|
contact us at any time using the address given in the legal notice if you have further questions about the issue of
|
||||||
|
privacy and data protection. You may also, of course, file a complaint with the competent regulatory authorities.</p>
|
||||||
|
<h2 i18n>2. General information and mandatory information</h2>
|
||||||
|
<h3 i18n>Data protection</h3>
|
||||||
|
<p i18n>The operators of this website take the protection of your personal data very seriously. We treat your personal data as
|
||||||
|
confidential and in accordance with the statutory data protection regulations and this privacy policy.</p>
|
||||||
|
<p i18n>If you use this website, various pieces of personal data will be collected. Personal information is any data with which
|
||||||
|
you could be personally identified. This privacy policy explains what information we collect and what we use it for.
|
||||||
|
It also explains how and for what purpose this happens.</p>
|
||||||
|
<p i18n>Please note that data transmitted via the internet (e.g. via email communication) may be subject to security breaches.
|
||||||
|
Complete protection of your data from third-party access is not possible.</p>
|
||||||
|
<h3 i18n>Notice concerning the party responsible for this website</h3>
|
||||||
|
<p i18n>The party responsible for processing data on this website is:</p>
|
||||||
|
<p i18n>Jim Martens<br />
|
||||||
|
Flaßheide 45<br />
|
||||||
|
22525 Hamburg</p>
|
||||||
|
|
||||||
|
<p i18n>Telephone: 04021082122<br />
|
||||||
|
Email: admin@2martens.de</p>
|
||||||
|
<p i18n>The responsible party is the natural or legal person who alone or jointly with others decides on the purposes and means
|
||||||
|
of processing personal data (names, email addresses, etc.).</p>
|
||||||
|
<h3 i18n>SSL or TLS encryption</h3>
|
||||||
|
<p i18n>This site uses SSL or TLS encryption for security reasons and for the protection of the
|
||||||
|
transmission of confidential content, such as the inquiries you send to us as the site operator. You can recognize an
|
||||||
|
encrypted connection in your browser's address line when it changes from "http://" to "https://" and the lock icon is
|
||||||
|
displayed in your browser's address bar.</p>
|
||||||
|
<p i18n>If SSL or TLS encryption is activated, the data you transfer to us cannot be read by third parties.</p>
|
||||||
|
<h3 i18n>Opposition to promotional emails</h3>
|
||||||
|
<p i18n>We hereby expressly prohibit the use of contact data published in the context
|
||||||
|
of website legal notice requirements with regard to sending promotional and informational materials not expressly
|
||||||
|
requested. The website operator reserves the right to take specific legal action if unsolicited advertising material,
|
||||||
|
such as email spam, is received.</p>
|
||||||
|
<h2 i18n>3. Data collection on our website</h2>
|
||||||
|
<h3 i18n>Server log files</h3>
|
||||||
|
<p i18n>The website provider automatically collects and stores information that your browser automatically transmits to us in
|
||||||
|
"server log files". These are:</p>
|
||||||
|
<ul>
|
||||||
|
<li i18n>Browser type and browser version</li>
|
||||||
|
<li i18n>Operating system used</li>
|
||||||
|
<li i18n>Referrer URL</li>
|
||||||
|
<li i18n>Host name of the accessing computer</li>
|
||||||
|
<li i18n>Time of the server request</li>
|
||||||
|
<li i18n>IP address</li>
|
||||||
|
</ul>
|
||||||
|
<p i18n>These data will not be combined with data from other sources.</p>
|
||||||
|
<p i18n>The basis for data processing is Art. 6 (1) (b) DSGVO, which allows the processing of data to fulfill a contract or
|
||||||
|
for measures preliminary to a contract.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,7 @@
|
||||||
|
@use 'mixins';
|
||||||
|
|
||||||
|
@include mixins.centralColumnLayout();
|
||||||
|
|
||||||
|
p {
|
||||||
|
@include mixins.justifiedText();
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PrivacyPolicyComponent } from './privacy-policy.component';
|
||||||
|
|
||||||
|
describe('PrivacyPolicyComponent', () => {
|
||||||
|
let component: PrivacyPolicyComponent;
|
||||||
|
let fixture: ComponentFixture<PrivacyPolicyComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [PrivacyPolicyComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(PrivacyPolicyComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-privacy-policy',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './privacy-policy.component.html',
|
||||||
|
styleUrls: ['./privacy-policy.component.scss']
|
||||||
|
})
|
||||||
|
export class PrivacyPolicyComponent {
|
||||||
|
|
||||||
|
}
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 711 B |
After Width: | Height: | Size: 857 B |
|
@ -0,0 +1,8 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
parent.postMessage(location.href, location.origin);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const environment = {
|
||||||
|
backendURL: "http://localhost:12000/timetable/v1",
|
||||||
|
keycloakURL: "https://id.2martens.de",
|
||||||
|
realm: "2martens",
|
||||||
|
clientId: "tsw-timetable-frontend"
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const environment = {
|
||||||
|
backendURL: "https://api.2martens.de/timetable/v1",
|
||||||
|
keycloakURL: "https://id.2martens.de",
|
||||||
|
realm: "2martens",
|
||||||
|
clientId: "tsw-timetable-frontend"
|
||||||
|
};
|
After Width: | Height: | Size: 948 B |
|
@ -0,0 +1,19 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title i18n="application title|Title of browser tab">Electoral Law</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
|
<meta name="theme-color" content="#1976d2">
|
||||||
|
</head>
|
||||||
|
<body class="mat-typography">
|
||||||
|
<app-root></app-root>
|
||||||
|
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,8 @@
|
||||||
|
/// <reference types="@angular/localize" />
|
||||||
|
|
||||||
|
import {bootstrapApplication} from "@angular/platform-browser";
|
||||||
|
import {AppComponent} from "./app/app.component";
|
||||||
|
import "@angular/localize/init";
|
||||||
|
import {appConfig} from "./app/app.config";
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));
|
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"name": "tsw-timetable-frontend",
|
||||||
|
"short_name": "tsw-timetable-frontend",
|
||||||
|
"theme_color": "#1976d2",
|
||||||
|
"background_color": "#fafafa",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "./",
|
||||||
|
"start_url": "./",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "assets/icons/icon-72x72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "assets/icons/icon-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "assets/icons/icon-128x128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "assets/icons/icon-144x144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "assets/icons/icon-152x152.png",
|
||||||
|
"sizes": "152x152",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "assets/icons/icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "assets/icons/icon-384x384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "assets/icons/icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
@mixin container() {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin flexDirection($value: row) {
|
||||||
|
flex-direction: $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin flexWrap($value: nowrap) {
|
||||||
|
flex-wrap: $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin justifyContent($value) {
|
||||||
|
justify-content: $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin alignItems($value) {
|
||||||
|
align-items: $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin order($order: 0) {
|
||||||
|
order: $order;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin flex($grow: 0, $shrink: 1, $basis: auto) {
|
||||||
|
flex: $grow $shrink $basis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
@include container();
|
||||||
|
@include flexDirection(row);
|
||||||
|
@include justifyContent(center);
|
||||||
|
@include alignItems(stretch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two-column layout with one main column and one sidebar column (left or right)
|
||||||
|
@mixin twoColumnLayout() {
|
||||||
|
.mainContentColumn {
|
||||||
|
@include flex(9, 0);
|
||||||
|
}
|
||||||
|
.sidebarColumn {
|
||||||
|
@include flex(3, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One column that does not grow but takes up 60% of the space
|
||||||
|
@mixin centralColumnLayout() {
|
||||||
|
.mainContentColumn {
|
||||||
|
@include flex(0, 1, 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mixin justifiedText() {
|
||||||
|
text-align: justify;
|
||||||
|
text-justify: inter-word;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
|
@ -0,0 +1,16 @@
|
||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": [
|
||||||
|
"@angular/localize"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine",
|
||||||
|
"@angular/localize"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|