first commit
This commit is contained in:
@@ -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
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"targets": {
|
||||
"iwuzzler-web-app": {
|
||||
"hosting": {
|
||||
"frontend": [
|
||||
"iwuzzler"
|
||||
]
|
||||
}
|
||||
},
|
||||
"iwuzzler-develop-16bee": {
|
||||
"hosting": {
|
||||
"frontend": [
|
||||
"iwuzzler-development"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"projects": {
|
||||
"default": "iwuzzler-web-app"
|
||||
}
|
||||
}
|
||||
+47
@@ -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
|
||||
@@ -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
|
||||
Vendored
+22
@@ -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}"
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"hosting": [
|
||||
{
|
||||
"site": "iwuzzler-development",
|
||||
"public": "dist/frontend/browser",
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"hosting": [
|
||||
{
|
||||
"site": "iwuzzler",
|
||||
"public": "dist/frontend/browser",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
Generated
+14791
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
};
|
||||
@@ -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' },
|
||||
];
|
||||
@@ -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']);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.dialog-container {
|
||||
max-width: 500px;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
}
|
||||
+30
@@ -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;
|
||||
}
|
||||
+13
@@ -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>
|
||||
|
||||
+23
@@ -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();
|
||||
});
|
||||
});
|
||||
+88
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
+198
@@ -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'));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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