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