Initial commit

This commit is contained in:
Jim Martens 2023-10-01 10:39:17 +02:00
commit 810218cedf
66 changed files with 25422 additions and 0 deletions

16
.browserlist Normal file
View File

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

86
.drone.yml Normal file
View File

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

16
.editorconfig Normal file
View File

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

42
.gitignore vendored Normal file
View File

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

17
.htaccess Normal file
View File

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

27
README.md Normal file
View File

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

172
angular.json Normal file
View File

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

30
ngsw-config.json Normal file
View File

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

23968
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

55
package.json Normal file
View File

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

View File

@ -0,0 +1,3 @@
<app-navigation>
<router-outlet></router-outlet>
</app-navigation>

View File

View File

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

13
src/app/app.component.ts Normal file
View File

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

58
src/app/app.config.ts Normal file
View File

@ -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'
})
]
}

22
src/app/app.routes.ts Normal file
View File

@ -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'
}
];

View File

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

View File

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

View File

@ -0,0 +1,7 @@
@use 'mixins';
@include mixins.centralColumnLayout();
p {
@include mixins.justifiedText();
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
@use 'mixins';
@include mixins.centralColumnLayout();
p {
@include mixins.justifiedText();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
export enum MessageType {
UNAUTHENTICATED,
UNAUTHORIZED,
INTERNAL_SERVER_ERROR,
SERVICE_WORKER_ERROR
}

View File

@ -0,0 +1,6 @@
import {MessageType} from "./message-type";
export interface Message {
text: string;
type: MessageType;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
<div class="container">
<h1>Permission Denied to Access this Page!!!!</h1>
</div>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
@use 'mixins';
@include mixins.centralColumnLayout();
p {
@include mixins.justifiedText();
}

View File

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

View File

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

0
src/assets/.gitkeep Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

View File

@ -0,0 +1,8 @@
<!doctype html>
<html lang="en">
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>

View File

@ -0,0 +1,6 @@
export const environment = {
backendURL: "http://localhost:12000/timetable/v1",
keycloakURL: "https://id.2martens.de",
realm: "2martens",
clientId: "tsw-timetable-frontend"
};

View File

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

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

19
src/index.html Normal file
View File

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

8
src/main.ts Normal file
View File

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

59
src/manifest.webmanifest Normal file
View File

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

57
src/sass/_mixins.scss Normal file
View File

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

4
src/sass/styles.scss Normal file
View File

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

16
tsconfig.app.json Normal file
View File

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

34
tsconfig.json Normal file
View File

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

15
tsconfig.spec.json Normal file
View File

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