refactor: Refactor SharedService, extract logic into smaller services (ResizeService and ColorService)

This commit is contained in:
Chneemann 2025-03-30 10:43:36 +02:00
parent a00690ffae
commit 8659c16517
11 changed files with 92 additions and 139 deletions

View file

@ -227,7 +227,7 @@ export class AddTaskComponent implements OnInit {
* @returns The updated search input flag. * @returns The updated search input flag.
*/ */
updateSearchInput() { updateSearchInput() {
this.searchValue = this.sharedService.replaceXSSChars(this.searchValue); this.searchValue = this.replaceXSSChars(this.searchValue);
if (this.searchValue) { if (this.searchValue) {
this.searchInput = this.searchValue.toLowerCase().length > 0; this.searchInput = this.searchValue.toLowerCase().length > 0;
} else { } else {
@ -241,7 +241,7 @@ export class AddTaskComponent implements OnInit {
* This ensures that the subtask value is sanitized before further processing or storage. * This ensures that the subtask value is sanitized before further processing or storage.
*/ */
updateSubtaskValue() { updateSubtaskValue() {
this.subtaskValue = this.sharedService.replaceXSSChars(this.subtaskValue); this.subtaskValue = this.replaceXSSChars(this.subtaskValue);
} }
/** /**
@ -400,14 +400,10 @@ export class AddTaskComponent implements OnInit {
* @returns {void} * @returns {void}
*/ */
checkCrossSiteScripting() { checkCrossSiteScripting() {
this.taskData.title = this.sharedService.replaceXSSChars( this.taskData.title = this.replaceXSSChars(this.taskData.title);
this.taskData.title this.taskData.description = this.replaceXSSChars(this.taskData.description);
);
this.taskData.description = this.sharedService.replaceXSSChars(
this.taskData.description
);
this.taskData.subtasksTitle = this.taskData.subtasksTitle.map((title) => this.taskData.subtasksTitle = this.taskData.subtasksTitle.map((title) =>
this.sharedService.replaceXSSChars(title) this.replaceXSSChars(title)
); );
} }
@ -421,6 +417,10 @@ export class AddTaskComponent implements OnInit {
this.closeDialog(); this.closeDialog();
} }
replaceXSSChars(input: string) {
return input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
@HostListener('document:click', ['$event']) @HostListener('document:click', ['$event'])
/** /**
* Closes the assigned dropdown menu if the user clicks anywhere outside of * Closes the assigned dropdown menu if the user clicks anywhere outside of

View file

@ -6,7 +6,6 @@ import { TaskEmptyComponent } from './task/task-empty/task-empty.component';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { OverlayService } from '../../services/overlay.service'; import { OverlayService } from '../../services/overlay.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { SharedService } from '../../services/shared.service';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { TaskHighlightedComponent } from './task/task-highlighted/task-highlighted.component'; import { TaskHighlightedComponent } from './task/task-highlighted/task-highlighted.component';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
@ -16,6 +15,7 @@ import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner
import { finalize, Subject, takeUntil } from 'rxjs'; import { finalize, Subject, takeUntil } from 'rxjs';
import { TaskUpdateService } from '../../services/task-update.service'; import { TaskUpdateService } from '../../services/task-update.service';
import { ToastNotificationService } from '../../services/toast-notification.servic'; import { ToastNotificationService } from '../../services/toast-notification.servic';
import { ResizeService } from '../../services/resize.service';
@Component({ @Component({
selector: 'app-board', selector: 'app-board',
@ -43,7 +43,7 @@ export class BoardComponent {
constructor( constructor(
public dragDropService: DragDropService, public dragDropService: DragDropService,
public overlayService: OverlayService, public overlayService: OverlayService,
private sharedService: SharedService, private resizeService: ResizeService,
private taskService: TaskService, private taskService: TaskService,
private apiService: ApiService, private apiService: ApiService,
private taskUpdateService: TaskUpdateService, private taskUpdateService: TaskUpdateService,
@ -129,7 +129,7 @@ export class BoardComponent {
* @param status The status to be used for the new task. * @param status The status to be used for the new task.
*/ */
addNewTaskOverlay(status: string) { addNewTaskOverlay(status: string) {
this.sharedService.isPageViewMedia this.resizeService.isPageViewMedia
? this.router.navigate(['/add-task', status]) ? this.router.navigate(['/add-task', status])
: this.overlayService.setOverlayData('newTaskOverlay', status); : this.overlayService.setOverlayData('newTaskOverlay', status);
} }

View file

@ -13,7 +13,7 @@
> >
{{ task.category }} {{ task.category }}
</div> </div>
@if (sharedService.isPageViewMedia) { @if (resizeService.isPageViewMedia) {
<div class="menu-btn" (click)="handleMenuButtonClick($event, task.id)"> <div class="menu-btn" (click)="handleMenuButtonClick($event, task.id)">
<img <img
class="menu-img" class="menu-img"

View file

@ -6,7 +6,7 @@ import { FirebaseService } from '../../../services/firebase.service';
import { OverlayService } from '../../../services/overlay.service'; import { OverlayService } from '../../../services/overlay.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TaskMenuComponent } from './task-menu/task-menu.component'; import { TaskMenuComponent } from './task-menu/task-menu.component';
import { SharedService } from '../../../services/shared.service'; import { ResizeService } from '../../../services/resize.service';
@Component({ @Component({
selector: 'app-task', selector: 'app-task',
@ -34,7 +34,7 @@ export class TaskComponent {
public dragDropService: DragDropService, public dragDropService: DragDropService,
public firebaseService: FirebaseService, public firebaseService: FirebaseService,
public overlayService: OverlayService, public overlayService: OverlayService,
public sharedService: SharedService, public resizeService: ResizeService,
private router: Router private router: Router
) {} ) {}
@ -74,7 +74,7 @@ export class TaskComponent {
if (this.isMenuOpen) { if (this.isMenuOpen) {
this.toggleTaskMenu(); this.toggleTaskMenu();
} }
this.sharedService.isPageViewMedia this.resizeService.isPageViewMedia
? this.router.navigate(['/task', taskId]) ? this.router.navigate(['/task', taskId])
: this.overlayService.setOverlayData('taskOverlay', taskId); : this.overlayService.setOverlayData('taskOverlay', taskId);
} }

View file

@ -6,6 +6,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { ContactFormComponent } from './contact-form/contact-form.component'; import { ContactFormComponent } from './contact-form/contact-form.component';
import { SharedService } from '../../../services/shared.service'; import { SharedService } from '../../../services/shared.service';
import { ColorPickerModule } from 'ngx-color-picker'; import { ColorPickerModule } from 'ngx-color-picker';
import { ColorService } from '../../../services/color.service';
@Component({ @Component({
selector: 'app-contact-edit', selector: 'app-contact-edit',
@ -30,7 +31,8 @@ export class ContactEditNewComponent implements OnInit {
constructor( constructor(
public firebaseService: FirebaseService, public firebaseService: FirebaseService,
public sharedService: SharedService public sharedService: SharedService,
private colorService: ColorService
) {} ) {}
/** /**
@ -39,7 +41,7 @@ export class ContactEditNewComponent implements OnInit {
* This method sets a random color for the user to be edited. * This method sets a random color for the user to be edited.
*/ */
ngOnInit() { ngOnInit() {
this.randomColor = this.sharedService.generateRandomColor(); this.randomColor = this.colorService.generateRandomColor();
} }
/** /**

View file

@ -23,7 +23,7 @@
autocomplete="off" autocomplete="off"
required required
/> />
@if (sharedService.isContactViewMedia) { @if (resizeService.isContactViewMedia) {
<div class="error-msg"> <div class="error-msg">
@if (firstName.hasError('pattern') && firstName.touched) { @if (firstName.hasError('pattern') && firstName.touched) {
<p>{{ "contactDialogForm.invalidPattern" | translate }}</p> <p>{{ "contactDialogForm.invalidPattern" | translate }}</p>
@ -49,7 +49,7 @@
required required
/> />
</div> </div>
@if (sharedService.isContactViewMedia) { @if (resizeService.isContactViewMedia) {
<div class="error-msg"> <div class="error-msg">
@if (lastName.hasError('pattern') && lastName.touched) { @if (lastName.hasError('pattern') && lastName.touched) {
<p>{{ "contactDialogForm.invalidPattern" | translate }}</p> <p>{{ "contactDialogForm.invalidPattern" | translate }}</p>
@ -57,7 +57,7 @@
<p>{{ "contactDialogForm.invalidLastName" | translate }}</p> <p>{{ "contactDialogForm.invalidLastName" | translate }}</p>
}} }}
</div> </div>
} @if (!sharedService.isContactViewMedia) { } @if (!resizeService.isContactViewMedia) {
<div class="error-msg"> <div class="error-msg">
@if (firstName.hasError('pattern') || lastName.hasError('pattern') && @if (firstName.hasError('pattern') || lastName.hasError('pattern') &&
firstName.touched) { firstName.touched) {

View file

@ -14,6 +14,7 @@ import { FirebaseService } from '../../../../services/firebase.service';
import { FormBtnComponent } from '../../../../shared/components/buttons/form-btn/form-btn.component'; import { FormBtnComponent } from '../../../../shared/components/buttons/form-btn/form-btn.component';
import { User } from '../../../../interfaces/user.interface'; import { User } from '../../../../interfaces/user.interface';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ResizeService } from '../../../../services/resize.service';
@Component({ @Component({
selector: 'app-contact-form', selector: 'app-contact-form',
@ -32,6 +33,7 @@ export class ContactFormComponent implements OnInit, OnChanges {
constructor( constructor(
private router: Router, private router: Router,
public firebaseService: FirebaseService, public firebaseService: FirebaseService,
public resizeService: ResizeService,
public sharedService: SharedService public sharedService: SharedService
) { ) {
this.updateContactData(); this.updateContactData();

View file

@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class ColorService {
generateRandomColor(): string {
const letters = '0123456789ABCDEF';
let color = '#';
let isGrayScale = true;
while (isGrayScale) {
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
const rgb = this.hexToRgb(color);
if (rgb.r !== rgb.g || rgb.g !== rgb.b) {
isGrayScale = false;
} else {
color = '#';
}
}
return color;
}
hexToRgb(hex: string): { r: number; g: number; b: number } {
const bigint = parseInt(hex.substring(1), 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
}
}

View file

@ -25,6 +25,7 @@ import { Router } from '@angular/router';
import CryptoES from 'crypto-es'; import CryptoES from 'crypto-es';
import { CryptoESSecretKey } from '../environments/config'; import { CryptoESSecretKey } from '../environments/config';
import { catchError, map, Observable, of } from 'rxjs'; import { catchError, map, Observable, of } from 'rxjs';
import { ColorService } from './color.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -39,6 +40,7 @@ export class LoginService {
constructor( constructor(
private firebaseService: FirebaseService, private firebaseService: FirebaseService,
private colorService: ColorService,
public sharedService: SharedService, public sharedService: SharedService,
private router: Router private router: Router
) {} ) {}
@ -124,7 +126,7 @@ export class LoginService {
? registerData.firstName.slice(0, 1).toUpperCase() + ? registerData.firstName.slice(0, 1).toUpperCase() +
registerData.lastName.slice(0, 1).toUpperCase() registerData.lastName.slice(0, 1).toUpperCase()
: '', : '',
color: this.sharedService.generateRandomColor(), color: this.colorService.generateRandomColor(),
lastLogin: new Date().getTime(), lastLogin: new Date().getTime(),
}; };
this.createUserInFirestore(userDataToSave); this.createUserInFirestore(userDataToSave);
@ -169,7 +171,7 @@ export class LoginService {
isOnline: true, isOnline: true,
phone: '', phone: '',
initials: firstName.slice(0, 1) + lastName.slice(0, 1), initials: firstName.slice(0, 1) + lastName.slice(0, 1),
color: this.sharedService.generateRandomColor(), color: this.colorService.generateRandomColor(),
lastLogin: 0, lastLogin: 0,
}); });
} else { } else {

View file

@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class ResizeService {
isPageViewMedia: boolean = window.innerWidth <= 650;
isContactViewMedia: boolean = window.innerWidth <= 800;
private resizeListener!: () => void;
constructor() {
this.initResizeListener();
}
private initResizeListener() {
this.resizeListener = this.onResize.bind(this);
window.addEventListener('resize', this.resizeListener);
}
private onResize() {
this.isPageViewMedia = window.innerWidth <= 650;
this.isContactViewMedia = window.innerWidth <= 800;
}
cleanup() {
window.removeEventListener('resize', this.resizeListener);
}
}

View file

@ -10,121 +10,5 @@ export class SharedService {
isNewContactDialogOpen: boolean = false; isNewContactDialogOpen: boolean = false;
isEditContactDialogOpen: boolean = false; isEditContactDialogOpen: boolean = false;
isDeleteContactDialogOpen: boolean = false; isDeleteContactDialogOpen: boolean = false;
isPageViewMedia: boolean = window.innerWidth <= 650;
isContactViewMedia: boolean = window.innerWidth <= 800;
currentUserId: string = ''; currentUserId: string = '';
private resizeListener!: () => void;
constructor() {
this.initResizeListener();
}
// CHECK VIEW WIDTH
/**
* Initializes the window resize listener.
*
* @remarks
* This function will set up the window resize listener and bind the
* onResize() method to it.
*/
private initResizeListener() {
this.resizeListener = this.onResize.bind(this);
window.addEventListener('resize', this.resizeListener);
}
/**
* Removes the window resize listener.
*
* @remarks
* This function will remove the window resize listener that was set up in
* the constructor. This is useful when the service is being destroyed.
*/
private removeResizeListener() {
window.removeEventListener('resize', this.resizeListener);
}
/**
* Window resize event listener method.
*
* @remarks
* This method will update the isPageViewMedia and isContactViewMedia
* properties based on the current window width whenever the window is
* resized.
*/
private onResize() {
this.isPageViewMedia = window.innerWidth <= 650;
this.isContactViewMedia = window.innerWidth <= 800;
}
/**
* Cleans up the service by removing the window resize listener.
*
* @remarks
* This function is useful when the service is being destroyed.
*/
cleanup() {
this.removeResizeListener();
}
// RANDOM COLOR
/**
* Generates a random color string.
*
* @remarks
* The generated color is a hex string that is not a shade of gray.
* The color is generated by randomly selecting a hex digit from the
* string '0123456789ABCDEF' and appending it to the color string. The
* loop continues until a non-gray color is generated.
*
* @returns A random color string.
*/
generateRandomColor(): string {
const letters = '0123456789ABCDEF';
let color = '#';
let isGrayScale = true;
while (isGrayScale) {
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
const rgb = this.hexToRgb(color);
if (rgb.r !== rgb.g || rgb.g !== rgb.b) {
isGrayScale = false;
} else {
color = '#';
}
}
return color;
}
/**
* Converts a hex color string to an RGB object.
*
* @param hex A color string in hex format, e.g. '#FF0000'.
* @returns An object with r, g, and b properties, each an integer between
* 0 and 255, representing the red, green, and blue components of the color.
*/
hexToRgb(hex: string): { r: number; g: number; b: number } {
const bigint = parseInt(hex.substring(1), 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
}
// SECURITY
/**
* Sanitizes the input string by replacing any potential cross-site scripting characters.
* This ensures that the input string is safe to be stored in the Firebase Realtime Database.
* @param input The input string to be sanitized.
* @returns The sanitized string.
*/
replaceXSSChars(input: string) {
return input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
} }