first commit
This commit is contained in:
16
.editorconfig
Normal file
16
.editorconfig
Normal 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
|
||||||
21
.firebaserc
Normal file
21
.firebaserc
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"iwuzzler-web-app": {
|
||||||
|
"hosting": {
|
||||||
|
"frontend": [
|
||||||
|
"iwuzzler"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iwuzzler-develop-16bee": {
|
||||||
|
"hosting": {
|
||||||
|
"frontend": [
|
||||||
|
"iwuzzler-development"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"default": "iwuzzler-web-app"
|
||||||
|
}
|
||||||
|
}
|
||||||
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# Firebase
|
||||||
|
.firebase
|
||||||
|
*-debug.log
|
||||||
|
.runtimeconfig.json
|
||||||
49
.tfignore
Normal file
49
.tfignore
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#test
|
||||||
|
/testignore
|
||||||
|
test.txt
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Firebase
|
||||||
|
.firebase
|
||||||
|
*-debug.log
|
||||||
|
.runtimeconfig.json
|
||||||
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "iWuzzler",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "chrome",
|
||||||
|
"url": "http://localhost:4200",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Launch Chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "chrome",
|
||||||
|
"url": "http://localhost:4200",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
27
README.md
Normal file
27
README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Frontend
|
||||||
|
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.1.0.
|
||||||
|
|
||||||
|
## 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.
|
||||||
126
angular.json
Normal file
126
angular.json
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"frontend": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/frontend",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/indigo-pink.css",
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "5mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.development.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "frontend:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "frontend:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "frontend:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/indigo-pink.css",
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deploy": {
|
||||||
|
"builder": "@angular/fire:deploy",
|
||||||
|
"options": {
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "frontend:build:production",
|
||||||
|
"serveTarget": "frontend:serve:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "frontend:build:development",
|
||||||
|
"serveTarget": "frontend:serve:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
15
firebase-develop.json
Normal file
15
firebase-develop.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"hosting": [
|
||||||
|
{
|
||||||
|
"site": "iwuzzler-development",
|
||||||
|
"public": "dist/frontend/browser",
|
||||||
|
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "**",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
19
firebase.json
Normal file
19
firebase.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"hosting": [
|
||||||
|
{
|
||||||
|
"site": "iwuzzler",
|
||||||
|
"public": "dist/frontend/browser",
|
||||||
|
"ignore": [
|
||||||
|
"firebase.json",
|
||||||
|
"**/.*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
],
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "**",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
firestore.rules
Normal file
9
firestore.rules
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
|
||||||
|
service cloud.firestore {
|
||||||
|
match /databases/{database}/documents {
|
||||||
|
match /{document=**} {
|
||||||
|
allow read, write: if request.time < timestamp.date(2024, 3, 31);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}firestore.rules
|
||||||
14791
package-lock.json
generated
Normal file
14791
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
package.json
Normal file
49
package.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build --configuration=production",
|
||||||
|
"build:dev": "ng build --configuration=development",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"deploy": "firebase -P iwuzzler deploy",
|
||||||
|
"deploy:dev": "firebase -P iwuzzler-development -c firebase-develop.json deploy",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^17.1.0",
|
||||||
|
"@angular/cdk": "^17.2.1",
|
||||||
|
"@angular/common": "^17.1.0",
|
||||||
|
"@angular/compiler": "^17.1.0",
|
||||||
|
"@angular/core": "^17.1.0",
|
||||||
|
"@angular/fire": "^17.0.1",
|
||||||
|
"@angular/forms": "^17.1.0",
|
||||||
|
"@angular/material": "^17.2.1",
|
||||||
|
"@angular/platform-browser": "^17.1.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^17.1.0",
|
||||||
|
"@angular/router": "^17.1.0",
|
||||||
|
"@ngneat/hot-toast": "^7.0.0",
|
||||||
|
"@ngneat/until-destroy": "^10.0.0",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"firebase": "^10.8.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"zone.js": "~0.14.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^17.1.0",
|
||||||
|
"@angular/cli": "^17.1.0",
|
||||||
|
"@angular/compiler-cli": "^17.1.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"jasmine-core": "~5.1.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.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/app/angular-material/angular-material.module.ts
Normal file
34
src/app/angular-material/angular-material.module.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {MatCardModule} from '@angular/material/card';
|
||||||
|
import {MatSnackBarModule} from '@angular/material/snack-bar';
|
||||||
|
import {MatIcon, MatIconModule} from '@angular/material/icon';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatSnackBarModule,
|
||||||
|
MatIcon
|
||||||
|
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatSnackBarModule,
|
||||||
|
MatIcon]
|
||||||
|
})
|
||||||
|
export class AngularMaterialModule { }
|
||||||
0
src/app/app.component.css
Normal file
0
src/app/app.component.css
Normal file
1
src/app/app.component.html
Normal file
1
src/app/app.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<router-outlet></router-outlet>
|
||||||
29
src/app/app.component.spec.ts
Normal file
29
src/app/app.component.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AppComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should have the 'Frontend' title`, () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app.title).toEqual('Frontend');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, Frontend');
|
||||||
|
});
|
||||||
|
});
|
||||||
20
src/app/app.component.ts
Normal file
20
src/app/app.component.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import { AngularMaterialModule } from './angular-material/angular-material.module';
|
||||||
|
import { AuthService } from './auth/authService/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [RouterOutlet, AngularMaterialModule],
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrl: './app.component.css',
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'Frontend';
|
||||||
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.authService.reloadUserUntilVerified();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/app/app.config.ts
Normal file
30
src/app/app.config.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
|
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
|
||||||
|
import { getAuth, provideAuth } from '@angular/fire/auth';
|
||||||
|
import { getFirestore, provideFirestore } from '@angular/fire/firestore';
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideRouter(routes),
|
||||||
|
provideAnimationsAsync(),
|
||||||
|
importProvidersFrom(
|
||||||
|
provideFirebaseApp(() =>
|
||||||
|
initializeApp({
|
||||||
|
projectId: environment.firebase.projectId,
|
||||||
|
appId: environment.firebase.appId,
|
||||||
|
storageBucket: environment.firebase.storageBucket,
|
||||||
|
apiKey: environment.firebase.apiKey,
|
||||||
|
authDomain: environment.firebase.authDomain,
|
||||||
|
messagingSenderId: environment.firebase.messagingSenderId,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
importProvidersFrom(provideAuth(() => getAuth())),
|
||||||
|
importProvidersFrom(provideFirestore(() => getFirestore())),
|
||||||
|
provideAnimationsAsync(),
|
||||||
|
],
|
||||||
|
};
|
||||||
50
src/app/app.routes.ts
Normal file
50
src/app/app.routes.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { LoginPasswordComponent } from './auth/login-password/login-password.component';
|
||||||
|
import { SignupPasswordComponent } from './auth/signup-password/signup-password.component';
|
||||||
|
import { UserProfileComponent } from './user-profile/user-profile.component';
|
||||||
|
import {
|
||||||
|
canActivate,
|
||||||
|
redirectLoggedInTo,
|
||||||
|
redirectUnauthorizedTo,
|
||||||
|
} from '@angular/fire/auth-guard';
|
||||||
|
import { PendingScoreGuard } from './score/pending-score.guard';
|
||||||
|
import { ScoreTableComponent } from './score-table/score-table/score-table.component';
|
||||||
|
import { ResetPwComponent } from './auth/resetPw/resetPw.component';
|
||||||
|
import { EmailVerificationModalComponent } from './auth/email-verification-modal/email-verification-modal.component';
|
||||||
|
import { FinishSignupComponent } from './auth/finish-signup/finish-signup.component';
|
||||||
|
import { SignupComponent } from './auth/signup/signup.component';
|
||||||
|
import { LoginComponent } from './auth/login/login.component';
|
||||||
|
|
||||||
|
const redirectToHome = () => redirectLoggedInTo(['score-table']);
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'score-table',
|
||||||
|
component: ScoreTableComponent,
|
||||||
|
},
|
||||||
|
{ path: 'login', component: LoginComponent, ...canActivate(redirectToHome) },
|
||||||
|
{
|
||||||
|
path: 'finish-signup',
|
||||||
|
component: FinishSignupComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'user-profile',
|
||||||
|
component: UserProfileComponent,
|
||||||
|
canActivate: [PendingScoreGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'user-profile/:id',
|
||||||
|
component: UserProfileComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reset-password',
|
||||||
|
component: ResetPwComponent,
|
||||||
|
...canActivate(redirectToHome),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'email-verification',
|
||||||
|
component: EmailVerificationModalComponent,
|
||||||
|
},
|
||||||
|
|
||||||
|
{ path: '', redirectTo: '/score-table', pathMatch: 'full' },
|
||||||
|
];
|
||||||
194
src/app/auth/authService/auth.service.ts
Normal file
194
src/app/auth/authService/auth.service.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable, catchError, from, map, throwError } from 'rxjs';
|
||||||
|
import {
|
||||||
|
Auth,
|
||||||
|
authState,
|
||||||
|
createUserWithEmailAndPassword,
|
||||||
|
signInWithEmailAndPassword,
|
||||||
|
sendEmailVerification,
|
||||||
|
sendPasswordResetEmail,
|
||||||
|
sendSignInLinkToEmail,
|
||||||
|
isSignInWithEmailLink,
|
||||||
|
signInWithEmailLink,
|
||||||
|
} from '@angular/fire/auth';
|
||||||
|
import { AuthErrorCodes } from 'firebase/auth';
|
||||||
|
import { NotifierService } from '../../shared/notifierService/notifier.service';
|
||||||
|
import { User } from '../model/user.model';
|
||||||
|
import { PendingActionsService } from '../../score/pending-actions.service';
|
||||||
|
import { UserService } from '../../user-profile/user.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
currentUser$ = authState(this.auth);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private auth: Auth,
|
||||||
|
private notifierService: NotifierService,
|
||||||
|
private pendingService: PendingActionsService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
reloadUserUntilVerified() {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
const user = this.getCurrentUser();
|
||||||
|
if (user) {
|
||||||
|
await user.reload();
|
||||||
|
if (user.emailVerified) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
login(username: string, password: string) {
|
||||||
|
return from(signInWithEmailAndPassword(this.auth, username, password)).pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
let errorMessage = 'Ein unbekannter Fehler ist aufgetreten.';
|
||||||
|
if (error.code === AuthErrorCodes.INVALID_LOGIN_CREDENTIALS) {
|
||||||
|
errorMessage = 'Email oder Passwort ungültig.';
|
||||||
|
}
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loginWithEmail(): Observable<User> {
|
||||||
|
if (isSignInWithEmailLink(this.auth, window.location.href)) {
|
||||||
|
let email = window.localStorage.getItem('emailForSignIn');
|
||||||
|
if (!email) {
|
||||||
|
email = window.prompt('Please provide your email for confirmation');
|
||||||
|
}
|
||||||
|
if (email) {
|
||||||
|
const [firstName, lastName] = this.extractNamesFromEmail(email);
|
||||||
|
return from(
|
||||||
|
signInWithEmailLink(this.auth, email, window.location.href).then(
|
||||||
|
(result: any) => {
|
||||||
|
window.localStorage.removeItem('emailForSignIn');
|
||||||
|
const newUser: User = {
|
||||||
|
uid: result.user.uid,
|
||||||
|
email: email,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
firmenposition: null,
|
||||||
|
photoUrl: null,
|
||||||
|
wins: null,
|
||||||
|
};
|
||||||
|
this.pendingService.addUserRefToQRCodeData(
|
||||||
|
newUser.uid,
|
||||||
|
this.pendingService.getTempQRId()
|
||||||
|
);
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
let errorMessage = 'Ein unbekannter Fehler ist aufgetreten.';
|
||||||
|
if (error.code === AuthErrorCodes.EMAIL_EXISTS) {
|
||||||
|
errorMessage = 'Diese E-Mail-Adresse existiert bereits.';
|
||||||
|
}
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return throwError(() => new Error('Kein Link zum Anmelden gefunden.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
return from(this.auth.signOut());
|
||||||
|
}
|
||||||
|
|
||||||
|
signUp(email: string, score?: string, uid?: string) {
|
||||||
|
let finishSignUpUrl = '/finish-signup';
|
||||||
|
if (score && uid) {
|
||||||
|
finishSignUpUrl += `?score=${score}&uid=${uid}`;
|
||||||
|
}
|
||||||
|
console.log('finishSignUpUrl=', finishSignUpUrl);
|
||||||
|
|
||||||
|
const actionCodeSettings = {
|
||||||
|
url: window.location.origin + finishSignUpUrl,
|
||||||
|
handleCodeInApp: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
sendSignInLinkToEmail(this.auth, email, actionCodeSettings)
|
||||||
|
.then(() => {
|
||||||
|
window.localStorage.setItem('emailForSignIn', email);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
let errorMessage = 'Ein unbekannter Fehler ist aufgetreten.';
|
||||||
|
if (error.code === AuthErrorCodes.EMAIL_EXISTS) {
|
||||||
|
errorMessage = 'Diese E-Mail-Adresse existiert bereits.';
|
||||||
|
}
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
signUpPassword(email: string, password: string) {
|
||||||
|
return from(
|
||||||
|
createUserWithEmailAndPassword(this.auth, email, password)
|
||||||
|
).pipe(
|
||||||
|
map((userCredential) => {
|
||||||
|
// Send verification email after successful sign up
|
||||||
|
this.sendVerificationEmail();
|
||||||
|
return userCredential;
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
let errorMessage = 'Ein unbekannter Fehler ist aufgetreten.';
|
||||||
|
if (error.code === AuthErrorCodes.EMAIL_EXISTS) {
|
||||||
|
errorMessage = 'Diese E-Mail-Adresse existiert bereits.';
|
||||||
|
}
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUser() {
|
||||||
|
return this.auth.currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPassword(email: string) {
|
||||||
|
return sendPasswordResetEmail(this.auth, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isVerifiedUser$(): Observable<boolean> {
|
||||||
|
return this.currentUser$.pipe(
|
||||||
|
map((user) => (user ? user.emailVerified : false))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendVerificationEmail() {
|
||||||
|
sendEmailVerification(this.getCurrentUser()!, {
|
||||||
|
url: window.location.origin + '/email-verification',
|
||||||
|
}).catch((error) => {
|
||||||
|
this.notifierService.showNotification(
|
||||||
|
'Zu viele Versuche. Bitte probiere es später noch einmal.',
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractNamesFromEmail(email: string): [string, string] {
|
||||||
|
const sanitizedEmail = email.replace(/.\d|\.external/g, '');
|
||||||
|
const [fullName] = sanitizedEmail.split('@');
|
||||||
|
const names = fullName.split('.');
|
||||||
|
|
||||||
|
// Vor- und Nachname grossgeschrieben
|
||||||
|
const capitalize = (name: string) =>
|
||||||
|
name
|
||||||
|
.split('-')
|
||||||
|
.map(
|
||||||
|
(part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
||||||
|
)
|
||||||
|
.join('-');
|
||||||
|
|
||||||
|
// Capitalize both the first name and last name parts
|
||||||
|
const firstName = names.length > 0 ? capitalize(names[0]) : '';
|
||||||
|
const lastName =
|
||||||
|
names.length > 1 ? capitalize(names.slice(1).join('-')) : '';
|
||||||
|
|
||||||
|
return [firstName, lastName];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { EmailVerificationModalComponent } from './email-verification-modal.component';
|
||||||
|
|
||||||
|
describe('EmailVerificationModalComponent', () => {
|
||||||
|
let component: EmailVerificationModalComponent;
|
||||||
|
let fixture: ComponentFixture<EmailVerificationModalComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [EmailVerificationModalComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(EmailVerificationModalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { CustomDialogComponent } from '../../shared/custom-dialog/custom-dialog.component';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-email-verification-modal',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './email-verification-modal.component.html',
|
||||||
|
styleUrl: './email-verification-modal.component.css',
|
||||||
|
})
|
||||||
|
export class EmailVerificationModalComponent {
|
||||||
|
constructor(private dialog: MatDialog, private router: Router) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.dialog.open(CustomDialogComponent, {
|
||||||
|
data: {
|
||||||
|
title: 'E-Mail-Verifizierungs-Hinweis',
|
||||||
|
bodyText:
|
||||||
|
'Falls du dich mit deinem Privathandy + Arbeitsprofil angemeldet hast, kehre bitte wieder in deinen Privat-Browser zurück und aktualisiere die iWuzzler Page.',
|
||||||
|
onConfirm: () => {
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/app/auth/finish-signup/finish-signup.component.html
Normal file
7
src/app/auth/finish-signup/finish-signup.component.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<div class="flex">
|
||||||
|
<div class="m-auto kick-motto">
|
||||||
|
<div *ngIf="isLoading" class="loading-spinner-correct">
|
||||||
|
<app-loading-spinner></app-loading-spinner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
66
src/app/auth/finish-signup/finish-signup.component.ts
Normal file
66
src/app/auth/finish-signup/finish-signup.component.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { AuthService } from '../authService/auth.service';
|
||||||
|
import { LoadingSpinnerComponent } from '../../shared/loading-spinner/loading-spinner.component';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { from, map, switchMap } from 'rxjs';
|
||||||
|
import { PendingActionsService } from '../../score/pending-actions.service';
|
||||||
|
import { UserService } from '../../user-profile/user.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { User } from '../model/user.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-finish-signup',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './finish-signup.component.html',
|
||||||
|
styleUrl: './finish-signup.component.css',
|
||||||
|
imports: [LoadingSpinnerComponent, CommonModule],
|
||||||
|
})
|
||||||
|
export class FinishSignupComponent {
|
||||||
|
isLoading = false;
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private pendingService: PendingActionsService,
|
||||||
|
private userService: UserService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.authService
|
||||||
|
.loginWithEmail()
|
||||||
|
.pipe(
|
||||||
|
switchMap(async (newUser: User) => {
|
||||||
|
const addUser$ = from(this.userService.addUser(newUser));
|
||||||
|
return addUser$;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.route.queryParams.subscribe((params) => {
|
||||||
|
const score = params['score'];
|
||||||
|
const uid = params['uid'];
|
||||||
|
if (score && uid) {
|
||||||
|
this.router.navigate(['/user-profile'], {
|
||||||
|
queryParams: {
|
||||||
|
score: score,
|
||||||
|
uid: uid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Error:', err);
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/auth/login-password/login-password.component.css
Normal file
16
src/app/auth/login-password/login-password.component.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Form */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Fields */
|
||||||
|
mat-form-field {
|
||||||
|
width: 17.8rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .mat-mdc-text-field-wrapper {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #FFF;
|
||||||
|
}
|
||||||
101
src/app/auth/login-password/login-password.component.html
Normal file
101
src/app/auth/login-password/login-password.component.html
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<!-- Flex container -->
|
||||||
|
<div class="flex" style="overflow: hidden">
|
||||||
|
<!-- Centered container -->
|
||||||
|
<div class="m-auto kick-motto">
|
||||||
|
<div *ngIf="isLoading" class="loading-spinner-correct">
|
||||||
|
<app-loading-spinner></app-loading-spinner>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="isAuthenticated" style="margin-top: 100%">
|
||||||
|
<app-score-table></app-score-table>
|
||||||
|
</div>
|
||||||
|
<!-- Material card -->
|
||||||
|
<div *ngIf="!isAuthenticated">
|
||||||
|
<mat-card
|
||||||
|
class="mat-mdc-card_noshadow default-background-color"
|
||||||
|
*ngIf="!isLoading"
|
||||||
|
>
|
||||||
|
<!-- Card image -->
|
||||||
|
<header-lsr></header-lsr>
|
||||||
|
<!-- Card content -->
|
||||||
|
<mat-card-content>
|
||||||
|
<!-- Login form -->
|
||||||
|
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
|
||||||
|
<!-- Email input field -->
|
||||||
|
<mat-form-field appearance="outline" class="custom-form-field">
|
||||||
|
<mat-label>Email</mat-label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
matInput
|
||||||
|
formControlName="email"
|
||||||
|
placeholder="@atos.net | @eviden.com"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="loginForm.get('email')?.hasError('required')"
|
||||||
|
>Die E-Mail-Adresse wird benötigt.
|
||||||
|
</mat-error>
|
||||||
|
<mat-error *ngIf="loginForm.get('email')?.hasError('pattern')">
|
||||||
|
Bitte gib deine Firmen-E-Mail lautend auf '@atos.net' oder
|
||||||
|
'@eviden.com' an.
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
<!-- Password input field -->
|
||||||
|
<mat-form-field appearance="outline" class="custom-form-field">
|
||||||
|
<mat-label>Passwort</mat-label>
|
||||||
|
<input
|
||||||
|
[type]="hide ? 'password' : 'text'"
|
||||||
|
matInput
|
||||||
|
formControlName="password"
|
||||||
|
placeholder="Eingabe"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="loginForm.get('password')?.hasError('required')"
|
||||||
|
>Ein Passwort wird benötigt.</mat-error
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Suffix button to toggle visibility -->
|
||||||
|
<!-- Add type="button" to prevent form submission when clicking the button -->
|
||||||
|
<button
|
||||||
|
id="pwVisibility"
|
||||||
|
type="button"
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
(click)="hide = !hide"
|
||||||
|
[attr.aria-label]="hide ? 'Show password' : 'Hide password'"
|
||||||
|
[attr.aria-pressed]="!hide"
|
||||||
|
>
|
||||||
|
<mat-icon>{{
|
||||||
|
hide ? "visibility_off" : "visibility"
|
||||||
|
}}</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Card footer with sign-up link -->
|
||||||
|
<mat-card-footer class="m-auto">
|
||||||
|
<a routerLink="/login">Login ohne Passwort!</a>
|
||||||
|
</mat-card-footer>
|
||||||
|
|
||||||
|
<!-- Card footer with sign-up link -->
|
||||||
|
<mat-card-footer class="m-auto margin-top-correct">
|
||||||
|
<a routerLink="/signup">Kein Konto? Registriere dich jetzt!</a>
|
||||||
|
</mat-card-footer>
|
||||||
|
|
||||||
|
<!-- Card footer with reset-password link -->
|
||||||
|
<mat-card-footer class="m-auto margin-top-correct">
|
||||||
|
<a routerLink="/reset-password">Passwort vergessen?</a>
|
||||||
|
</mat-card-footer>
|
||||||
|
|
||||||
|
<!-- Card actions with login button -->
|
||||||
|
<mat-card-actions>
|
||||||
|
<button
|
||||||
|
class="m-auto blue-buttons disabled-button"
|
||||||
|
mat-button
|
||||||
|
type="submit"
|
||||||
|
[disabled]="!loginForm.valid"
|
||||||
|
>
|
||||||
|
LOGIN
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</form>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
110
src/app/auth/login-password/login-password.component.ts
Normal file
110
src/app/auth/login-password/login-password.component.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { AngularMaterialModule } from '../../angular-material/angular-material.module';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
Validators,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormGroup,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { Router, RouterModule } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AuthService } from '../authService/auth.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { LoadingSpinnerComponent } from '../../shared/loading-spinner/loading-spinner.component';
|
||||||
|
import { NotifierService } from '../../shared/notifierService/notifier.service';
|
||||||
|
import { PendingActionsService } from '../../score/pending-actions.service';
|
||||||
|
import { ScoreTableComponent } from '../../score-table/score-table/score-table.component';
|
||||||
|
import { HeaderLoginSignupResetpw } from '../../shared/header-login-signup-resetpw/header-lsr.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LoginComponent represents the component responsible for handling the login functionality.
|
||||||
|
* It allows users to enter their email and password to authenticate themselves.
|
||||||
|
* This component utilizes Angular reactive forms for validation and form control management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login-password',
|
||||||
|
standalone: true,
|
||||||
|
providers: [AuthService],
|
||||||
|
templateUrl: './login-password.component.html',
|
||||||
|
styleUrl: './login-password.component.css',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
AngularMaterialModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule,
|
||||||
|
HttpClientModule,
|
||||||
|
LoadingSpinnerComponent,
|
||||||
|
ScoreTableComponent,
|
||||||
|
HeaderLoginSignupResetpw,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class LoginPasswordComponent implements OnInit {
|
||||||
|
loginForm!: FormGroup;
|
||||||
|
isLoading = false;
|
||||||
|
isAuthenticated = false;
|
||||||
|
hide = true;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private notifierService: NotifierService,
|
||||||
|
private router: Router,
|
||||||
|
private pendingActionsService: PendingActionsService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.authService.currentUser$.subscribe((user) => {
|
||||||
|
this.isAuthenticated = !!user;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the login form with form controls and validators
|
||||||
|
this.loginForm = new FormGroup({
|
||||||
|
email: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.email,
|
||||||
|
Validators.pattern(/\b[A-Za-z0-9._%+-]+@(atos\.net|eviden\.com)\b/),
|
||||||
|
]),
|
||||||
|
password: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
//Validation Text fehlt + blockiert LogIn nach PasswortReset mit einfachem Passwort
|
||||||
|
//Validators.minLength(10),
|
||||||
|
//Validators.maxLength(20),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
const email = this.loginForm.get('email')?.value;
|
||||||
|
const password = this.loginForm.get('password')?.value;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.authService.login(email, password).subscribe(
|
||||||
|
async (resData) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
// Handle the pending score after successful login
|
||||||
|
const pendingData = await this.pendingActionsService.getTempQRCodeData(
|
||||||
|
this.pendingActionsService.getTempQRId()
|
||||||
|
);
|
||||||
|
if (pendingData) {
|
||||||
|
this.router.navigate(['/user-profile'], {
|
||||||
|
queryParams: {
|
||||||
|
score: pendingData['score'],
|
||||||
|
uid: pendingData['uid'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(errorMessage) => {
|
||||||
|
this.notifierService.showNotification(errorMessage, 'OK');
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.loginForm.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/auth/login/login.component.css
Normal file
16
src/app/auth/login/login.component.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Form */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Fields */
|
||||||
|
mat-form-field {
|
||||||
|
width: 17.8rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .mat-mdc-text-field-wrapper {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
57
src/app/auth/login/login.component.html
Normal file
57
src/app/auth/login/login.component.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<!-- Flex container -->
|
||||||
|
<div class="flex" style="overflow: hidden">
|
||||||
|
<!-- Centered container -->
|
||||||
|
<div class="m-auto kick-motto">
|
||||||
|
<div *ngIf="isLoading" class="loading-spinner-correct">
|
||||||
|
<app-loading-spinner></app-loading-spinner>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="isAuthenticated" style="margin-top: 100%">
|
||||||
|
<app-score-table></app-score-table>
|
||||||
|
</div>
|
||||||
|
<!-- Material card -->
|
||||||
|
<div *ngIf="!isAuthenticated">
|
||||||
|
<mat-card
|
||||||
|
class="mat-mdc-card_noshadow default-background-color"
|
||||||
|
*ngIf="!isLoading"
|
||||||
|
>
|
||||||
|
<!-- Card image -->
|
||||||
|
<header-lsr></header-lsr>
|
||||||
|
<!-- Card content -->
|
||||||
|
<mat-card-content>
|
||||||
|
<!-- Login form -->
|
||||||
|
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
|
||||||
|
<!-- Email input field -->
|
||||||
|
<mat-form-field appearance="outline" class="custom-form-field">
|
||||||
|
<mat-label>Email</mat-label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
matInput
|
||||||
|
formControlName="email"
|
||||||
|
placeholder="@atos.net | @eviden.com"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="loginForm.get('email')?.hasError('required')"
|
||||||
|
>Die E-Mail-Adresse wird benötigt.
|
||||||
|
</mat-error>
|
||||||
|
<mat-error *ngIf="loginForm.get('email')?.hasError('pattern')">
|
||||||
|
Bitte gib deine Firmen-E-Mail lautend auf '@atos.net' oder
|
||||||
|
'@eviden.com' an.
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Card actions with login button -->
|
||||||
|
<mat-card-actions>
|
||||||
|
<button
|
||||||
|
class="m-auto blue-buttons disabled-button"
|
||||||
|
mat-button
|
||||||
|
type="submit"
|
||||||
|
[disabled]="!loginForm.valid"
|
||||||
|
>
|
||||||
|
LOGIN | SIGN UP
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</form>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
88
src/app/auth/login/login.component.ts
Normal file
88
src/app/auth/login/login.component.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { AngularMaterialModule } from '../../angular-material/angular-material.module';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
Validators,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormGroup,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AuthService } from '../authService/auth.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { LoadingSpinnerComponent } from '../../shared/loading-spinner/loading-spinner.component';
|
||||||
|
import { PendingActionsService } from '../../score/pending-actions.service';
|
||||||
|
import { ScoreTableComponent } from '../../score-table/score-table/score-table.component';
|
||||||
|
import { HeaderLoginSignupResetpw } from '../../shared/header-login-signup-resetpw/header-lsr.component';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { CustomDialogComponent } from '../../shared/custom-dialog/custom-dialog.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
AngularMaterialModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule,
|
||||||
|
HttpClientModule,
|
||||||
|
LoadingSpinnerComponent,
|
||||||
|
ScoreTableComponent,
|
||||||
|
HeaderLoginSignupResetpw,
|
||||||
|
],
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrl: './login.component.css',
|
||||||
|
})
|
||||||
|
export class LoginComponent {
|
||||||
|
loginForm!: FormGroup;
|
||||||
|
isLoading = false;
|
||||||
|
isAuthenticated = false;
|
||||||
|
hide = true;
|
||||||
|
qrData: { score: string; uid: string } | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router,
|
||||||
|
private pendingActionsService: PendingActionsService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.authService.currentUser$.subscribe((user) => {
|
||||||
|
this.isAuthenticated = !!user;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loginForm = new FormGroup({
|
||||||
|
email: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.email,
|
||||||
|
Validators.pattern(/\b[A-Za-z0-9._%+-]+@(atos\.net|eviden\.com)\b/),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
this.route.queryParams.subscribe((params) => {
|
||||||
|
this.qrData = { score: params['score'], uid: params['uid'] };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSubmit() {
|
||||||
|
const email = this.loginForm.get('email')?.value;
|
||||||
|
this.isLoading = true;
|
||||||
|
console.log('login qrData=', this.qrData);
|
||||||
|
|
||||||
|
this.authService.signUp(email, this.qrData?.score, this.qrData?.uid);
|
||||||
|
this.dialog.open(CustomDialogComponent, {
|
||||||
|
data: {
|
||||||
|
title: 'E-Mail-Anmeldung',
|
||||||
|
bodyText:
|
||||||
|
'Eine Anmeldungs-Email wurde an deine Adresse gesendet. Bitte überprüfe dein Postfach und klicke auf den Link, um dich anzumelden.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.isLoading = false;
|
||||||
|
// Handle the pending score after successful login
|
||||||
|
|
||||||
|
this.loginForm.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/app/auth/model/user.model.ts
Normal file
13
src/app/auth/model/user.model.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export interface User {
|
||||||
|
uid: string;
|
||||||
|
email?: string | null;
|
||||||
|
photoUrl?: string | null;
|
||||||
|
wins?: number | null;
|
||||||
|
lastName?: string;
|
||||||
|
firstName?: string;
|
||||||
|
firmenposition?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RankedUser extends User {
|
||||||
|
rank: number;
|
||||||
|
}
|
||||||
16
src/app/auth/resetPw/resetPw.component.css
Normal file
16
src/app/auth/resetPw/resetPw.component.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Form */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Fields */
|
||||||
|
mat-form-field {
|
||||||
|
width: 17.8rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .mat-mdc-text-field-wrapper {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #FFF;
|
||||||
|
}
|
||||||
45
src/app/auth/resetPw/resetPw.component.html
Normal file
45
src/app/auth/resetPw/resetPw.component.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<!-- Flex container -->
|
||||||
|
<div class="flex">
|
||||||
|
<!-- Centered container -->
|
||||||
|
<div class="m-auto kick-motto">
|
||||||
|
<div *ngIf="isLoading" class="loading-spinner-correct">
|
||||||
|
<app-loading-spinner></app-loading-spinner>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="isAuthenticated" style="margin-top: 100%;">
|
||||||
|
<app-score-table></app-score-table>
|
||||||
|
</div>
|
||||||
|
<!-- Material card -->
|
||||||
|
<div *ngIf="!isAuthenticated">
|
||||||
|
<mat-card class="default-background-color mat-mdc-card_noshadow" *ngIf="!isLoading">
|
||||||
|
<!-- Card image -->
|
||||||
|
<header-lsr></header-lsr>
|
||||||
|
<!-- Card content -->
|
||||||
|
<mat-card-content>
|
||||||
|
<!-- Login form -->
|
||||||
|
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
|
||||||
|
<!-- Email input field -->
|
||||||
|
<mat-form-field appearance="outline" class="custom-form-field">
|
||||||
|
<mat-label>Email</mat-label>
|
||||||
|
<input type="email" matInput formControlName="email" placeholder="@atos.net | @eviden.com">
|
||||||
|
<mat-error *ngIf="loginForm.get('email')?.hasError('pattern')">
|
||||||
|
Bitte gib deine Firmen-E-Mail lautend auf '@atos.net' oder '@eviden.com' an.
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
<!-- Card footer with logIn link -->
|
||||||
|
<mat-card-footer class="m-auto">
|
||||||
|
<a routerLink="/login">Du hast bereits ein Konto? Einloggen.</a>
|
||||||
|
</mat-card-footer>
|
||||||
|
<!-- Card footer with sign-up link -->
|
||||||
|
<mat-card-footer class="m-auto margin-top-correct">
|
||||||
|
<a routerLink="/signup">Kein Konto? Registriere dich jetzt!</a>
|
||||||
|
</mat-card-footer>
|
||||||
|
<!-- Card actions with login button -->
|
||||||
|
<mat-card-actions>
|
||||||
|
<button class="m-auto blue-buttons disabled-button" mat-button type="submit" [disabled]="!loginForm.valid">RESET</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</form>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
94
src/app/auth/resetPw/resetPw.component.ts
Normal file
94
src/app/auth/resetPw/resetPw.component.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { AngularMaterialModule } from '../../angular-material/angular-material.module';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
Validators,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormGroup,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { Router, RouterModule } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AuthService } from '../authService/auth.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { LoadingSpinnerComponent } from '../../shared/loading-spinner/loading-spinner.component';
|
||||||
|
import { NotifierService } from '../../shared/notifierService/notifier.service';
|
||||||
|
import { ScoreTableComponent } from '../../score-table/score-table/score-table.component';
|
||||||
|
import {Firestore, getDocs, where} from "@angular/fire/firestore";
|
||||||
|
import {collection, query} from "firebase/firestore";
|
||||||
|
import { HeaderLoginSignupResetpw } from '../../shared/header-login-signup-resetpw/header-lsr.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login-password',
|
||||||
|
standalone: true,
|
||||||
|
providers: [AuthService],
|
||||||
|
templateUrl: './resetPw.component.html',
|
||||||
|
styleUrl: './resetPw.component.css',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
AngularMaterialModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule,
|
||||||
|
HttpClientModule,
|
||||||
|
LoadingSpinnerComponent,
|
||||||
|
ScoreTableComponent,
|
||||||
|
HeaderLoginSignupResetpw,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export class ResetPwComponent implements OnInit {
|
||||||
|
/** Represents the login form group */
|
||||||
|
loginForm!: FormGroup;
|
||||||
|
isLoading = false;
|
||||||
|
isAuthenticated = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private notifierService: NotifierService,
|
||||||
|
private router: Router,
|
||||||
|
private firestore: Firestore,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.authService.currentUser$.subscribe((user) => {
|
||||||
|
this.isAuthenticated = !!user;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the login form with form controls and validators
|
||||||
|
this.loginForm = new FormGroup({
|
||||||
|
email: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.email,
|
||||||
|
Validators.pattern(/^[a-zA-Z0-9._%+-]+\.+[a-zA-Z0-9._%+-]+@(eviden\.com|atos\.net)$/),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
const email = this.loginForm.get('email')?.value;
|
||||||
|
const lowercaseEmail = email.toLowerCase();
|
||||||
|
this.isLoading = true;
|
||||||
|
const usersCollection = collection(this.firestore, 'users');
|
||||||
|
const scoreQuery = query(usersCollection, where('email', '==', lowercaseEmail));
|
||||||
|
|
||||||
|
getDocs(scoreQuery)
|
||||||
|
.then(querySnapshot => {
|
||||||
|
if (querySnapshot.size > 0) {
|
||||||
|
// Email exists, perform desired action
|
||||||
|
this.authService.resetPassword(email).then(() => {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
this.isLoading = false;
|
||||||
|
this.notifierService.showNotification("Reset Link gesendet an "+email.toLowerCase(), 'OK');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.notifierService.showNotification("Fehler: Die E-Mail existiert nicht!", 'OK');
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loginForm.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/app/auth/signUp/signup.component.css
Normal file
15
src/app/auth/signUp/signup.component.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* Form */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Fields */
|
||||||
|
mat-form-field {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .mat-mdc-text-field-wrapper {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
60
src/app/auth/signUp/signup.component.html
Normal file
60
src/app/auth/signUp/signup.component.html
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<!-- Flex container -->
|
||||||
|
<div class="flex">
|
||||||
|
<!-- Centered container -->
|
||||||
|
<div class="m-auto kick-motto">
|
||||||
|
<div *ngIf="isLoading" class="loading-spinner-correct">
|
||||||
|
<app-loading-spinner></app-loading-spinner>
|
||||||
|
</div>
|
||||||
|
<!-- Material card -->
|
||||||
|
<div>
|
||||||
|
<mat-card
|
||||||
|
class="default-background-color mat-mdc-card_noshadow"
|
||||||
|
*ngIf="!isLoading"
|
||||||
|
>
|
||||||
|
<!-- Card image -->
|
||||||
|
<header-lsr></header-lsr>
|
||||||
|
<!-- Card content -->
|
||||||
|
<mat-card-content>
|
||||||
|
<!-- Login form -->
|
||||||
|
<form [formGroup]="signUpForm" (ngSubmit)="onSubmit()">
|
||||||
|
<!-- Email input field -->
|
||||||
|
<mat-form-field appearance="outline" class="custom-form-field">
|
||||||
|
<mat-label>Email</mat-label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
matInput
|
||||||
|
formControlName="email"
|
||||||
|
placeholder="@atos.net | @eviden.com"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="signUpForm.get('email')?.hasError('pattern')">
|
||||||
|
Bitte gib deine Firmen-E-Mail lautend auf '@atos.net' oder
|
||||||
|
'@eviden.com' an.
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
<!-- Card footer with logIn link -->
|
||||||
|
<mat-card-footer class="m-auto">
|
||||||
|
<a routerLink="/login">Du hast bereits ein Konto? Einloggen.</a>
|
||||||
|
</mat-card-footer>
|
||||||
|
<!-- Card footer with sign-up link -->
|
||||||
|
<mat-card-footer class="m-auto margin-top-correct">
|
||||||
|
<a routerLink="/signup-password"
|
||||||
|
>Registriere dich hier mit E-Mail und Passwort!</a
|
||||||
|
>
|
||||||
|
</mat-card-footer>
|
||||||
|
<!-- Card actions with login button -->
|
||||||
|
<mat-card-actions>
|
||||||
|
<button
|
||||||
|
class="m-auto blue-buttons disabled-button"
|
||||||
|
mat-button
|
||||||
|
type="submit"
|
||||||
|
[disabled]="!signUpForm.valid"
|
||||||
|
>
|
||||||
|
REGISTRIEREN
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</form>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
76
src/app/auth/signUp/signup.component.ts
Normal file
76
src/app/auth/signUp/signup.component.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { AngularMaterialModule } from '../../angular-material/angular-material.module';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
Validators,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormGroup,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { Router, RouterModule } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AuthService } from '../authService/auth.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { LoadingSpinnerComponent } from '../../shared/loading-spinner/loading-spinner.component';
|
||||||
|
import { NotifierService } from '../../shared/notifierService/notifier.service';
|
||||||
|
import { ScoreTableComponent } from '../../score-table/score-table/score-table.component';
|
||||||
|
import { Firestore } from '@angular/fire/firestore';
|
||||||
|
import { HeaderLoginSignupResetpw } from '../../shared/header-login-signup-resetpw/header-lsr.component';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { pipe, switchMap } from 'rxjs';
|
||||||
|
import { User } from '../model/user.model';
|
||||||
|
import { PendingActionsService } from '../../score/pending-actions.service';
|
||||||
|
import { CustomDialogComponent } from '../../shared/custom-dialog/custom-dialog.component';
|
||||||
|
import { UserService } from '../../user-profile/user.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-signup',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
AngularMaterialModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule,
|
||||||
|
HttpClientModule,
|
||||||
|
LoadingSpinnerComponent,
|
||||||
|
ScoreTableComponent,
|
||||||
|
HeaderLoginSignupResetpw,
|
||||||
|
],
|
||||||
|
templateUrl: './signup.component.html',
|
||||||
|
styleUrl: './signup.component.css',
|
||||||
|
})
|
||||||
|
export class SignupComponent {
|
||||||
|
/** Represents the login form group */
|
||||||
|
signUpForm!: FormGroup;
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
|
constructor(private authService: AuthService, private dialog: MatDialog) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Initialize the login form with form controls and validators
|
||||||
|
this.signUpForm = new FormGroup({
|
||||||
|
email: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.email,
|
||||||
|
Validators.pattern(
|
||||||
|
/^[a-zA-Z0-9._%+-]+\.+[a-zA-Z0-9._%+-]+@(eviden\.com|atos\.net)$/
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
const emailBig = this.signUpForm.get('email')?.value;
|
||||||
|
const email = emailBig.toLowerCase();
|
||||||
|
this.authService.signUp(email);
|
||||||
|
this.dialog.open(CustomDialogComponent, {
|
||||||
|
data: {
|
||||||
|
title: 'E-Mail-Regristrierung',
|
||||||
|
bodyText:
|
||||||
|
'Eine Regristrierungs-Email wurde an deine Adresse gesendet. Bitte überprüfe dein Postfach und klicke auf den Link, um dich zu registrieren.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.signUpForm.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/auth/signup-password/signup-password.component.css
Normal file
23
src/app/auth/signup-password/signup-password.component.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* Form */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Fields */
|
||||||
|
mat-form-field {
|
||||||
|
width: 17.8rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Footer */
|
||||||
|
mat-card-footer {
|
||||||
|
color: #0596FF;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .mat-mdc-text-field-wrapper {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #FFF;
|
||||||
|
}
|
||||||
119
src/app/auth/signup-password/signup-password.component.html
Normal file
119
src/app/auth/signup-password/signup-password.component.html
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<!-- Flex container -->
|
||||||
|
<div class="flex">
|
||||||
|
<!-- Centered container -->
|
||||||
|
<div class="m-auto kick-motto">
|
||||||
|
<div *ngIf="isLoading" class="loading-spinner-correct">
|
||||||
|
<app-loading-spinner></app-loading-spinner>
|
||||||
|
</div>
|
||||||
|
<!-- Material card -->
|
||||||
|
<mat-card
|
||||||
|
class="default-background-color mat-mdc-card_noshadow"
|
||||||
|
*ngIf="!isLoading"
|
||||||
|
>
|
||||||
|
<header-lsr></header-lsr>
|
||||||
|
<!-- Card content -->
|
||||||
|
<mat-card-content>
|
||||||
|
<!-- Sign up form -->
|
||||||
|
<form [formGroup]="signUpForm" (ngSubmit)="onSubmit()">
|
||||||
|
<!-- Email input field -->
|
||||||
|
<mat-form-field appearance="outline" class="custom-form-field">
|
||||||
|
<mat-label>Email</mat-label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
matInput
|
||||||
|
formControlName="email"
|
||||||
|
placeholder="@atos.net | @eviden.com"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="signUpForm.get('email')?.hasError('required')"
|
||||||
|
>Die E-Mail-Adresse wird benötigt.
|
||||||
|
</mat-error>
|
||||||
|
<mat-error *ngIf="signUpForm.get('email')?.hasError('pattern')">
|
||||||
|
Bitte gib deine Firmen-E-Mail lautend auf '@atos.net' oder
|
||||||
|
'@eviden.com' an.
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Password input field -->
|
||||||
|
<mat-form-field appearance="outline" class="example-full-width">
|
||||||
|
<mat-label>Passwort</mat-label>
|
||||||
|
<input
|
||||||
|
[type]="hide ? 'password' : 'text'"
|
||||||
|
matInput
|
||||||
|
formControlName="password"
|
||||||
|
placeholder="10-20 Zeichen"
|
||||||
|
autocomplete="new-password"
|
||||||
|
title="10-20 Zeichen, mind. 1 Großbuchstaben und 1 Zahl."
|
||||||
|
(blur)="updateErrorMessage()"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="signUpForm.get('password')?.invalid">
|
||||||
|
{{ this.errorMessagePassword }}
|
||||||
|
</mat-error>
|
||||||
|
<!-- Suffix button to toggle visibility -->
|
||||||
|
<!-- Add type="button" to prevent form submission when clicking the button -->
|
||||||
|
<button
|
||||||
|
id="pwVisibility"
|
||||||
|
type="button"
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
(click)="hide = !hide"
|
||||||
|
[attr.aria-label]="hide ? 'Show password' : 'Hide password'"
|
||||||
|
[attr.aria-pressed]="!hide"
|
||||||
|
>
|
||||||
|
<mat-icon>{{ hide ? "visibility_off" : "visibility" }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Repeat password input field -->
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="example-full-width">
|
||||||
|
<mat-label>Passwort bestätigen</mat-label>
|
||||||
|
<input
|
||||||
|
[type]="hide ? 'password' : 'text'"
|
||||||
|
matInput
|
||||||
|
formControlName="confirmPassword"
|
||||||
|
placeholder="10-20 Zeichen"
|
||||||
|
autocomplete="new-password"
|
||||||
|
title="10-20 Zeichen, mind. 1 Großbuchstaben und 1 Zahl."
|
||||||
|
(blur)="updateErrorMessage()"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="signUpForm.get('confirmPassword')?.invalid">
|
||||||
|
{{ this.errorMessageConfirmPassword }}
|
||||||
|
</mat-error>
|
||||||
|
|
||||||
|
<!-- Suffix button to toggle visibility -->
|
||||||
|
<!-- Add type="button" to prevent form submission when clicking the button -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
(click)="hide = !hide"
|
||||||
|
[attr.aria-label]="hide ? 'Show password' : 'Hide password'"
|
||||||
|
[attr.aria-pressed]="!hide"
|
||||||
|
>
|
||||||
|
<mat-icon>{{ hide ? "visibility_off" : "visibility" }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Card footer with login link -->
|
||||||
|
<mat-card-footer
|
||||||
|
><a routerLink="/login"
|
||||||
|
>Du hast bereits ein Konto? Einloggen.</a
|
||||||
|
></mat-card-footer
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Card actions with sign up button -->
|
||||||
|
<mat-card-actions>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
class="m-auto blue-buttons disabled-button"
|
||||||
|
type="submit"
|
||||||
|
[disabled]="!signUpForm.valid"
|
||||||
|
>
|
||||||
|
REGISTRIEREN
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</form>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
234
src/app/auth/signup-password/signup-password.component.ts
Normal file
234
src/app/auth/signup-password/signup-password.component.ts
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { AngularMaterialModule } from '../../angular-material/angular-material.module';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
Validators,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormGroup,
|
||||||
|
AbstractControl,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { Router, RouterModule } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AuthService } from '../authService/auth.service';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { LoadingSpinnerComponent } from '../../shared/loading-spinner/loading-spinner.component';
|
||||||
|
import { NotifierService } from '../../shared/notifierService/notifier.service';
|
||||||
|
import { Subscription, switchMap } from 'rxjs';
|
||||||
|
import { UserService } from '../../user-profile/user.service';
|
||||||
|
import {
|
||||||
|
hasNumberValidator,
|
||||||
|
hasUpperCaseValidator,
|
||||||
|
} from '../../shared/validation/passwordSaferValidation';
|
||||||
|
import {
|
||||||
|
PasswordValidationType,
|
||||||
|
passwordValidator,
|
||||||
|
} from '../../shared/validation/passwordMatcherValidation';
|
||||||
|
import { User } from '../model/user.model';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { CustomDialogComponent } from '../../shared/custom-dialog/custom-dialog.component';
|
||||||
|
import { HeaderLoginSignupResetpw } from '../../shared/header-login-signup-resetpw/header-lsr.component';
|
||||||
|
import { PendingActionsService } from '../../score/pending-actions.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SignUpComponent represents the component responsible for user registration.
|
||||||
|
* It allows users to sign up by providing their email, password, and confirming the password.
|
||||||
|
* This component utilizes Angular reactive forms for validation and form control management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-registrierung',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
AngularMaterialModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule,
|
||||||
|
HttpClientModule,
|
||||||
|
LoadingSpinnerComponent,
|
||||||
|
HeaderLoginSignupResetpw,
|
||||||
|
],
|
||||||
|
providers: [AuthService, UserService],
|
||||||
|
templateUrl: './signup-password.component.html',
|
||||||
|
styleUrl: './signup-password.component.css',
|
||||||
|
})
|
||||||
|
export class SignupPasswordComponent implements OnInit, OnDestroy {
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private userService: UserService,
|
||||||
|
private notifierService: NotifierService,
|
||||||
|
private router: Router,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private pendingService: PendingActionsService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Represents the sign-up form group */
|
||||||
|
signUpForm!: FormGroup;
|
||||||
|
isLoading = false;
|
||||||
|
hide = true;
|
||||||
|
errorMessagePassword = '';
|
||||||
|
errorMessageConfirmPassword = '';
|
||||||
|
|
||||||
|
private passwordValueChangesSubscription!: Subscription;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Initialize the sign-up form with form controls and validators
|
||||||
|
this.signUpForm = new FormGroup({
|
||||||
|
email: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.email,
|
||||||
|
Validators.pattern(
|
||||||
|
/^[a-zA-Z0-9._%+-]+\.+[a-zA-Z0-9._%+-]+@(eviden\.com|atos\.net)$/
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
password: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.minLength(10),
|
||||||
|
Validators.maxLength(20),
|
||||||
|
hasUpperCaseValidator(),
|
||||||
|
hasNumberValidator(),
|
||||||
|
]),
|
||||||
|
confirmPassword: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
passwordValidator('password', PasswordValidationType.Match),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
// Set up a subscription to the password field's valueChanges observable
|
||||||
|
const passwordControl = this.signUpForm.get('password');
|
||||||
|
const confirmPasswordControl = this.signUpForm.get('confirmPassword');
|
||||||
|
|
||||||
|
if (passwordControl && confirmPasswordControl) {
|
||||||
|
passwordControl.valueChanges.subscribe(() => {
|
||||||
|
this.updateErrorMessage();
|
||||||
|
confirmPasswordControl.updateValueAndValidity();
|
||||||
|
});
|
||||||
|
|
||||||
|
confirmPasswordControl.valueChanges.subscribe(() => {
|
||||||
|
this.updateErrorMessage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Logs the form data to the console.
|
||||||
|
onSubmit() {
|
||||||
|
const emailBig = this.signUpForm.get('email')?.value;
|
||||||
|
const email = emailBig.toLowerCase();
|
||||||
|
const password = this.signUpForm.get('password')?.value;
|
||||||
|
|
||||||
|
// Extract first name and last name from the email address
|
||||||
|
const [firstName, lastName] = this.extractNamesFromEmail(email);
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.authService
|
||||||
|
.signUpPassword(email, password)
|
||||||
|
.pipe(
|
||||||
|
switchMap(({ user: { uid } }) => {
|
||||||
|
const newUser: User = {
|
||||||
|
uid: uid,
|
||||||
|
email: email,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
firmenposition: null,
|
||||||
|
photoUrl: null,
|
||||||
|
wins: null,
|
||||||
|
};
|
||||||
|
this.pendingService.addUserRefToQRCodeData(
|
||||||
|
uid,
|
||||||
|
this.pendingService.getTempQRId()
|
||||||
|
);
|
||||||
|
return this.userService.addUser(newUser);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
(resData) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
this.dialog.open(CustomDialogComponent, {
|
||||||
|
data: {
|
||||||
|
title: 'E-Mail-Verifizierung',
|
||||||
|
bodyText:
|
||||||
|
'Eine Verifizierung-Email wurde an deine Adresse gesendet. Bitte überprüfe dein Postfach und klicke auf den Link, um dein Konto zu verifizieren und Spiele hinzuzufügen.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(errorMessage) => {
|
||||||
|
this.notifierService.showNotification(errorMessage, 'OK');
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.signUpForm.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.passwordValueChangesSubscription) {
|
||||||
|
this.passwordValueChangesSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractNamesFromEmail(email: string): [string, string] {
|
||||||
|
const sanitizedEmail = email.replace(/.\d|\.external/g, '');
|
||||||
|
const [fullName] = sanitizedEmail.split('@');
|
||||||
|
const names = fullName.split('.');
|
||||||
|
|
||||||
|
// Vor- und Nachname grossgeschrieben
|
||||||
|
const capitalize = (name: string) =>
|
||||||
|
name
|
||||||
|
.split('-')
|
||||||
|
.map(
|
||||||
|
(part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
||||||
|
)
|
||||||
|
.join('-');
|
||||||
|
|
||||||
|
// Capitalize both the first name and last name parts
|
||||||
|
const firstName = names.length > 0 ? capitalize(names[0]) : '';
|
||||||
|
const lastName =
|
||||||
|
names.length > 1 ? capitalize(names.slice(1).join('-')) : '';
|
||||||
|
|
||||||
|
return [firstName, lastName];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateErrorMessage() {
|
||||||
|
const passwordControl = this.signUpForm.get('password');
|
||||||
|
const confirmPasswordControl = this.signUpForm.get('confirmPassword');
|
||||||
|
|
||||||
|
this.errorMessagePassword = this.getPasswordErrorMessage(passwordControl);
|
||||||
|
this.errorMessageConfirmPassword = this.getConfirmPasswordErrorMessage(
|
||||||
|
confirmPasswordControl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPasswordErrorMessage(control: AbstractControl | null): string {
|
||||||
|
if (control?.hasError('required')) {
|
||||||
|
return 'Ein Passwort wird benötigt.';
|
||||||
|
} else if (control?.hasError('minlength')) {
|
||||||
|
return 'Das Passwort muss mindestens 10 Zeichen lang sein.';
|
||||||
|
} else if (control?.hasError('maxlength')) {
|
||||||
|
return 'Das Passwort darf maximal 20 Zeichen lang sein.';
|
||||||
|
} else if (control?.hasError('passwordsSame')) {
|
||||||
|
return 'Das neue Passwort muss sich vom aktuellen Passwort unterscheiden.';
|
||||||
|
} else if (
|
||||||
|
control?.hasError('hasNumber') &&
|
||||||
|
control?.hasError('hasUpperCase')
|
||||||
|
) {
|
||||||
|
return 'Mind. 1 Großbuchstaben und 1 Zahl.';
|
||||||
|
} else if (control?.hasError('hasUpperCase')) {
|
||||||
|
return 'Mind. 1 Großbuchstaben.';
|
||||||
|
} else if (control?.hasError('hasNumber')) {
|
||||||
|
return 'Mind. 1 Zahl.';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getConfirmPasswordErrorMessage(
|
||||||
|
control: AbstractControl | null
|
||||||
|
): string {
|
||||||
|
if (control?.hasError('required')) {
|
||||||
|
return 'Passwortbestätigung eingeben.';
|
||||||
|
} else if (control?.hasError('passwordMismatch')) {
|
||||||
|
return 'Die eingegebenen Passwörter stimmen nicht überein.';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/app/score-table/custom-mat-paginator-intl.ts
Normal file
19
src/app/score-table/custom-mat-paginator-intl.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { MatPaginatorIntl } from '@angular/material/paginator';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
export class CustomMatPaginatorIntl implements MatPaginatorIntl {
|
||||||
|
changes = new Subject<void>();
|
||||||
|
itemsPerPageLabel = 'Einträge/Seite';
|
||||||
|
nextPageLabel = 'Nächste Seite';
|
||||||
|
previousPageLabel = 'Vorherige Seite';
|
||||||
|
firstPageLabel = 'Erste Seite';
|
||||||
|
lastPageLabel = 'Letzte Seite';
|
||||||
|
|
||||||
|
getRangeLabel(page: number, pageSize: number, length: number): string {
|
||||||
|
if (length === 0) {
|
||||||
|
return `1 von 1`;
|
||||||
|
}
|
||||||
|
const amountPages = Math.ceil(length / pageSize);
|
||||||
|
return `${page + 1} von ${amountPages}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/score-table/score-table.service.spec.ts
Normal file
16
src/app/score-table/score-table.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ScoreTableService } from './score-table.service';
|
||||||
|
|
||||||
|
describe('ScoreTableServiceService', () => {
|
||||||
|
let service: ScoreTableService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ScoreTableService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
185
src/app/score-table/score-table.service.ts
Normal file
185
src/app/score-table/score-table.service.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
Firestore,
|
||||||
|
orderBy,
|
||||||
|
onSnapshot,
|
||||||
|
doc,
|
||||||
|
where,
|
||||||
|
getDocs,
|
||||||
|
getDoc,
|
||||||
|
setDoc,
|
||||||
|
addDoc,
|
||||||
|
} from '@angular/fire/firestore';
|
||||||
|
import { BehaviorSubject, Observable, take } from 'rxjs';
|
||||||
|
import { collection, query } from 'firebase/firestore';
|
||||||
|
import { RankedUser, User } from '../auth/model/user.model';
|
||||||
|
import { Games } from './score-table/score-table.component';
|
||||||
|
import { AuthService } from '../auth/authService/auth.service';
|
||||||
|
import { UserService } from '../user-profile/user.service';
|
||||||
|
import { NotifierService } from '../shared/notifierService/notifier.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ScoreTableService {
|
||||||
|
private showUserGamesSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
showUserGames$ = this.showUserGamesSubject.asObservable();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private firestore: Firestore,
|
||||||
|
private authService: AuthService,
|
||||||
|
private userService: UserService,
|
||||||
|
private notifierService: NotifierService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
setShowUserGames(value: boolean) {
|
||||||
|
this.showUserGamesSubject.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTopUsers(): Observable<RankedUser[]> {
|
||||||
|
return new Observable<RankedUser[]>((observer) => {
|
||||||
|
const usersCollection = collection(this.firestore, 'users');
|
||||||
|
const scoreQuery = query(
|
||||||
|
usersCollection,
|
||||||
|
where('wins', '>', 0),
|
||||||
|
orderBy('wins', 'desc')
|
||||||
|
);
|
||||||
|
const unsubscribe = onSnapshot(
|
||||||
|
scoreQuery,
|
||||||
|
(querySnapshot) => {
|
||||||
|
const users: RankedUser[] = [];
|
||||||
|
querySnapshot.forEach((doc) => {
|
||||||
|
users.push({ uid: doc.id, ...doc.data() } as RankedUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign ranks to users
|
||||||
|
const rankedUsers = this.assignRanks(users);
|
||||||
|
observer.next(rankedUsers);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
observer.error(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unsubscribe when the observer unsubscribes
|
||||||
|
return () => unsubscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private assignRanks(users: RankedUser[]): RankedUser[] {
|
||||||
|
if (users.length === 0) return users;
|
||||||
|
|
||||||
|
let currentRank = 1;
|
||||||
|
let previousWins = users[0].wins ?? 0; // Default to 0 if wins is null or undefined
|
||||||
|
users[0].rank = currentRank;
|
||||||
|
|
||||||
|
for (let i = 1; i < users.length; i++) {
|
||||||
|
const currentWins = users[i].wins ?? 0; // Default to 0 if wins is null or undefined
|
||||||
|
if (currentWins < previousWins) {
|
||||||
|
currentRank++;
|
||||||
|
}
|
||||||
|
users[i].rank = currentRank;
|
||||||
|
previousWins = currentWins;
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAndAddGame(score: string, uid: string) {
|
||||||
|
this.authService.currentUser$.pipe(take(1)).subscribe((user) => {
|
||||||
|
if (user) {
|
||||||
|
const qrCodeRef = doc(this.firestore, 'qrCodes', uid);
|
||||||
|
getDoc(qrCodeRef).then((qrCodeDoc) => {
|
||||||
|
const usedBy =
|
||||||
|
qrCodeDoc.exists() && qrCodeDoc.data()['usedBy']
|
||||||
|
? qrCodeDoc.data()['usedBy']
|
||||||
|
: [];
|
||||||
|
|
||||||
|
let message = usedBy.includes(user.uid)
|
||||||
|
? 'Dieses Spiel wurde bereits zu deinem Konto hinzugefügt!'
|
||||||
|
: usedBy.length >= 2
|
||||||
|
? 'Dieser QR-Code ist nicht mehr gültig!'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
const updatedUsedBy = [...usedBy, user.uid];
|
||||||
|
setDoc(qrCodeRef, { usedBy: updatedUsedBy }, { merge: true }).then(
|
||||||
|
() => {
|
||||||
|
this.addGameToDatabase(score, user.uid);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
this.notifierService.showNotification(message, 'OK');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addGameToDatabase(score: string, userId: string) {
|
||||||
|
const userRef = doc(this.firestore, 'users', userId);
|
||||||
|
const game = {
|
||||||
|
score: score,
|
||||||
|
date: new Date(),
|
||||||
|
userRef: userRef,
|
||||||
|
};
|
||||||
|
const gamesRef = collection(this.firestore, 'games');
|
||||||
|
|
||||||
|
addDoc(gamesRef, game)
|
||||||
|
.then(() => {
|
||||||
|
this.userService.updateUserWins(userId).subscribe(); // Update the user's wins count
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
this.notifierService.showNotification(
|
||||||
|
'Spiel erfolgreich zu deinem Konto hinzugefügt!',
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadUserGames(userId: string): Promise<Games[]> {
|
||||||
|
const gamesRef = collection(this.firestore, 'games');
|
||||||
|
const q = query(
|
||||||
|
gamesRef,
|
||||||
|
where('userRef', '==', doc(this.firestore, 'users', userId)),
|
||||||
|
orderBy('date', 'desc')
|
||||||
|
);
|
||||||
|
|
||||||
|
const querySnapshot = await getDocs(q);
|
||||||
|
const gamesArray: Games[] = querySnapshot.docs.map((doc) => {
|
||||||
|
const score = doc.data()['score'].split('-');
|
||||||
|
const scoreTransformed = (
|
||||||
|
score[0].length == 1 ? '0'.concat(score[0]) : score[0]
|
||||||
|
)
|
||||||
|
.concat(' - ')
|
||||||
|
.concat(score[1].length == 1 ? '0'.concat(score[1]) : score[1]);
|
||||||
|
const dateTime: Date = doc.data()['date'].toDate();
|
||||||
|
|
||||||
|
// Format the date and time as per the new requirements
|
||||||
|
const formattedDate = dateTime.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: '2-digit',
|
||||||
|
});
|
||||||
|
const formattedTime = dateTime.toLocaleTimeString('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: doc.id,
|
||||||
|
...doc.data(),
|
||||||
|
score: scoreTransformed,
|
||||||
|
// Combine the formatted date and time
|
||||||
|
date: `${formattedDate} ${formattedTime}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return gamesArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
149
src/app/score-table/score-table/score-table.component.css
Normal file
149
src/app/score-table/score-table/score-table.component.css
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
.btn-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-container {
|
||||||
|
margin: 0 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.center-container {
|
||||||
|
margin: 0 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-mdc-paginator-container {
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-mdc-paginator-range-label {
|
||||||
|
margin-left: 12px !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search {
|
||||||
|
background-color: inherit;
|
||||||
|
border: 0;
|
||||||
|
color: #002d3c;
|
||||||
|
width: 2rem !important;
|
||||||
|
height: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search .mat-icon {
|
||||||
|
position: relative;
|
||||||
|
right: 3px;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
color: #000;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-row .mat-mdc-cell {
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
border-top: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-row:hover .mat-mdc-cell {
|
||||||
|
border-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-column-position {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-column-wins {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-table {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spalten gleich*/
|
||||||
|
.user-games-table .mat-elevation-z8 {
|
||||||
|
overflow-x: auto; /* Ensures the table is scrollable horizontally if it overflows */
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed; /* Use a fixed table layout */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-column-date,
|
||||||
|
.mat-column-score {
|
||||||
|
width: 50%; /* Set each column to occupy half of the table's width */
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional styles for responsiveness and aesthetics */
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.center-container {
|
||||||
|
margin: 0 5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-cell, .mat-header-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis; /* Prevent text overflow */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Mat Slide Toggle Button - change of colors when switching
|
||||||
|
*/
|
||||||
|
|
||||||
|
@keyframes shadow-color-change {
|
||||||
|
0%, 25% {
|
||||||
|
background-color: #0596ff;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: #BCBCBC;
|
||||||
|
}
|
||||||
|
75%, 100% {
|
||||||
|
background-color: #ff6d43;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-mdc-slide-toggle.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__shadow {
|
||||||
|
background-color: #ff6d43;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-mdc-slide-toggle.atos-company.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__shadow {
|
||||||
|
background-color: #0596ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-mdc-slide-toggle:not(.mat-mdc-slide-toggle-checked) .mdc-switch__shadow {
|
||||||
|
animation: shadow-color-change 3s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-mdc-slide-toggle.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__track::after {
|
||||||
|
background-color: #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-mdc-paginator-page-size-select {
|
||||||
|
width: 64px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bestenliste-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #000; /* Black color */
|
||||||
|
margin: 0 10px; /* Spacing between the toggle and search button */
|
||||||
|
padding: 0; /* Remove default padding if any */
|
||||||
|
line-height: 1; /* Adjust line height if needed */
|
||||||
|
}
|
||||||
124
src/app/score-table/score-table/score-table.component.html
Normal file
124
src/app/score-table/score-table/score-table.component.html
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<app-header></app-header>
|
||||||
|
<!-- Flex container -->
|
||||||
|
<div class="flex">
|
||||||
|
<!-- Centered container -->
|
||||||
|
<div class="center-container">
|
||||||
|
<div class="btn-header">
|
||||||
|
<mat-slide-toggle
|
||||||
|
#slideToggle
|
||||||
|
[(ngModel)]="showUserGames"
|
||||||
|
(click)="setPageSize()"
|
||||||
|
[class.atos-company]="isCompanyAtos()"
|
||||||
|
></mat-slide-toggle>
|
||||||
|
|
||||||
|
<!-- Bestenliste/Deine Spiele as an h1 element -->
|
||||||
|
<h1 *ngIf="!showUserGames" class="bestenliste-text">
|
||||||
|
iWuzzler Bestenliste
|
||||||
|
</h1>
|
||||||
|
<h1 *ngIf="showUserGames" class="bestenliste-text">Deine Spiele</h1>
|
||||||
|
|
||||||
|
<button
|
||||||
|
[disabled]="showUserGames"
|
||||||
|
class="btn-search"
|
||||||
|
mat-button
|
||||||
|
(click)="toggleSearch()"
|
||||||
|
>
|
||||||
|
<mat-icon matPrefix>search</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="score-table" *ngIf="!showUserGames">
|
||||||
|
<app-table-search
|
||||||
|
[isSearchActive]="isSearchActive"
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
></app-table-search>
|
||||||
|
|
||||||
|
<!--- Table -->
|
||||||
|
<div class="mat-elevation-z8">
|
||||||
|
<table
|
||||||
|
mat-table
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
aria-label="user-score-table"
|
||||||
|
>
|
||||||
|
<!-- Position Column -->
|
||||||
|
<ng-container matColumnDef="position">
|
||||||
|
<th class="center-text" mat-header-cell *matHeaderCellDef>
|
||||||
|
Position
|
||||||
|
</th>
|
||||||
|
<td class="center-text" mat-cell *matCellDef="let element">
|
||||||
|
{{ element.rank }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Name Column -->
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<th class="center-text" mat-header-cell *matHeaderCellDef>Name</th>
|
||||||
|
<td class="center-text" mat-cell *matCellDef="let element">
|
||||||
|
{{ element.firstName }} {{ element.lastName }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Score Column -->
|
||||||
|
<ng-container matColumnDef="wins">
|
||||||
|
<th class="center-text" mat-header-cell *matHeaderCellDef>Wins</th>
|
||||||
|
<td class="center-text" mat-cell *matCellDef="let element">
|
||||||
|
{{ element.wins }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr
|
||||||
|
(click)="goToUserPage(row.uid)"
|
||||||
|
mat-row
|
||||||
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
|
></tr>
|
||||||
|
</table>
|
||||||
|
<mat-paginator [pageSizeOptions]="[10, 25, 50]"></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-games-table mat-elevation-z8" *ngIf="showUserGames">
|
||||||
|
<table
|
||||||
|
mat-table
|
||||||
|
[dataSource]="dataSourceUserGames"
|
||||||
|
aria-label="user-games-table"
|
||||||
|
>
|
||||||
|
<!-- Date Column -->
|
||||||
|
<ng-container matColumnDef="date">
|
||||||
|
<th class="center-text" mat-header-cell *matHeaderCellDef>Zeit</th>
|
||||||
|
<td class="center-text" mat-cell *matCellDef="let element">
|
||||||
|
{{ element.date }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Score Column -->
|
||||||
|
<ng-container matColumnDef="score">
|
||||||
|
<th class="center-text" mat-header-cell *matHeaderCellDef>Score</th>
|
||||||
|
<td class="center-text" mat-cell *matCellDef="let element">
|
||||||
|
{{ element.score }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumnsUserGames"></tr>
|
||||||
|
<tr
|
||||||
|
mat-row
|
||||||
|
*matRowDef="let row; columns: displayedColumnsUserGames"
|
||||||
|
></tr>
|
||||||
|
<tr class="mat-row" *matNoDataRow>
|
||||||
|
<td class="mat-cell" [attr.colspan]="displayedColumns.length">
|
||||||
|
<p *ngIf="isUserLoggedIn(); else loggedOut" class="empty-table">
|
||||||
|
Keine Spiele gefunden.
|
||||||
|
</p>
|
||||||
|
<ng-template #loggedOut>
|
||||||
|
<p class="empty-table">
|
||||||
|
<a routerLink="/login">Logge dich ein</a>, um deine Spiele zu
|
||||||
|
sehen. Kein Konto?
|
||||||
|
<a routerLink="/signup">Registriere dich jetzt</a>
|
||||||
|
</p>
|
||||||
|
</ng-template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<mat-paginator [pageSizeOptions]="[10, 25, 50]"></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ScoreTableComponent } from './score-table.component';
|
||||||
|
|
||||||
|
describe('ScoreTableComponent', () => {
|
||||||
|
let component: ScoreTableComponent;
|
||||||
|
let fixture: ComponentFixture<ScoreTableComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ScoreTableComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ScoreTableComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
233
src/app/score-table/score-table/score-table.component.ts
Normal file
233
src/app/score-table/score-table/score-table.component.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import { Component, ElementRef, ViewChild, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
|
||||||
|
import { FormGroup, FormsModule } from '@angular/forms';
|
||||||
|
import { MatTableModule, MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { TableSearchComponent } from '../../score-table/table-search/table-search.component';
|
||||||
|
import { MatPaginator, MatPaginatorIntl, MatPaginatorModule } from '@angular/material/paginator';
|
||||||
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
import { RankedUser } from '../../auth/model/user.model';
|
||||||
|
import { ScoreTableService } from '../score-table.service';
|
||||||
|
import { Router, RouterModule } from '@angular/router';
|
||||||
|
import { MatCard } from '@angular/material/card';
|
||||||
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AuthService } from '../../auth/authService/auth.service';
|
||||||
|
import { MatIconModule, MatIcon } from '@angular/material/icon';
|
||||||
|
import { PendingActionsService } from '../../score/pending-actions.service';
|
||||||
|
import { HeaderComponent } from '../../shared/header/header.component';
|
||||||
|
import { CustomMatPaginatorIntl } from '../custom-mat-paginator-intl';
|
||||||
|
import { DocumentData } from '@angular/fire/firestore';
|
||||||
|
import { User as FirebaseUser } from 'firebase/auth';
|
||||||
|
import { CONSTANTS } from '../../shared/constants';
|
||||||
|
|
||||||
|
export interface Games {
|
||||||
|
id: string;
|
||||||
|
score: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-score-table',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './score-table.component.html',
|
||||||
|
styleUrls: ['./score-table.component.css'],
|
||||||
|
imports: [
|
||||||
|
MatTableModule,
|
||||||
|
TableSearchComponent,
|
||||||
|
MatPaginatorModule,
|
||||||
|
MatCard,
|
||||||
|
MatSlideToggleModule,
|
||||||
|
FormsModule,
|
||||||
|
CommonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatIcon,
|
||||||
|
HeaderComponent,
|
||||||
|
RouterModule,
|
||||||
|
],
|
||||||
|
providers: [{ provide: MatPaginatorIntl, useClass: CustomMatPaginatorIntl }],
|
||||||
|
})
|
||||||
|
|
||||||
|
export class ScoreTableComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
|
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||||
|
@ViewChild('slideToggle', { read: ElementRef }) slideToggleElement: ElementRef | undefined;
|
||||||
|
displayedColumns: string[] = ['position', 'name', 'wins'];
|
||||||
|
displayedColumnsUserGames: string[] = ['date', 'score'];
|
||||||
|
dataSource = new MatTableDataSource<RankedUser>([]);
|
||||||
|
dataSourceUserGames = new MatTableDataSource<Games>([]);
|
||||||
|
users$: Observable<RankedUser[]>;
|
||||||
|
subscription = new Subscription();
|
||||||
|
showUserGames = false;
|
||||||
|
isSearchActive = false;
|
||||||
|
userProfileForm!: FormGroup;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private scoreTableService: ScoreTableService,
|
||||||
|
private router: Router,
|
||||||
|
private authService: AuthService,
|
||||||
|
private pendingActionsService: PendingActionsService
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Subscribe to the observable that provides the top users
|
||||||
|
this.users$ = scoreTableService.getTopUsers();
|
||||||
|
// Subscribe to the observable that indicates whether to show user games
|
||||||
|
this.subscription.add(
|
||||||
|
this.scoreTableService.showUserGames$.subscribe((data) => {
|
||||||
|
this.showUserGames = data;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook that is called after Angular has initialized all data-bound properties.
|
||||||
|
* Here, we subscribe to the users$ observable to get the top users and set the data source.
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
this.subscription.add(
|
||||||
|
this.users$.subscribe((users) => {
|
||||||
|
this.setPageSize();
|
||||||
|
this.dataSource.data = users;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subscription.add(
|
||||||
|
this.authService.currentUser$.subscribe((user) => {
|
||||||
|
if (user) this.handleUser(user);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook that is called when the component is destroyed.
|
||||||
|
* Here, we unsubscribe from all subscriptions to prevent memory leaks.
|
||||||
|
*/
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook that is called after Angular has fully initialized a component's view.
|
||||||
|
* Here, we set the SVG paths for the slide toggle icons.
|
||||||
|
*/
|
||||||
|
ngAfterViewInit() {
|
||||||
|
if (this.slideToggleElement) {
|
||||||
|
this.slideToggleElement.nativeElement
|
||||||
|
.querySelector('.mdc-switch__icon--on')
|
||||||
|
.firstChild.setAttribute('d', CONSTANTS.SCORE_BOARD.PERSON_SVG_PATH);
|
||||||
|
this.slideToggleElement.nativeElement
|
||||||
|
.querySelector('.mdc-switch__icon--off')
|
||||||
|
.firstChild.setAttribute('d', CONSTANTS.SCORE_BOARD.GROUP_SVG_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the user data when a user is logged in.
|
||||||
|
* Loads the user's games and checks for any pending QR data.
|
||||||
|
*
|
||||||
|
* @param user - The currently logged-in user.
|
||||||
|
*/
|
||||||
|
async handleUser(user: FirebaseUser) {
|
||||||
|
if (!user?.uid) return;
|
||||||
|
|
||||||
|
this.dataSourceUserGames.data = await this.scoreTableService.loadUserGames(
|
||||||
|
user.uid
|
||||||
|
);
|
||||||
|
|
||||||
|
const pendingData = await this.pendingActionsService.checkForPendingQRData(
|
||||||
|
user.uid
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!pendingData) return;
|
||||||
|
|
||||||
|
if (this.isUserVerified()) {
|
||||||
|
this.navigateToUserProfile(pendingData.data());
|
||||||
|
this.pendingActionsService.deleteTempQRCodeData(
|
||||||
|
pendingData.data()['uid']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.pendingActionsService.addUserRefToQRCodeData(
|
||||||
|
user.uid,
|
||||||
|
pendingData.data()['uid']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the user profile page with the given pending data.
|
||||||
|
*
|
||||||
|
* @param pendingData - The data to pass to the user profile page after a QR code scan.
|
||||||
|
*/
|
||||||
|
navigateToUserProfile(pendingData: DocumentData) {
|
||||||
|
this.router.navigate(['/user-profile'], {
|
||||||
|
queryParams: {
|
||||||
|
score: pendingData['score'],
|
||||||
|
uid: pendingData['uid'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the page size for the paginator based on the screen width.
|
||||||
|
* Adjusts the page size for different screen sizes.
|
||||||
|
*/
|
||||||
|
setPageSize() {
|
||||||
|
const width = window.screen.width;
|
||||||
|
if (width < 768) {
|
||||||
|
this.paginator.pageSize = 10;
|
||||||
|
} else if (width >= 768 && width < 1024) {
|
||||||
|
this.paginator.pageSize = 25;
|
||||||
|
} else {
|
||||||
|
this.paginator.pageSize = 50;
|
||||||
|
}
|
||||||
|
if (this.showUserGames) {
|
||||||
|
this.dataSourceUserGames.paginator = this.paginator;
|
||||||
|
} else {
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the search functionality on and off.
|
||||||
|
*/
|
||||||
|
toggleSearch() {
|
||||||
|
this.isSearchActive = !this.isSearchActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to user profile pages for the given user ID.
|
||||||
|
*
|
||||||
|
* @param uid - The user ID to navigate to.
|
||||||
|
*/
|
||||||
|
goToUserPage(uid: string) {
|
||||||
|
if (this.isUserVerified()) {
|
||||||
|
this.router.navigate(['/user-profile', uid]);
|
||||||
|
this.scoreTableService.setShowUserGames(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current user's email belongs to the Atos company.
|
||||||
|
*
|
||||||
|
* @returns True if the user's email includes 'atos.net', false otherwise.
|
||||||
|
*/
|
||||||
|
isCompanyAtos() {
|
||||||
|
const email = this.authService.getCurrentUser()?.email;
|
||||||
|
return email && email.includes('atos.net');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current user's email is verified after registration.
|
||||||
|
*
|
||||||
|
* @returns True if the user's email is verified, false otherwise.
|
||||||
|
*/
|
||||||
|
isUserVerified() {
|
||||||
|
return this.authService.getCurrentUser()?.emailVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a user is currently logged in.
|
||||||
|
*
|
||||||
|
* @returns The current user if logged in, null otherwise.
|
||||||
|
*/
|
||||||
|
isUserLoggedIn() {
|
||||||
|
return this.authService.getCurrentUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/app/score-table/table-search/table-search.component.css
Normal file
15
src/app/score-table/table-search/table-search.component.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.flex-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 767px) {
|
||||||
|
.flex-container {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-item {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
47
src/app/score-table/table-search/table-search.component.html
Normal file
47
src/app/score-table/table-search/table-search.component.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<!-- Flex container -->
|
||||||
|
<div class="flex-container" *ngIf="isSearchActive">
|
||||||
|
<!-- Search Input -->
|
||||||
|
<mat-form-field appearance="outline" class="flex-item">
|
||||||
|
<mat-label>Suche</mat-label>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="filteredValues.name"
|
||||||
|
(ngModelChange)="applyNameFilter($event)"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="flex-item">
|
||||||
|
<mat-select
|
||||||
|
[(value)]="scoreSelect.defaultValue"
|
||||||
|
(selectionChange)="applyScoreFilter($event)"
|
||||||
|
>
|
||||||
|
<mat-option *ngFor="let op of scoreSelect.options" [value]="op">
|
||||||
|
{{ op }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="flex-item">
|
||||||
|
<mat-label>Score</mat-label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="filteredValues.score.value"
|
||||||
|
(ngModelChange)="applyFilter()"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="flex-item">
|
||||||
|
<mat-label>{{ companySelect.name }}</mat-label>
|
||||||
|
<mat-select
|
||||||
|
[(value)]="companySelect.defaultValue"
|
||||||
|
(selectionChange)="applySelectFilter($event)"
|
||||||
|
>
|
||||||
|
<mat-option *ngFor="let op of companySelect.options" [value]="op">
|
||||||
|
{{ op }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TableSearchComponent } from './table-search.component';
|
||||||
|
|
||||||
|
describe('TableSearchComponent', () => {
|
||||||
|
let component: TableSearchComponent;
|
||||||
|
let fixture: ComponentFixture<TableSearchComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TableSearchComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(TableSearchComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
106
src/app/score-table/table-search/table-search.component.ts
Normal file
106
src/app/score-table/table-search/table-search.component.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { AngularMaterialModule } from '../../angular-material/angular-material.module';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { MatTableModule, MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { MatSelectModule, MatSelectChange } from '@angular/material/select';
|
||||||
|
import { RankedUser, User } from '../../auth/model/user.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-table-search',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
AngularMaterialModule,
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatTableModule,
|
||||||
|
],
|
||||||
|
templateUrl: './table-search.component.html',
|
||||||
|
styleUrl: './table-search.component.css',
|
||||||
|
})
|
||||||
|
export class TableSearchComponent {
|
||||||
|
@Input() dataSource = new MatTableDataSource<RankedUser>([]);
|
||||||
|
@Input() isSearchActive = false;
|
||||||
|
company: string[] = ['All', 'Atos', 'Eviden'];
|
||||||
|
companyDefaultValue = 'All';
|
||||||
|
companySelect = {
|
||||||
|
name: 'company',
|
||||||
|
options: this.company,
|
||||||
|
defaultValue: this.companyDefaultValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
scoreOperation: string[] = ['weniger als', 'mehr als'];
|
||||||
|
scoreOperationDefaultValue = 'weniger als';
|
||||||
|
scoreSelect = {
|
||||||
|
name: 'operation',
|
||||||
|
options: this.scoreOperation,
|
||||||
|
defaultValue: this.scoreOperationDefaultValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
filteredValues = {
|
||||||
|
name: '',
|
||||||
|
score: { value: null, operation: 'weniger als' },
|
||||||
|
company: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.dataSource.filterPredicate = this.customFilterPredicate();
|
||||||
|
}
|
||||||
|
|
||||||
|
customFilterPredicate() {
|
||||||
|
const myFilterPredicate = (data: User, filter: string): boolean => {
|
||||||
|
let searchString = JSON.parse(filter);
|
||||||
|
let name =
|
||||||
|
(data.firstName ? data.firstName : '') +
|
||||||
|
(data.lastName ? data.lastName : '');
|
||||||
|
let nameResult = name
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchString.name.trim().toLowerCase());
|
||||||
|
|
||||||
|
let companyResult =
|
||||||
|
searchString.company == 'All' ||
|
||||||
|
(data.email ? data.email : '')
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchString.company.trim().toLowerCase());
|
||||||
|
let scoreResult = true;
|
||||||
|
if (searchString.score.value != null && data.wins != undefined) {
|
||||||
|
switch (searchString.score.operation) {
|
||||||
|
case 'weniger als':
|
||||||
|
scoreResult = data.wins <= searchString.score.value;
|
||||||
|
break;
|
||||||
|
case 'mehr als':
|
||||||
|
scoreResult = data.wins >= searchString.score.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nameResult && companyResult && scoreResult;
|
||||||
|
};
|
||||||
|
return myFilterPredicate;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter() {
|
||||||
|
this.dataSource.filter = JSON.stringify(this.filteredValues);
|
||||||
|
this.dataSource.paginator?.firstPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyNameFilter(filter: any) {
|
||||||
|
this.filteredValues.name = filter;
|
||||||
|
this.applyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
applySelectFilter(event: MatSelectChange) {
|
||||||
|
this.filteredValues.company = event.value;
|
||||||
|
this.applyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyScoreFilter(event: MatSelectChange) {
|
||||||
|
this.filteredValues.score.operation = event.value;
|
||||||
|
this.applyFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/app/score/pending-actions.service.ts
Normal file
115
src/app/score/pending-actions.service.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
Firestore,
|
||||||
|
collection,
|
||||||
|
deleteDoc,
|
||||||
|
doc,
|
||||||
|
getDoc,
|
||||||
|
getDocs,
|
||||||
|
query,
|
||||||
|
setDoc,
|
||||||
|
updateDoc,
|
||||||
|
where,
|
||||||
|
} from '@angular/fire/firestore';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class PendingActionsService {
|
||||||
|
private tempQRId: string | null = null;
|
||||||
|
|
||||||
|
constructor(private firestore: Firestore) {}
|
||||||
|
|
||||||
|
setTempQRId(id: string) {
|
||||||
|
this.tempQRId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTempQRId(): string | null {
|
||||||
|
return this.tempQRId;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTempQRId() {
|
||||||
|
this.tempQRId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTempQRCodeData(qrData: any) {
|
||||||
|
const docRef = doc(collection(this.firestore, 'tempQRData'), qrData.uid);
|
||||||
|
setDoc(docRef, qrData).catch((error) => {
|
||||||
|
console.error('Error saving qr data', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
return docRef.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTempQRCodeData(uid: string | null) {
|
||||||
|
if (uid === null) return null;
|
||||||
|
const docRef = doc(this.firestore, `tempQRData/${uid}`);
|
||||||
|
return getDoc(docRef)
|
||||||
|
.then((docSnap) => {
|
||||||
|
if (docSnap.exists()) {
|
||||||
|
return docSnap.data();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error getting qr data', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTempQRCodeData(uid: string | null) {
|
||||||
|
if (uid === null) return null;
|
||||||
|
const docRef = doc(this.firestore, `tempQRData/${uid}`);
|
||||||
|
return deleteDoc(docRef).catch((error) => {
|
||||||
|
console.error('Error deleting qr data', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addUserRefToQRCodeData(userId: string | null, uid: string | null) {
|
||||||
|
if (userId === null || uid === null) return null;
|
||||||
|
const docRef = doc(this.firestore, `tempQRData/${uid}`);
|
||||||
|
const userRef = doc(this.firestore, 'users', userId);
|
||||||
|
|
||||||
|
return updateDoc(docRef, { userRef: userRef });
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForPendingQRData(userId: string | null) {
|
||||||
|
if (userId === null) return null;
|
||||||
|
const userRef = doc(this.firestore, 'users', userId);
|
||||||
|
|
||||||
|
const tempQRDataRef = collection(this.firestore, 'tempQRData');
|
||||||
|
const q = query(tempQRDataRef, where('userRef', '==', userRef));
|
||||||
|
|
||||||
|
return getDocs(q)
|
||||||
|
.then((querySnapshot) => {
|
||||||
|
if (querySnapshot.empty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const firstDoc = querySnapshot.docs[0];
|
||||||
|
return firstDoc;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Fehler beim Abrufen der Dokumente:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setScoreLocalStorage(score: string, uid: string) {
|
||||||
|
localStorage.setItem('score', score);
|
||||||
|
localStorage.setItem('uid', uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
getScoreLocalStorage() {
|
||||||
|
return {
|
||||||
|
score: localStorage.getItem('score'),
|
||||||
|
uid: localStorage.getItem('uid'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLocalStorage() {
|
||||||
|
localStorage.removeItem('score');
|
||||||
|
localStorage.removeItem('uid');
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/app/score/pending-score.guard.ts
Normal file
64
src/app/score/pending-score.guard.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
Router,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { take, switchMap } from 'rxjs/operators';
|
||||||
|
import { AuthService } from '../auth/authService/auth.service';
|
||||||
|
import { PendingActionsService } from './pending-actions.service';
|
||||||
|
import { NotifierService } from '../shared/notifierService/notifier.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class PendingScoreGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private pendingActionsService: PendingActionsService,
|
||||||
|
private router: Router,
|
||||||
|
private notifierService: NotifierService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
canActivate(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot
|
||||||
|
): Observable<boolean> {
|
||||||
|
return this.authService.currentUser$.pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((user) => {
|
||||||
|
if (user) {
|
||||||
|
// User is logged in, allow access to the route
|
||||||
|
return of(true);
|
||||||
|
} else {
|
||||||
|
// User is not logged in, store the score and redirect to login
|
||||||
|
const score = route.queryParams['score'];
|
||||||
|
const uid = route.queryParams['uid'];
|
||||||
|
if (score && uid) {
|
||||||
|
const uniqueID = this.pendingActionsService.saveTempQRCodeData({
|
||||||
|
score,
|
||||||
|
uid,
|
||||||
|
});
|
||||||
|
this.pendingActionsService.setTempQRId(uniqueID);
|
||||||
|
this.router.navigate(['/login'], {
|
||||||
|
queryParams: {
|
||||||
|
score: score,
|
||||||
|
uid: uid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.notifierService.showNotification(
|
||||||
|
'Bitte registriere oder logge dich ein, um ein Spiel zu deinem Konto hinzuzufügen.',
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
return of(false);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/app/shared/constants.ts
Normal file
7
src/app/shared/constants.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const CONSTANTS = {
|
||||||
|
SCORE_BOARD: {
|
||||||
|
PERSON_SVG_PATH: 'M12 6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2m0 10c2.7 0 5.8 1.29 6 2H6c.23-.72 3.31-2 6-2m0-12C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 10c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z',
|
||||||
|
GROUP_SVG_PATH: 'M4,13c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2s-2,0.9-2,2C2,12.1,2.9,13,4,13z M5.13,14.1C4.76,14.04,4.39,14,4,14 c-0.99,0-1.93,0.21-2.78,0.58C0.48,14.9,0,15.62,0,16.43V18l4.5,0v-1.61C4.5,15.56,4.73,14.78,5.13,14.1z M20,13c1.1,0,2-0.9,2-2 c0-1.1-0.9-2-2-2s-2,0.9-2,2C18,12.1,18.9,13,20,13z M24,16.43c0-0.81-0.48-1.53-1.22-1.85C21.93,14.21,20.99,14,20,14 c-0.39,0-0.76,0.04-1.13,0.1c0.4,0.68,0.63,1.46,0.63,2.29V18l4.5,0V16.43z M16.24,13.65c-1.17-0.52-2.61-0.9-4.24-0.9 c-1.63,0-3.07,0.39-4.24,0.9C6.68,14.13,6,15.21,6,16.39V18h12v-1.61C18,15.21,17.32,14.13,16.24,13.65z M8.07,16 c0.09-0.23,0.13-0.39,0.91-0.69c0.97-0.38,1.99-0.56,3.02-0.56s2.05,0.18,3.02,0.56c0.77,0.3,0.81,0.46,0.91,0.69H8.07z M12,8 c0.55,0,1,0.45,1,1s-0.45,1-1,1s-1-0.45-1-1S11.45,8,12,8 M12,6c-1.66,0-3,1.34-3,3c0,1.66,1.34,3,3,3s3-1.34,3-3 C15,7.34,13.66,6,12,6L12,6z'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
3
src/app/shared/custom-dialog/custom-dialog.component.css
Normal file
3
src/app/shared/custom-dialog/custom-dialog.component.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.dialog-container {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
18
src/app/shared/custom-dialog/custom-dialog.component.html
Normal file
18
src/app/shared/custom-dialog/custom-dialog.component.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="dialog-container">
|
||||||
|
<h2 mat-dialog-title>{{ data.title }}</h2>
|
||||||
|
<mat-dialog-content>
|
||||||
|
<p>
|
||||||
|
{{ data.bodyText }}
|
||||||
|
</p>
|
||||||
|
</mat-dialog-content>
|
||||||
|
<mat-dialog-actions [align]="'end'">
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
[mat-dialog-close]="data.returnValue"
|
||||||
|
(click)="data.onConfirm ? data.onConfirm() : null"
|
||||||
|
mat-dialog-close
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
|
</div>
|
||||||
22
src/app/shared/custom-dialog/custom-dialog.component.spec.ts
Normal file
22
src/app/shared/custom-dialog/custom-dialog.component.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CustomDialogComponent } from './custom-dialog.component';
|
||||||
|
|
||||||
|
describe('CustomDialogComponent', () => {
|
||||||
|
let component: CustomDialogComponent;
|
||||||
|
let fixture: ComponentFixture<CustomDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CustomDialogComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CustomDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
32
src/app/shared/custom-dialog/custom-dialog.component.ts
Normal file
32
src/app/shared/custom-dialog/custom-dialog.component.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import {
|
||||||
|
MatDialogTitle,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogClose,
|
||||||
|
} from '@angular/material/dialog';
|
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-custom-dialog',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MatDialogTitle,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogClose,
|
||||||
|
MatButtonModule,
|
||||||
|
],
|
||||||
|
templateUrl: './custom-dialog.component.html',
|
||||||
|
styleUrl: './custom-dialog.component.css',
|
||||||
|
})
|
||||||
|
export class CustomDialogComponent {
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public data: any) {}
|
||||||
|
|
||||||
|
onConfirm(): void {
|
||||||
|
if (this.data.onConfirm) {
|
||||||
|
this.data.onConfirm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { AngularMaterialModule } from '../../angular-material/angular-material.module';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'header-lsr',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './header-lsr.html',
|
||||||
|
styleUrl: '././header-lsr.css',
|
||||||
|
imports: [AngularMaterialModule],
|
||||||
|
})
|
||||||
|
export class HeaderLoginSignupResetpw {
|
||||||
|
constructor(private router: Router) {}
|
||||||
|
|
||||||
|
goToScoreTable() {
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/app/shared/header-login-signup-resetpw/header-lsr.css
Normal file
24
src/app/shared/header-login-signup-resetpw/header-lsr.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* Card */
|
||||||
|
mat-card {
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image */
|
||||||
|
img {
|
||||||
|
display: flex;
|
||||||
|
width: 3.1rem;
|
||||||
|
height: 3.1rem;
|
||||||
|
margin-top: 1.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Header */
|
||||||
|
mat-card-header {
|
||||||
|
color: #000;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
}
|
||||||
12
src/app/shared/header-login-signup-resetpw/header-lsr.html
Normal file
12
src/app/shared/header-login-signup-resetpw/header-lsr.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="m-auto kick-motto cursor-enable" (click)="goToScoreTable()">
|
||||||
|
<img
|
||||||
|
class="m-auto"
|
||||||
|
mat-card-image src="assets\img\AtosEvidenBall.png"
|
||||||
|
alt="AtosEvidenBall"
|
||||||
|
>
|
||||||
|
<!-- Card header -->
|
||||||
|
<mat-card-header class="m-auto">
|
||||||
|
<!-- Header text -->
|
||||||
|
<p class="kick-motto-style">Kick it like <span class="atos-color">Atos</span> & <span class="eviden-color">Eviden</span></p>
|
||||||
|
</mat-card-header>
|
||||||
|
</div>
|
||||||
21
src/app/shared/header-menu/header-menu.component.html
Normal file
21
src/app/shared/header-menu/header-menu.component.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<button mat-icon-button [matMenuTriggerFor]="isUserLoggedIn() ? menu : menu2">
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
<!-- User logged in -->
|
||||||
|
<mat-menu #menu="matMenu">
|
||||||
|
<button mat-menu-item (click)="navigateToProfileOrScoreboard()">
|
||||||
|
<mat-icon>{{ isOnScoreTable() ? 'account_circle' : 'scoreboard' }}</mat-icon>
|
||||||
|
<span>{{ isOnScoreTable() ? 'Mein Profil' : 'Scoreboard'}}</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="logout()">
|
||||||
|
<mat-icon>logout</mat-icon>
|
||||||
|
<span>Logout</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
<!-- User not logged in -->
|
||||||
|
<mat-menu #menu2="matMenu">
|
||||||
|
<button mat-menu-item (click)="login()">
|
||||||
|
<mat-icon>login</mat-icon>
|
||||||
|
<span>Login</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
23
src/app/shared/header-menu/header-menu.component.spec.ts
Normal file
23
src/app/shared/header-menu/header-menu.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HeaderMenuComponent } from './header-menu.component';
|
||||||
|
|
||||||
|
describe('HeaderMenuComponent', () => {
|
||||||
|
let component: HeaderMenuComponent;
|
||||||
|
let fixture: ComponentFixture<HeaderMenuComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [HeaderMenuComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HeaderMenuComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
55
src/app/shared/header-menu/header-menu.component.ts
Normal file
55
src/app/shared/header-menu/header-menu.component.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Component, ViewChild } from '@angular/core';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../../auth/authService/auth.service';
|
||||||
|
import { NotifierService } from '../notifierService/notifier.service';
|
||||||
|
import { ScoreTableService } from '../../score-table/score-table.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-header-menu',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatButtonModule, MatMenuModule, MatIconModule],
|
||||||
|
templateUrl: './header-menu.component.html',
|
||||||
|
styleUrl: './header-menu.component.css',
|
||||||
|
})
|
||||||
|
export class HeaderMenuComponent {
|
||||||
|
@ViewChild(MatMenuTrigger) trigger!: MatMenuTrigger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private authService: AuthService,
|
||||||
|
private notifierService: NotifierService,
|
||||||
|
private scoreTableService: ScoreTableService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
isUserLoggedIn() {
|
||||||
|
return this.authService.getCurrentUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
isOnScoreTable() {
|
||||||
|
return this.router.url === '/score-table';
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToProfileOrScoreboard() {
|
||||||
|
this.scoreTableService.setShowUserGames(false);
|
||||||
|
if (this.isOnScoreTable()) {
|
||||||
|
this.router.navigate(['/user-profile']);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.authService.logout();
|
||||||
|
this.trigger.closeMenu();
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
window.location.reload();
|
||||||
|
this.notifierService.showNotification('Du wurdest ausgeloggt.', 'OK');
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/app/shared/header/header.component.css
Normal file
58
src/app/shared/header/header.component.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: sticky;
|
||||||
|
z-index: 100;
|
||||||
|
top: 0;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #f6f9fc65;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
font-size: 18px ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title{
|
||||||
|
margin: 0 1rem;
|
||||||
|
font-size: 18px;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-container,
|
||||||
|
.header-menu-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 5%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-container {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball-container img {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.clickable-image {
|
||||||
|
cursor: pointer; /* Changes the cursor to indicate the image is clickable */
|
||||||
|
transition: transform 0.3s ease; /* Smooth transition for feedback */
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-image:active {
|
||||||
|
transform: scale(0.95); /* Scales down the image when clicked */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Define the spin animation */
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Class to apply the spin animation */
|
||||||
|
.spin-animation {
|
||||||
|
animation: spin 0.8s; /* Run the spin animation for 1 second */
|
||||||
|
}
|
||||||
18
src/app/shared/header/header.component.html
Normal file
18
src/app/shared/header/header.component.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="header">
|
||||||
|
<div class="ball-container">
|
||||||
|
<img
|
||||||
|
#clickableImage
|
||||||
|
(click)="goToScoreTable(clickableImage)"
|
||||||
|
class="clickable-image"
|
||||||
|
src="assets/img/AtosEvidenBall 3.png"
|
||||||
|
alt="AtosEvidenBall"
|
||||||
|
title="Go to Score Table"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="header-title" (click)="goToScoreTable(clickableImage)">
|
||||||
|
Kick it like <span style="color: #0596FF;">Atos</span> & <span style="color: #FF6D43;">Eviden</span>
|
||||||
|
</p>
|
||||||
|
<div class="header-menu-container">
|
||||||
|
<app-header-menu></app-header-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
23
src/app/shared/header/header.component.spec.ts
Normal file
23
src/app/shared/header/header.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HeaderComponent } from './header.component';
|
||||||
|
|
||||||
|
describe('HeaderComponent', () => {
|
||||||
|
let component: HeaderComponent;
|
||||||
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [HeaderComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HeaderComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
25
src/app/shared/header/header.component.ts
Normal file
25
src/app/shared/header/header.component.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {Component, Renderer2} from '@angular/core';
|
||||||
|
import { HeaderMenuComponent } from '../header-menu/header-menu.component';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-header',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './header.component.html',
|
||||||
|
styleUrl: './header.component.css',
|
||||||
|
imports: [HeaderMenuComponent],
|
||||||
|
})
|
||||||
|
export class HeaderComponent {
|
||||||
|
constructor(private router: Router, private renderer: Renderer2) {}
|
||||||
|
|
||||||
|
goToScoreTable(image: HTMLElement) {
|
||||||
|
this.renderer.addClass(image, 'spin-animation');
|
||||||
|
|
||||||
|
// Wait for the animation to complete before navigating
|
||||||
|
setTimeout(() => {
|
||||||
|
this.renderer.removeClass(image, 'spin-animation');
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
}, 800); // Adjust the timeout/delay to match the animation duration (milliseconds)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<div class="loadingio-spinner-dual-ball-sv85ib7wgti"><div class="ldio-ia0w02586ob">
|
||||||
|
<div></div><div></div><div></div>
|
||||||
|
</div></div>
|
||||||
|
<style type="text/css">
|
||||||
|
@keyframes ldio-ia0w02586ob-o {
|
||||||
|
0% { opacity: 1; transform: translate(0 0) }
|
||||||
|
49.99% { opacity: 1; transform: translate(80px,0) }
|
||||||
|
50% { opacity: 0; transform: translate(80px,0) }
|
||||||
|
100% { opacity: 0; transform: translate(0,0) }
|
||||||
|
}
|
||||||
|
@keyframes ldio-ia0w02586ob {
|
||||||
|
0% { transform: translate(0,0) }
|
||||||
|
50% { transform: translate(80px,0) }
|
||||||
|
100% { transform: translate(0,0) }
|
||||||
|
}
|
||||||
|
.ldio-ia0w02586ob div {
|
||||||
|
position: absolute;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 60px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
.ldio-ia0w02586ob div:nth-child(1) {
|
||||||
|
background: #ff6d43;
|
||||||
|
animation: ldio-ia0w02586ob 1.36986301369863s linear infinite;
|
||||||
|
animation-delay: -0.684931506849315s;
|
||||||
|
}
|
||||||
|
.ldio-ia0w02586ob div:nth-child(2) {
|
||||||
|
background: #0596ff;
|
||||||
|
animation: ldio-ia0w02586ob 1.36986301369863s linear infinite;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
.ldio-ia0w02586ob div:nth-child(3) {
|
||||||
|
background: #ff6d43;
|
||||||
|
animation: ldio-ia0w02586ob-o 1.36986301369863s linear infinite;
|
||||||
|
animation-delay: -0.684931506849315s;
|
||||||
|
}
|
||||||
|
.loadingio-spinner-dual-ball-sv85ib7wgti {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #F6F9FC;
|
||||||
|
}
|
||||||
|
.ldio-ia0w02586ob {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
transform: translateZ(0) scale(1);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform-origin: 0 0; /* see note above */
|
||||||
|
}
|
||||||
|
.ldio-ia0w02586ob div { box-sizing: content-box; }
|
||||||
|
/* generated by https://loading.io/ */
|
||||||
|
</style>
|
||||||
12
src/app/shared/loading-spinner/loading-spinner.component.ts
Normal file
12
src/app/shared/loading-spinner/loading-spinner.component.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-loading-spinner',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './loading-spinner.component.html',
|
||||||
|
styleUrl: './loading-spinner.component.css'
|
||||||
|
})
|
||||||
|
export class LoadingSpinnerComponent {
|
||||||
|
|
||||||
|
}
|
||||||
36
src/app/shared/notifierService/notifier.service.ts
Normal file
36
src/app/shared/notifierService/notifier.service.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class NotifierService {
|
||||||
|
|
||||||
|
constructor(private snackBar: MatSnackBar) { }
|
||||||
|
|
||||||
|
// Method to display a notification message with a button
|
||||||
|
showNotification(displayMessage: string, button: string) {
|
||||||
|
// Opens a snack bar with the provided message and button text
|
||||||
|
this.snackBar.open(displayMessage, button, {
|
||||||
|
// Duration for which the notification is displayed (in milliseconds)
|
||||||
|
duration: 7000,
|
||||||
|
// Horizontal position of the notification on the screen
|
||||||
|
horizontalPosition: 'center',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to display a notification message without a button
|
||||||
|
showInformation(displayMessage: string) {
|
||||||
|
this.snackBar.open(displayMessage, undefined, {
|
||||||
|
duration: 7000,
|
||||||
|
horizontalPosition: 'center',
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Close Snackbar
|
||||||
|
dismissNotification() {
|
||||||
|
if (this.snackBar) {
|
||||||
|
this.snackBar.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.spinner-section {
|
||||||
|
color: rgba(0, 0, 0, 0.87)
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-spinner-color .mat-progress-spinner circle {
|
||||||
|
stroke: #002D3C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-spinner-color .mat-progress-spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field.custom-form-field {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 32px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="spinner-section" *ngIf="showSpinner">
|
||||||
|
<!-- Show spinner when loading -->
|
||||||
|
<mat-progress-spinner
|
||||||
|
class="custom-spinner-color"
|
||||||
|
[mode]="mode"
|
||||||
|
[value]="value"
|
||||||
|
[diameter]="20"
|
||||||
|
[strokeWidth]="2"
|
||||||
|
></mat-progress-spinner>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { ProgressSpinnerMode, MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-progress-spinner',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatProgressSpinnerModule, CommonModule],
|
||||||
|
templateUrl: './progress-spinner.component.html',
|
||||||
|
styleUrls: ['./progress-spinner.component.css']
|
||||||
|
})
|
||||||
|
export class ProgressSpinnerComponent {
|
||||||
|
mode: ProgressSpinnerMode = 'indeterminate';
|
||||||
|
value = 0;
|
||||||
|
@Input() showSpinner: boolean = false;
|
||||||
|
}
|
||||||
26
src/app/shared/validation/passwordMatcherValidation.ts
Normal file
26
src/app/shared/validation/passwordMatcherValidation.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||||
|
|
||||||
|
export enum PasswordValidationType {
|
||||||
|
Match,
|
||||||
|
Different
|
||||||
|
}
|
||||||
|
|
||||||
|
export function passwordValidator(matchTo: string, validationType: PasswordValidationType): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
if (!control.parent || !control.parent.get(matchTo)) {
|
||||||
|
return null; // parent or matching control not found
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchToControl = control.parent.get(matchTo);
|
||||||
|
|
||||||
|
// Depending on the validation type, return the appropriate error
|
||||||
|
switch (validationType) {
|
||||||
|
case PasswordValidationType.Match:
|
||||||
|
return control.value === matchToControl?.value ? null : { passwordMismatch: true };
|
||||||
|
case PasswordValidationType.Different:
|
||||||
|
return control.value !== matchToControl?.value ? null : { passwordsSame: true };
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
47
src/app/shared/validation/passwordSaferValidation.ts
Normal file
47
src/app/shared/validation/passwordSaferValidation.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
|
||||||
|
|
||||||
|
export function passwordStrengthValidator(): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const value = control.value;
|
||||||
|
if (!value) {
|
||||||
|
return null; // Don't validate empty value
|
||||||
|
}
|
||||||
|
const hasUpperCase = /[A-Z]/.test(value);
|
||||||
|
const hasNumber = /\d/.test(value);
|
||||||
|
//Special character validation commented out until Chrome decides to generate passwords with special characters -.-
|
||||||
|
//const hasSymbol = /[!@#$%^&*(),.?":{}|<>-]/.test(value);
|
||||||
|
|
||||||
|
const valid = hasUpperCase && hasNumber;
|
||||||
|
return valid ? null : { passwordStrength: true };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasUpperCaseValidator(): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const value = control.value;
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const hasUpperCase = /[A-Z]/.test(value);
|
||||||
|
//Special character validation commented out until Chrome decides to generate passwords with special characters -.-
|
||||||
|
//const hasSymbol = /[!@#$%^&*(),.?":{}|<>-]/.test(value);
|
||||||
|
|
||||||
|
const valid = hasUpperCase;
|
||||||
|
return valid ? null : { hasUpperCase: true };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasNumberValidator(): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const value = control.value;
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const hasNumber = /\d/.test(value);
|
||||||
|
//Special character validation commented out until Chrome decides to generate passwords with special characters -.-
|
||||||
|
//const hasSymbol = /[!@#$%^&*(),.?":{}|<>-]/.test(value);
|
||||||
|
|
||||||
|
const valid = hasNumber;
|
||||||
|
return valid ? null : { hasNumber: true };
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
.upload-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-container button {
|
||||||
|
background-color: rgba(0, 0, 0, 0.25); ;
|
||||||
|
color: #FFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
margin-right: 5px;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-photo-upload-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<div class="profile-photo-upload-container" style="margin-bottom: 40px;">
|
||||||
|
<!-- Hidden file input for styling purposes -->
|
||||||
|
<input type="file" #fileInput hidden (change)="onFileSelected($event)" accept="image/*">
|
||||||
|
<div class="upload-container">
|
||||||
|
<button *ngIf="!currentUser?.photoUrl" (click)="fileSelected ? uploadFile() : fileInput.click()" class="image-upload-button">
|
||||||
|
<mat-icon>{{ fileSelected ? 'upload_file' : 'image' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button *ngIf="currentUser?.photoUrl" mat-fab (click)="deleteFile()" class="close-icon-button">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ProfilePhotoUploadComponent } from './profile-photo-upload.component';
|
||||||
|
|
||||||
|
describe('ProfilePhotoUploadComponent', () => {
|
||||||
|
let component: ProfilePhotoUploadComponent;
|
||||||
|
let fixture: ComponentFixture<ProfilePhotoUploadComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ProfilePhotoUploadComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ProfilePhotoUploadComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { UserService } from '../../user.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { User } from '../../../auth/model/user.model';
|
||||||
|
import { ProfilePhotoUploadService } from './profile-photo-upload.service';
|
||||||
|
import { AngularMaterialModule } from '../../../angular-material/angular-material.module';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NotifierService } from '../../../shared/notifierService/notifier.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-profile-photo-upload',
|
||||||
|
standalone: true,
|
||||||
|
imports: [AngularMaterialModule, CommonModule],
|
||||||
|
templateUrl: './profile-photo-upload.component.html',
|
||||||
|
styleUrls: ['./profile-photo-upload.component.css'],
|
||||||
|
})
|
||||||
|
export class ProfilePhotoUploadComponent implements OnInit, OnDestroy {
|
||||||
|
//TODO check why the userId remains empty/null in component, but in service the uid is returned proper?
|
||||||
|
userProfileSubscription!: Subscription;
|
||||||
|
currentUser: User | null = null;
|
||||||
|
fileSelected: boolean = false;
|
||||||
|
metadata = {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
customMetadata: {
|
||||||
|
imgId: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
selectedFile: File | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userService: UserService,
|
||||||
|
private profilePhotoUploadService: ProfilePhotoUploadService,
|
||||||
|
private notifierService: NotifierService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.userProfileSubscription =
|
||||||
|
this.userService.currentUserProfile$.subscribe((user: User | null) => {
|
||||||
|
this.currentUser = user;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.userProfileSubscription) {
|
||||||
|
this.userProfileSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileSelected(event: any) {
|
||||||
|
this.selectedFile = event.target.files[0];
|
||||||
|
this.fileSelected = true;
|
||||||
|
this.notifierService.showNotification(
|
||||||
|
`Datei wurde ausgewählt. Bitte klicke auf die Schaltfläche "Hochladen", um Bild einzustellen.`,
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFile() {
|
||||||
|
if (this.selectedFile && this.currentUser && this.currentUser.uid) {
|
||||||
|
const file = this.selectedFile;
|
||||||
|
const metadata = {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
customMetadata: {
|
||||||
|
imgId: this.currentUser?.uid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await this.profilePhotoUploadService.uploadFile(
|
||||||
|
this.currentUser.uid,
|
||||||
|
file,
|
||||||
|
metadata
|
||||||
|
);
|
||||||
|
this.notifierService.showNotification(`Datei wurde hochgeladen`, 'OK');
|
||||||
|
} else {
|
||||||
|
this.notifierService.showInformation(
|
||||||
|
'Keine Datei ausgewählt oder Benutzer-ID ist unbekannt.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile() {
|
||||||
|
if (this.currentUser && this.currentUser.uid) {
|
||||||
|
await this.profilePhotoUploadService.deleteFile(this.currentUser.uid);
|
||||||
|
this.notifierService.showNotification(`Datei wurde gelöscht`, 'OK');
|
||||||
|
} else {
|
||||||
|
this.notifierService.showInformation('Benutzer-ID ist unbekannt. Bitte melde dich an, um die Datei zu löschen.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
getStorage,
|
||||||
|
ref,
|
||||||
|
uploadBytesResumable,
|
||||||
|
getDownloadURL,
|
||||||
|
deleteObject,
|
||||||
|
} from 'firebase/storage';
|
||||||
|
import {
|
||||||
|
getFirestore,
|
||||||
|
doc,
|
||||||
|
setDoc,
|
||||||
|
getDoc,
|
||||||
|
deleteDoc,
|
||||||
|
updateDoc,
|
||||||
|
} from 'firebase/firestore';
|
||||||
|
import { NotifierService } from '../../../shared/notifierService/notifier.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ProfilePhotoUploadService {
|
||||||
|
storage = getStorage();
|
||||||
|
firestore = getFirestore();
|
||||||
|
|
||||||
|
constructor(private notifierService: NotifierService) {}
|
||||||
|
|
||||||
|
async uploadFile(userId: string, file: File, metadata: any): Promise<void> {
|
||||||
|
// get the current user's file metadata from firestore
|
||||||
|
const userDocRef = doc(this.firestore, 'users', userId);
|
||||||
|
const fileDocRef = doc(this.firestore, 'files', userId);
|
||||||
|
const fileDoc = await getDoc(fileDocRef);
|
||||||
|
|
||||||
|
if (fileDoc.exists()) {
|
||||||
|
// if there's an existing file, delete it from storage
|
||||||
|
const existingFilePath = fileDoc.data()['filePath'];
|
||||||
|
const existingFileRef = ref(this.storage, existingFilePath);
|
||||||
|
await deleteObject(existingFileRef).catch((error) => {
|
||||||
|
(`Error deleting existing file: ${{error}}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 30 * 1024) {
|
||||||
|
// If the file is too large, compress the image
|
||||||
|
this.notifierService.showInformation('File size exceeds 30 KB. Compressing the image...')
|
||||||
|
file = await this.compressImage(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
//creates a firebase storage for images
|
||||||
|
const filePath = `images/${userId}/${file.name}`;
|
||||||
|
const storageRef = ref(this.storage, filePath);
|
||||||
|
|
||||||
|
//uploads the image to the storage
|
||||||
|
const uploadTask = uploadBytesResumable(storageRef, file, metadata);
|
||||||
|
uploadTask.on(
|
||||||
|
'state_changed',
|
||||||
|
//show the upload process in the console
|
||||||
|
(snapshot) => {
|
||||||
|
switch (snapshot.state) {
|
||||||
|
case 'error':
|
||||||
|
this.notifierService.showInformation('Upload failed')
|
||||||
|
break;
|
||||||
|
case 'success':
|
||||||
|
this.notifierService.showInformation('Upload completed')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//error handling, display currently only in the console
|
||||||
|
(error) => {
|
||||||
|
switch (error.code) {
|
||||||
|
case 'storage/unauthorized':
|
||||||
|
this.notifierService.showInformation('The user is not authenticated. Please authenticate yourself and try again.');
|
||||||
|
break;
|
||||||
|
case 'storage/canceled':
|
||||||
|
this.notifierService.showInformation('The user has cancelled the process.');
|
||||||
|
break;
|
||||||
|
case 'storage/unknown':
|
||||||
|
this.notifierService.showInformation('An unknown error has occurred.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
//returns the download url of the uploaded image
|
||||||
|
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
|
||||||
|
const fileMetadata = {
|
||||||
|
id: userId,
|
||||||
|
filePath: filePath,
|
||||||
|
downloadURL: downloadURL,
|
||||||
|
};
|
||||||
|
// Set the file metadata in the 'files' collection
|
||||||
|
setDoc(fileDocRef, fileMetadata).catch((error) => {
|
||||||
|
this.notifierService.showInformation(`Error updating file${{error}}`);
|
||||||
|
});
|
||||||
|
// Update the user's document with the new photoUrl
|
||||||
|
updateDoc(userDocRef, { photoUrl: downloadURL })
|
||||||
|
.catch((error) => {
|
||||||
|
this.notifierService.showInformation(`Error updating user document with photo URL: ${{error}}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(userId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Get the current user's file metadata from Firestore
|
||||||
|
const userDocRef = doc(this.firestore, 'users', userId);
|
||||||
|
const fileDocRef = doc(this.firestore, 'files', userId);
|
||||||
|
const fileDoc = await getDoc(fileDocRef);
|
||||||
|
if (fileDoc.exists()) {
|
||||||
|
// If there's an existing file, delete it from storage
|
||||||
|
const existingFilePath = fileDoc.data()['filePath'];
|
||||||
|
const existingFileRef = ref(this.storage, existingFilePath);
|
||||||
|
await deleteObject(existingFileRef);
|
||||||
|
await deleteDoc(fileDocRef);
|
||||||
|
updateDoc(userDocRef, { photoUrl: null })
|
||||||
|
.catch((error) => {
|
||||||
|
this.notifierService.showInformation(`Error removing photo URL from user document: ${{error}}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.notifierService.showInformation('No file metadata found to delete');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.notifierService.showInformation(`Error deleting file: ${{error}}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to compress an image
|
||||||
|
private compressImage(file: File): Promise<File> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.src = URL.createObjectURL(file);
|
||||||
|
image.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error('Failed to get canvas context'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const maxWidth = 1024; // Set the maximum width you want for the images
|
||||||
|
const maxHeight = 1024; // Set the maximum height you want for the images
|
||||||
|
let width = image.width;
|
||||||
|
let height = image.height;
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
if (width > maxWidth) {
|
||||||
|
height *= maxWidth / width;
|
||||||
|
width = maxWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (height > maxHeight) {
|
||||||
|
width *= maxHeight / height;
|
||||||
|
height = maxHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
ctx.drawImage(image, 0, 0, width, height);
|
||||||
|
|
||||||
|
// Function to attempt to compress the image
|
||||||
|
const attemptCompression = (quality: number, resolve: (file: File) => void, reject: (reason: Error) => void) => {
|
||||||
|
canvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
|
if (blob === null) {
|
||||||
|
reject(new Error('Failed to create blob from canvas'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (blob.size <= 30 * 1024) {
|
||||||
|
// If the blob size is less than or equal to 100 KB, resolve the promise
|
||||||
|
const compressedFile = new File([blob], file.name, {
|
||||||
|
type: 'image/jpeg',
|
||||||
|
lastModified: Date.now(),
|
||||||
|
});
|
||||||
|
resolve(compressedFile);
|
||||||
|
} else if (quality > 0.1) {
|
||||||
|
// If the quality can still be reduced, try again with lower quality
|
||||||
|
attemptCompression(quality - 0.1, resolve, reject);
|
||||||
|
} else {
|
||||||
|
// If the quality is already too low, reject the promise
|
||||||
|
reject(new Error('Cannot compress image to the desired size'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'image/jpeg',
|
||||||
|
quality
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the compression attempt with an initial quality value
|
||||||
|
attemptCompression(0.5, resolve, reject);
|
||||||
|
};
|
||||||
|
|
||||||
|
image.onerror = () => {
|
||||||
|
reject(new Error('Image loading failed'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
214
src/app/user-profile/user-profile.component.css
Normal file
214
src/app/user-profile/user-profile.component.css
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
.flex-user {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh; /* Full viewport height */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex3 {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
mat-card {
|
||||||
|
padding: 2.5rem;
|
||||||
|
width: min-content;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Fields */
|
||||||
|
mat-form-field {
|
||||||
|
width: 17.8rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Footer */
|
||||||
|
mat-card-footer {
|
||||||
|
color: #0596ff;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
#logOutButton {
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
width: 15.9375rem;
|
||||||
|
height: 3.75rem;
|
||||||
|
background-color: #002d3c;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 6.25rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button-Disabled */
|
||||||
|
button[disabled] {
|
||||||
|
background-color: #cccccc;
|
||||||
|
color: #666666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep .mat-mdc-text-field-wrapper {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep
|
||||||
|
.mdc-text-field--outlined.mdc-text-field--disabled
|
||||||
|
.mdc-text-field__input {
|
||||||
|
color: #000010 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep
|
||||||
|
.mat-mdc-text-field-wrapper.mdc-text-field--outlined
|
||||||
|
.mdc-notched-outline--upgraded
|
||||||
|
.mdc-floating-label--float-above {
|
||||||
|
color: #949494 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spoiler text for the Change Password toggle */
|
||||||
|
.change-password-toggle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center; /* Center the text horizontally */
|
||||||
|
margin-top: 1rem; /* Add space above the text */
|
||||||
|
margin-bottom: 0.5rem; /* Reduce space below the text for closer proximity to fields */
|
||||||
|
cursor: pointer; /* Change cursor to indicate clickable text */
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-password-toggle p {
|
||||||
|
color: #002d3c; /* Set the text color */
|
||||||
|
text-decoration: underline; /* Underline the text to indicate it's clickable */
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-password-toggle p:hover {
|
||||||
|
color: #0596ff; /* Change text color on hover for visual feedback */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container for the Change Password form fields */
|
||||||
|
.change-password-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Stack the form fields vertically */
|
||||||
|
gap: 1.5rem; /* Space between each form field */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for each form field */
|
||||||
|
.change-password-fields mat-form-field {
|
||||||
|
margin-bottom: 1.5rem; /* Add space below each form field */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the Update Password button */
|
||||||
|
.update-password-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center; /* Center the button horizontally */
|
||||||
|
margin-top: 0.5rem; /* Space above the button, consistent with form field spacing */
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-password-button button {
|
||||||
|
padding: 0.5rem 1rem; /* Increase padding for more space around the text */
|
||||||
|
font-size: 0.75rem; /* Smaller font size for the button text */
|
||||||
|
color: #333; /* Change text color for the button */
|
||||||
|
background-color: transparent; /* No background color for the button */
|
||||||
|
border: 1px solid #002d3c; /* Add a border with the desired color */
|
||||||
|
border-radius: 0.25rem; /* Slight rounding of corners */
|
||||||
|
cursor: pointer; /* Change cursor to indicate clickable button */
|
||||||
|
text-transform: uppercase; /* Uppercase button text */
|
||||||
|
letter-spacing: 0.05rem; /* Add some letter spacing */
|
||||||
|
transition: all 0.3s; /* Transition for color and border color change */
|
||||||
|
box-shadow: none; /* Remove the shadow for a flat style */
|
||||||
|
font-weight: bold; /* Make the text bold */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override padding for suffix icon in LTR direction */
|
||||||
|
:host ::ng-deep .mat-mdc-form-field-icon-suffix {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override padding for prefix icon in RTL direction */
|
||||||
|
:host ::ng-deep [dir="rtl"] .mat-mdc-form-field-icon-prefix {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userProfileDiv {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
max-width: 17.8rem;
|
||||||
|
max-height: 17.8rem;
|
||||||
|
border-radius: 10%;
|
||||||
|
border: 1px solid #0011;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userProfileImg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon {
|
||||||
|
margin-top: 13px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-section {
|
||||||
|
margin-top: 13px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-section-center {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.non-editable-field ::ng-deep .mat-mdc-text-field-wrapper {
|
||||||
|
background: transparent; /* Setzt die Farbe des Labels auf inaktiv */
|
||||||
|
}
|
||||||
|
|
||||||
|
.non-editable-field input[readonly] {
|
||||||
|
color: #000000; /*rgba(0, 0, 0, 0.4); Graue Textfarbe für nicht bearbeitbare Eingabe */
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-container {
|
||||||
|
margin: 0 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.center-container {
|
||||||
|
margin: 0 5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-background-color {
|
||||||
|
background: #f6f9fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-field {
|
||||||
|
width: 13.8rem;
|
||||||
|
}
|
||||||
176
src/app/user-profile/user-profile.component.html
Normal file
176
src/app/user-profile/user-profile.component.html
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<app-header></app-header>
|
||||||
|
<!-- Flex container -->
|
||||||
|
<div class="flex" *ngIf="dataLoaded && showProfile">
|
||||||
|
<!-- Centered container -->
|
||||||
|
<div class="center-container">
|
||||||
|
<!-- Material card -->
|
||||||
|
<div class="user-info">
|
||||||
|
<mat-card class="default-background-color">
|
||||||
|
<!-- E-Mail verification warn card -->
|
||||||
|
<app-verify-email-warn-card
|
||||||
|
*ngIf="isProfileOwner"
|
||||||
|
></app-verify-email-warn-card>
|
||||||
|
|
||||||
|
<!-- Profile photo based on user -->
|
||||||
|
<div class="m-auto">
|
||||||
|
<div
|
||||||
|
*ngIf="
|
||||||
|
userProfilePhoto && userProfilePhoto != '';
|
||||||
|
else defaultAvatar
|
||||||
|
"
|
||||||
|
class="m-auto userProfileDiv"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="userProfileImg"
|
||||||
|
src="{{ userProfilePhoto }}"
|
||||||
|
alt="User Foto"
|
||||||
|
/>
|
||||||
|
<!-- Include the profile photo upload component here -->
|
||||||
|
<app-profile-photo-upload
|
||||||
|
*ngIf="isProfileOwner"
|
||||||
|
></app-profile-photo-upload>
|
||||||
|
</div>
|
||||||
|
<!-- Conditional SVG based on user's email domain -->
|
||||||
|
<ng-template #defaultAvatar>
|
||||||
|
<div class="userProfileDiv" style="margin-bottom: 40px">
|
||||||
|
<ng-container *ngIf="isCompanyAtos(); else otherAvatar">
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox="0 0 295 295"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="147.5"
|
||||||
|
cy="122.917"
|
||||||
|
r="36.875"
|
||||||
|
stroke="#0596FF"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M221.25 229.927C216.9 216.859 207.314 205.311 193.98 197.075C180.646 188.839 164.308 184.375 147.5 184.375C130.692 184.375 114.354 188.839 101.02 197.075C87.6857 205.311 78.1001 216.859 73.75 229.927"
|
||||||
|
stroke="#0596FF"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #otherAvatar>
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox="0 0 295 295"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="147.5"
|
||||||
|
cy="122.917"
|
||||||
|
r="36.875"
|
||||||
|
stroke="#FF6D43"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M221.25 229.927C216.9 216.859 207.314 205.311 193.98 197.075C180.646 188.839 164.308 184.375 147.5 184.375C130.692 184.375 114.354 188.839 101.02 197.075C87.6857 205.311 78.1001 216.859 73.75 229.927"
|
||||||
|
stroke="#FF6D43"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</ng-template>
|
||||||
|
<!-- Include the profile photo upload component here as well -->
|
||||||
|
<app-profile-photo-upload
|
||||||
|
*ngIf="isProfileOwner"
|
||||||
|
></app-profile-photo-upload>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
<!-- Conditional SVG based on user's email domain -->
|
||||||
|
|
||||||
|
<!-- Card content -->
|
||||||
|
<mat-card-content>
|
||||||
|
<form [formGroup]="userProfileForm" (ngSubmit)="saveProfile()">
|
||||||
|
<!-- Name input field -->
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="non-editable-field">
|
||||||
|
<mat-label>Name</mat-label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
matInput
|
||||||
|
[readonly]="true"
|
||||||
|
formControlName="name"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email input field -->
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="non-editable-field">
|
||||||
|
<mat-label>Email</mat-label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
matInput
|
||||||
|
[readonly]="true"
|
||||||
|
formControlName="email"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex3"
|
||||||
|
*ngIf="isProfileOwner || userProfileForm.value.firmenposition"
|
||||||
|
>
|
||||||
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="{{
|
||||||
|
isProfileOwner ? 'editable-field' : 'non-editable-field'
|
||||||
|
}}"
|
||||||
|
>
|
||||||
|
<mat-label>Firmenposition</mat-label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
matInput
|
||||||
|
[readonly]="!isProfileOwner"
|
||||||
|
formControlName="firmenposition"
|
||||||
|
placeholder="z. B. Developer"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
<!-- Save button -->
|
||||||
|
<mat-icon
|
||||||
|
class="sync-icon"
|
||||||
|
*ngIf="isProfileOwner && !showSpinnerModule"
|
||||||
|
(click)="saveProfile()"
|
||||||
|
>sync</mat-icon
|
||||||
|
>
|
||||||
|
<app-progress-spinner
|
||||||
|
class="spinner-section"
|
||||||
|
[showSpinner]="showSpinnerModule"
|
||||||
|
></app-progress-spinner>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User wins -->
|
||||||
|
<div class="flex3">
|
||||||
|
<h2>Siege:</h2>
|
||||||
|
<!-- Display user wins if available, otherwise show loading or blank -->
|
||||||
|
<h1 [style.color]="isCompanyAtos() ? '#0596ff' : '#ff6d43'">
|
||||||
|
{{
|
||||||
|
userProfileForm.controls["wins"].value
|
||||||
|
? userProfileForm.controls["wins"].value
|
||||||
|
: 0
|
||||||
|
}}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<!-- ... other parts of your form ... -->
|
||||||
|
</form>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-progress-spinner
|
||||||
|
class="spinner-section-center"
|
||||||
|
[showSpinner]="!dataLoaded"
|
||||||
|
></app-progress-spinner>
|
||||||
393
src/app/user-profile/user-profile.component.ts
Normal file
393
src/app/user-profile/user-profile.component.ts
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import {
|
||||||
|
AbstractControl,
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
Validators,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { AngularMaterialModule } from '../angular-material/angular-material.module';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { Router, RouterModule, ActivatedRoute } from '@angular/router';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||||
|
import { AuthService } from '../auth/authService/auth.service';
|
||||||
|
import { Subscription, Subject } from 'rxjs';
|
||||||
|
import { take, takeUntil } from 'rxjs/operators';
|
||||||
|
import { User } from '../auth/model/user.model';
|
||||||
|
import { ScoreTableService } from '../score-table/score-table.service';
|
||||||
|
import { PendingActionsService } from '../score/pending-actions.service';
|
||||||
|
import { NotifierService } from '../shared/notifierService/notifier.service';
|
||||||
|
import { ProfilePhotoUploadComponent } from './profile-photo/profile-photo-upload/profile-photo-upload.component';
|
||||||
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
|
import { ProgressSpinnerComponent } from '../shared/progress-spinner/progress-spinner/progress-spinner.component';
|
||||||
|
import { HeaderComponent } from '../shared/header/header.component';
|
||||||
|
import {
|
||||||
|
PasswordValidationType,
|
||||||
|
passwordValidator,
|
||||||
|
} from '../shared/validation/passwordMatcherValidation';
|
||||||
|
import {
|
||||||
|
hasNumberValidator,
|
||||||
|
hasUpperCaseValidator,
|
||||||
|
} from '../shared/validation/passwordSaferValidation';
|
||||||
|
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||||
|
import { VerifiyWarnCardComponent } from './verify-email-warn-card/verify-email-warn-card.component';
|
||||||
|
|
||||||
|
@UntilDestroy()
|
||||||
|
@Component({
|
||||||
|
selector: 'app-test',
|
||||||
|
standalone: true,
|
||||||
|
providers: [UserService, AuthService],
|
||||||
|
templateUrl: './user-profile.component.html',
|
||||||
|
styleUrl: './user-profile.component.css',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
AngularMaterialModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule,
|
||||||
|
HttpClientModule,
|
||||||
|
ProfilePhotoUploadComponent,
|
||||||
|
MatDividerModule,
|
||||||
|
ProgressSpinnerComponent,
|
||||||
|
HeaderComponent,
|
||||||
|
MatProgressSpinner,
|
||||||
|
VerifiyWarnCardComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class UserProfileComponent implements OnInit, OnDestroy {
|
||||||
|
userProfileForm!: FormGroup; // Form group to manage user profile data
|
||||||
|
userProfilePhoto: string | undefined | null;
|
||||||
|
userProfileSubscription!: Subscription; // Subscription to user profile changes
|
||||||
|
user$ = this.userService.currentUserProfile$; // Observable of current user profile data
|
||||||
|
dataLoaded: boolean = false;
|
||||||
|
showProfile: boolean = true;
|
||||||
|
hide: boolean = true;
|
||||||
|
showSpinnerModule: boolean = false;
|
||||||
|
isProfileOwner: boolean = false;
|
||||||
|
isUpdatingPassword: boolean = false;
|
||||||
|
private passwordValueChangesSubscription?: Subscription;
|
||||||
|
private unsubscribe$ = new Subject<void>();
|
||||||
|
showChangePassword: boolean = false;
|
||||||
|
passwordForm: FormGroup;
|
||||||
|
errorMessageCurrentPassword = '';
|
||||||
|
errorMessageNewPassword = '';
|
||||||
|
errorMessageConfirmPassword = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userService: UserService,
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private scoreTableService: ScoreTableService,
|
||||||
|
private pendingActionsService: PendingActionsService,
|
||||||
|
private notifierService: NotifierService
|
||||||
|
) {
|
||||||
|
this.dataLoaded = false;
|
||||||
|
// Initialize the password form with form controls and validators from the new branch
|
||||||
|
this.passwordForm = new FormGroup({
|
||||||
|
currentPassword: new FormControl('', [Validators.required]),
|
||||||
|
newPassword: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.minLength(10),
|
||||||
|
Validators.maxLength(20),
|
||||||
|
hasUpperCaseValidator(),
|
||||||
|
hasNumberValidator(),
|
||||||
|
]),
|
||||||
|
confirmNewPassword: new FormControl('', [Validators.required]),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply the non-matching validator to the newPassword control
|
||||||
|
const newPasswordControl = this.passwordForm.get('newPassword');
|
||||||
|
const currentPasswordControl = this.passwordForm.get('currentPassword');
|
||||||
|
const confirmNewPasswordControl =
|
||||||
|
this.passwordForm.get('confirmNewPassword');
|
||||||
|
|
||||||
|
if (
|
||||||
|
newPasswordControl &&
|
||||||
|
currentPasswordControl &&
|
||||||
|
confirmNewPasswordControl
|
||||||
|
) {
|
||||||
|
newPasswordControl.setValidators([
|
||||||
|
Validators.required,
|
||||||
|
Validators.minLength(10),
|
||||||
|
Validators.maxLength(20),
|
||||||
|
passwordValidator('currentPassword', PasswordValidationType.Different),
|
||||||
|
hasUpperCaseValidator(),
|
||||||
|
hasNumberValidator(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Apply the match validator to the confirmNewPassword control
|
||||||
|
confirmNewPasswordControl.setValidators([
|
||||||
|
Validators.required,
|
||||||
|
passwordValidator('newPassword', PasswordValidationType.Match),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set up a subscription to the currentPassword field's valueChanges observable
|
||||||
|
newPasswordControl.valueChanges.subscribe(() => {
|
||||||
|
this.updateErrorMessage();
|
||||||
|
confirmNewPasswordControl.updateValueAndValidity();
|
||||||
|
});
|
||||||
|
|
||||||
|
confirmNewPasswordControl.valueChanges.subscribe(() => {
|
||||||
|
this.updateErrorMessage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Subscribe to query parameters
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(untilDestroyed(this))
|
||||||
|
.subscribe(async (params) => {
|
||||||
|
const score = params['score'];
|
||||||
|
const uid = params['uid'];
|
||||||
|
if (this.isUserVerified() && score && uid) {
|
||||||
|
this.showProfile = false;
|
||||||
|
this.scoreTableService.validateAndAddGame(score, uid);
|
||||||
|
this.scoreTableService.setShowUserGames(true);
|
||||||
|
} else if (score && uid) {
|
||||||
|
this.showProfile = false;
|
||||||
|
const uniqueId = this.pendingActionsService.saveTempQRCodeData({
|
||||||
|
score,
|
||||||
|
uid,
|
||||||
|
});
|
||||||
|
if (this.authService.getCurrentUser())
|
||||||
|
this.pendingActionsService.addUserRefToQRCodeData(
|
||||||
|
this.authService.getCurrentUser()?.uid!,
|
||||||
|
uniqueId
|
||||||
|
);
|
||||||
|
this.router.navigate(['/score-table']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribing to the user profile changes to update component properties
|
||||||
|
this.userProfileSubscription = this.user$.subscribe((userProfile) => {
|
||||||
|
this.userProfilePhoto = userProfile?.photoUrl;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initializing the user profile form
|
||||||
|
this.initForm();
|
||||||
|
|
||||||
|
// Subscribe to route params
|
||||||
|
this.route.params.pipe(untilDestroyed(this)).subscribe((params) => {
|
||||||
|
this.initUserProfile(params['id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to save the updated profile data
|
||||||
|
saveProfile() {
|
||||||
|
this.showSpinnerModule = true;
|
||||||
|
this.authService.currentUser$.pipe(take(1)).subscribe((user) => {
|
||||||
|
if (user) {
|
||||||
|
// Check that the user is not null
|
||||||
|
const profileData = this.userProfileForm.value;
|
||||||
|
const updatedUser: User = {
|
||||||
|
uid: user.uid,
|
||||||
|
firmenposition: profileData.firmenposition,
|
||||||
|
// Include other fields as needed
|
||||||
|
};
|
||||||
|
this.userService.updateUser(updatedUser).subscribe();
|
||||||
|
setTimeout(() => (this.showSpinnerModule = false), 1000);
|
||||||
|
this.notifierService.showNotification(
|
||||||
|
'Firmenposition erfolgreich aktualisiert.',
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle logout
|
||||||
|
onLogout() {
|
||||||
|
// Dismiss any open notifications
|
||||||
|
this.notifierService.dismissNotification();
|
||||||
|
this.authService.logout().subscribe(() => {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
});
|
||||||
|
this.unsubscribe$.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// Unsubscribing from subscriptions to prevent memory leaks
|
||||||
|
if (this.userProfileSubscription) {
|
||||||
|
this.userProfileSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
if (this.passwordValueChangesSubscription) {
|
||||||
|
this.passwordValueChangesSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
this.unsubscribe$.next();
|
||||||
|
this.unsubscribe$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update the user's password
|
||||||
|
updatePassword() {
|
||||||
|
if (!this.passwordForm.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentUser = this.authService.getCurrentUser();
|
||||||
|
if (!currentUser) {
|
||||||
|
this.notifierService.showNotification(
|
||||||
|
'Kein Benutzer angemeldet. Bitte melde dich an.',
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isUpdatingPassword = true;
|
||||||
|
const currentPassword = this.passwordForm.value.currentPassword;
|
||||||
|
const newPassword = this.passwordForm.value.newPassword;
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.updatePassword(currentPassword, newPassword)
|
||||||
|
.pipe(takeUntil(this.unsubscribe$))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.notifierService.showNotification(
|
||||||
|
'Das Passwort wurde geändert.',
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
this.passwordForm.reset();
|
||||||
|
this.showChangePassword = false;
|
||||||
|
this.isUpdatingPassword = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.isUpdatingPassword = false;
|
||||||
|
if (error.code === 'auth/invalid-credential') {
|
||||||
|
this.notifierService.showNotification(
|
||||||
|
'Das aktuelle Passwort ist falsch!',
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('Error updating password:', error);
|
||||||
|
this.notifierService.showNotification(
|
||||||
|
'Bitte versuche es in 5 Min nochmal!',
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isUserVerified() {
|
||||||
|
return this.authService.getCurrentUser()?.emailVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
initForm() {
|
||||||
|
this.userProfileForm = new FormGroup({
|
||||||
|
uid: new FormControl(null),
|
||||||
|
name: new FormControl(''),
|
||||||
|
email: new FormControl(''),
|
||||||
|
firmenposition: new FormControl(''),
|
||||||
|
wins: new FormControl(''),
|
||||||
|
//New Password validation
|
||||||
|
currentPassword: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.minLength(10),
|
||||||
|
]),
|
||||||
|
newPassword: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
Validators.minLength(10),
|
||||||
|
Validators.maxLength(20),
|
||||||
|
]),
|
||||||
|
confirmNewPassword: new FormControl('', [
|
||||||
|
Validators.required,
|
||||||
|
passwordValidator('newPassword', PasswordValidationType.Match),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateForm(user: User | null) {
|
||||||
|
if (user) {
|
||||||
|
this.userProfileForm.patchValue({
|
||||||
|
name: user?.firstName + ' ' + user?.lastName,
|
||||||
|
...user,
|
||||||
|
});
|
||||||
|
this.setProfilePhoto(user.uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initUserProfile(uid: string) {
|
||||||
|
this.user$.pipe(untilDestroyed(this)).subscribe(async (user) => {
|
||||||
|
this.isProfileOwner = uid ? uid === user?.uid : true;
|
||||||
|
if (this.isProfileOwner) {
|
||||||
|
this.updateForm(user);
|
||||||
|
} else {
|
||||||
|
this.userService
|
||||||
|
.getUserProfile(uid)
|
||||||
|
.pipe(untilDestroyed(this))
|
||||||
|
.subscribe(async (user) => {
|
||||||
|
this.updateForm(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setProfilePhoto(uid: string) {
|
||||||
|
this.userService
|
||||||
|
.getPhotoUrl(uid)
|
||||||
|
.then((downloadURL) => {
|
||||||
|
this.userProfilePhoto = downloadURL;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.dataLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isCompanyAtos() {
|
||||||
|
const email = this.userProfileForm.controls['email'].value;
|
||||||
|
return email.includes('atos.net');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateErrorMessage() {
|
||||||
|
const currentPasswordControl = this.userProfileForm.get('currentPassword');
|
||||||
|
const newPasswordControl = this.passwordForm.get('newPassword');
|
||||||
|
const confirmNewPasswordControl =
|
||||||
|
this.passwordForm.get('confirmNewPassword');
|
||||||
|
|
||||||
|
this.errorMessageCurrentPassword = currentPasswordControl?.hasError(
|
||||||
|
'required'
|
||||||
|
)
|
||||||
|
? 'Aktuelles Passwort eingeben.'
|
||||||
|
: '';
|
||||||
|
this.errorMessageNewPassword =
|
||||||
|
this.getNewPasswordErrorMessage(newPasswordControl);
|
||||||
|
this.errorMessageConfirmPassword = this.getConfirmPasswordErrorMessage(
|
||||||
|
confirmNewPasswordControl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNewPasswordErrorMessage(control: AbstractControl | null): string {
|
||||||
|
if (control?.hasError('required')) {
|
||||||
|
return 'Neues Passwort eingeben.';
|
||||||
|
} else if (control?.hasError('minlength')) {
|
||||||
|
return 'Das Passwort muss mindestens 10 Zeichen lang sein.';
|
||||||
|
} else if (control?.hasError('maxlength')) {
|
||||||
|
return 'Das Passwort darf maximal 20 Zeichen lang sein.';
|
||||||
|
} else if (control?.hasError('passwordsSame')) {
|
||||||
|
return 'Das neue Passwort muss sich vom aktuellen Passwort unterscheiden.';
|
||||||
|
} else if (
|
||||||
|
control?.hasError('hasNumber') &&
|
||||||
|
control?.hasError('hasUpperCase')
|
||||||
|
) {
|
||||||
|
return 'Mind. 1 Großbuchstaben und 1 Zahl.';
|
||||||
|
} else if (control?.hasError('hasUpperCase')) {
|
||||||
|
return 'Mind. 1 Großbuchstaben.';
|
||||||
|
} else if (control?.hasError('hasNumber')) {
|
||||||
|
return 'Mind. 1 Zahl.';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getConfirmPasswordErrorMessage(
|
||||||
|
control: AbstractControl | null
|
||||||
|
): string {
|
||||||
|
if (control?.hasError('required')) {
|
||||||
|
return 'Passwortbestätigung eingeben.';
|
||||||
|
} else if (control?.hasError('passwordMismatch')) {
|
||||||
|
return 'Die eingegebenen Passwörter stimmen nicht überein.';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
196
src/app/user-profile/user.service.ts
Normal file
196
src/app/user-profile/user.service.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
Firestore,
|
||||||
|
doc,
|
||||||
|
docData,
|
||||||
|
setDoc,
|
||||||
|
updateDoc,
|
||||||
|
collection,
|
||||||
|
query,
|
||||||
|
where,
|
||||||
|
getDocs,
|
||||||
|
} from '@angular/fire/firestore';
|
||||||
|
import { User } from '../auth/model/user.model';
|
||||||
|
import { Observable, from, of, switchMap } from 'rxjs';
|
||||||
|
import { AuthService } from '../auth/authService/auth.service';
|
||||||
|
import {
|
||||||
|
EmailAuthProvider,
|
||||||
|
reauthenticateWithCredential,
|
||||||
|
updatePassword,
|
||||||
|
} from '@angular/fire/auth';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class UserService {
|
||||||
|
/**
|
||||||
|
* Retrieves the current user's profile.
|
||||||
|
* @returns An observable emitting the current user's profile.
|
||||||
|
*/
|
||||||
|
get currentUserProfile$(): Observable<User | null> {
|
||||||
|
return this.authService.currentUser$.pipe(
|
||||||
|
switchMap((user) => {
|
||||||
|
if (!user?.uid) {
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
const ref = doc(this.firestore, 'users', user?.uid);
|
||||||
|
return docData(ref) as Observable<User>;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new UserService.
|
||||||
|
* @param firestore - The Angular Firestore service instance.
|
||||||
|
* @param authService - The authentication service instance.
|
||||||
|
* @param httpClient - The Angular HttpClient service instance.
|
||||||
|
*/
|
||||||
|
constructor(private firestore: Firestore, private authService: AuthService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new user to the Firestore database.
|
||||||
|
* @param user - The user object to be added.
|
||||||
|
* @returns An observable indicating the success or failure of the operation.
|
||||||
|
*/
|
||||||
|
addUser(user: User): Observable<any> {
|
||||||
|
// Ensure that the user.uid is not undefined or an empty string
|
||||||
|
if (!user.uid) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid UID: UID is required to create a user document.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const ref = doc(this.firestore, 'users', user?.uid);
|
||||||
|
return from(setDoc(ref, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an existing user in the Firestore database.
|
||||||
|
* @param user - The updated user object.
|
||||||
|
* @returns An observable indicating the success or failure of the operation.
|
||||||
|
*/
|
||||||
|
updateUser(user: User): Observable<any> {
|
||||||
|
const ref = doc(this.firestore, 'users', user?.uid);
|
||||||
|
return from(updateDoc(ref, { ...user }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserService method to count the user's games and update the wins field
|
||||||
|
updateUserWins(userId: string): Observable<any> {
|
||||||
|
const userRef = doc(this.firestore, 'users', userId);
|
||||||
|
const gamesRef = collection(this.firestore, 'games');
|
||||||
|
const q = query(gamesRef, where('userRef', '==', userRef));
|
||||||
|
|
||||||
|
return from(getDocs(q)).pipe(
|
||||||
|
switchMap((querySnapshot) => {
|
||||||
|
// The size property indicates the number of documents in the QuerySnapshot
|
||||||
|
const wins = querySnapshot.size;
|
||||||
|
// Update the wins field in the user document
|
||||||
|
return from(updateDoc(userRef, { wins: wins }));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePassword(
|
||||||
|
currentPassword: string,
|
||||||
|
newPassword: string
|
||||||
|
): Observable<any> {
|
||||||
|
return this.authService.currentUser$.pipe(
|
||||||
|
switchMap((user) => {
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('No user signed in');
|
||||||
|
}
|
||||||
|
// Get the credential from the current password
|
||||||
|
return from(this.getCredentialFromPassword(currentPassword)).pipe(
|
||||||
|
switchMap((credential) => {
|
||||||
|
// Reauthenticate user with their current password
|
||||||
|
return from(reauthenticateWithCredential(user, credential)).pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
// Re-authentication successful, proceed to update password
|
||||||
|
return from(updatePassword(user, newPassword));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCredentialFromPassword(currentPassword: string): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.getUserEmail().subscribe(
|
||||||
|
(email: string | null) => {
|
||||||
|
if (!email) {
|
||||||
|
reject(new Error('User email is not available'));
|
||||||
|
} else {
|
||||||
|
const credential = EmailAuthProvider.credential(
|
||||||
|
email,
|
||||||
|
currentPassword
|
||||||
|
);
|
||||||
|
resolve(credential);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserEmail(): Observable<string | null> {
|
||||||
|
return this.authService.currentUser$.pipe(
|
||||||
|
switchMap((user) => {
|
||||||
|
if (!user?.uid) {
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
const ref = doc(this.firestore, 'users', user.uid);
|
||||||
|
return docData(ref).pipe(
|
||||||
|
switchMap((userData: any) => {
|
||||||
|
// Assuming 'email' is a field in your Firestore document
|
||||||
|
const email: string | null = userData.email || null;
|
||||||
|
return of(email);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserProfile(uid: string): Observable<User | null> {
|
||||||
|
return new Observable<User>((observer) => {
|
||||||
|
const usersCollection = collection(this.firestore, 'users');
|
||||||
|
const q = query(usersCollection, where('uid', '==', uid));
|
||||||
|
getDocs(q)
|
||||||
|
.then((querySnapshot) => {
|
||||||
|
querySnapshot.forEach((doc) => {
|
||||||
|
const userData = doc.data() as User;
|
||||||
|
observer.next(userData);
|
||||||
|
});
|
||||||
|
observer.complete();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
observer.error(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPhotoUrl(uid: string | undefined): Promise<string | null> {
|
||||||
|
if (!uid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const filesQuery = query(
|
||||||
|
collection(this.firestore, 'files'),
|
||||||
|
where('id', '==', uid)
|
||||||
|
);
|
||||||
|
const querySnapshot = await getDocs(filesQuery);
|
||||||
|
|
||||||
|
if (!querySnapshot.empty) {
|
||||||
|
const fileDoc = querySnapshot.docs[0];
|
||||||
|
return fileDoc.data()['downloadURL'] as string;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting image file:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.warning-card {
|
||||||
|
margin: 1rem;
|
||||||
|
background-color: #fff3cd;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<mat-card *ngIf="!hideCard" class="warning-card">
|
||||||
|
<mat-card-content>
|
||||||
|
<p>
|
||||||
|
Deine E-Mail wurde noch nicht verifiziert. Bitte überprüfen dein Postfach
|
||||||
|
und klicke auf den Verifizierungslink.
|
||||||
|
</p>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-actions>
|
||||||
|
<button mat-button (click)="resendVerificationEmail()">
|
||||||
|
Neue Verifizierungs-E-Mail anfordern
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { VerifiyWarnCardComponent } from './verify-email-warn-card.component';
|
||||||
|
|
||||||
|
describe('VerifiyWarnCardComponent', () => {
|
||||||
|
let component: VerifiyWarnCardComponent;
|
||||||
|
let fixture: ComponentFixture<VerifiyWarnCardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [VerifiyWarnCardComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(VerifiyWarnCardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
|
import { AngularMaterialModule } from '../../angular-material/angular-material.module';
|
||||||
|
import { AuthService } from '../../auth/authService/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-verify-email-warn-card',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, AngularMaterialModule, MatDividerModule],
|
||||||
|
templateUrl: './verify-email-warn-card.component.html',
|
||||||
|
styleUrl: './verify-email-warn-card.component.css',
|
||||||
|
})
|
||||||
|
export class VerifiyWarnCardComponent {
|
||||||
|
hideCard = false;
|
||||||
|
|
||||||
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.hideCard = this.isUserVerified();
|
||||||
|
}
|
||||||
|
|
||||||
|
resendVerificationEmail() {
|
||||||
|
this.authService.sendVerificationEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
isUserVerified() {
|
||||||
|
return !!this.authService.getCurrentUser()?.emailVerified;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/assets/.gitkeep
Normal file
0
src/assets/.gitkeep
Normal file
BIN
src/assets/fonts/static/Montserrat-Black.ttf
Normal file
BIN
src/assets/fonts/static/Montserrat-Black.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/static/Montserrat-BlackItalic.ttf
Normal file
BIN
src/assets/fonts/static/Montserrat-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/static/Montserrat-Bold.ttf
Normal file
BIN
src/assets/fonts/static/Montserrat-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/static/Montserrat-BoldItalic.ttf
Normal file
BIN
src/assets/fonts/static/Montserrat-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/static/Montserrat-ExtraBold.ttf
Normal file
BIN
src/assets/fonts/static/Montserrat-ExtraBold.ttf
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user