1 Commits

Author SHA1 Message Date
f33b55b1c8 Fix left side menu. (#13)
All checks were successful
Build and Deploy Java Gradle Application / build-and-deploy (push) Successful in 1m16s
2026-02-03 16:14:43 +01:00
4 changed files with 40 additions and 51 deletions

View File

@@ -1,6 +1,6 @@
@for (category of categories$ | async; track category) { @for (category of categories(); track category.id) {
<div class="category {{category.isOpenned ? 'openned' : ''}}"> <div class="category {{category.isOpenned ? 'openned' : ''}}">
<div id="category-{{category.id}}" class="category-header" (click)="setOpenned(category)"> <div id="category-{{category.id}}" class="category-header" (click)="setOpened(category)">
{{ category.name }} {{ category.name }}
<mat-icon>chevron_right</mat-icon> <mat-icon>chevron_right</mat-icon>
</div> </div>

View File

@@ -1,8 +1,7 @@
import {CommonModule} from "@angular/common"; import {CommonModule} from "@angular/common";
import {Component, EventEmitter, inject, OnInit, Output} from "@angular/core"; import {Component, inject, OnInit, output, Signal} from "@angular/core";
import {MatIconModule} from "@angular/material/icon"; import {MatIconModule} from "@angular/material/icon";
import {DisplayableCategory, SideMenuService} from "../side-menu.service"; import {DisplayableCategory, SideMenuService} from "../side-menu.service";
import {Observable} from "rxjs";
import {RouterModule} from "@angular/router"; import {RouterModule} from "@angular/router";
@Component({ @Component({
@@ -16,45 +15,44 @@ import {RouterModule} from "@angular/router";
styleUrl: './categories-menu.component.scss' styleUrl: './categories-menu.component.scss'
}) })
export class CategoriesMenuComponent implements OnInit { export class CategoriesMenuComponent implements OnInit {
private sideMenuService = inject(SideMenuService); readonly #sideMenuService = inject(SideMenuService);
@Output() categoryClicked = output<void>();
categoryClicked = new EventEmitter<void>();
ngOnInit(): void { ngOnInit(): void {
this.sideMenuService.loadCategories(); this.#sideMenuService.loadCategories();
} }
get categories$(): Observable<DisplayableCategory[]> { get categories(): Signal<DisplayableCategory[]> {
return this.sideMenuService.categories$; return this.#sideMenuService.categories;
} }
setOpenned(category: DisplayableCategory): void { setOpened(category: DisplayableCategory): void {
if (category.isOpenned) { if (category.isOpenned) {
const categoryDiv = document.getElementById(`category-${category.id}`); const categoryDiv = document.getElementById(`category-${category.id}`);
if (categoryDiv) { if (categoryDiv) {
this.closeAccordion(categoryDiv); this.#closeAccordion(categoryDiv);
} }
} else { } else {
const categoriesDivs = document.getElementsByClassName('category-header'); const categoriesDivs = document.getElementsByClassName('category-header');
Array.from(categoriesDivs) Array.from(categoriesDivs)
.map(category => category as HTMLElement) .map(category => category as HTMLElement)
.forEach(categoryDiv => this.closeAccordion(categoryDiv)); .forEach(categoryDiv => this.#closeAccordion(categoryDiv));
const categoryDiv = document.getElementById(`category-${category.id}`); const categoryDiv = document.getElementById(`category-${category.id}`);
if (categoryDiv) { if (categoryDiv) {
this.openAccordion(categoryDiv); this.#openAccordion(categoryDiv);
} }
} }
this.sideMenuService.setOpenned(category); this.#sideMenuService.setOpened(category);
} }
private closeAccordion(categoryDiv: HTMLElement): void { #closeAccordion(categoryDiv: HTMLElement): void {
const divContent = categoryDiv?.nextElementSibling as HTMLElement; const divContent = categoryDiv?.nextElementSibling as HTMLElement;
divContent.style.maxHeight = '0'; divContent.style.maxHeight = '0';
} }
private openAccordion(categoryDiv: HTMLElement): void { #openAccordion(categoryDiv: HTMLElement): void {
const divContent = categoryDiv?.nextElementSibling as HTMLElement; const divContent = categoryDiv?.nextElementSibling as HTMLElement;
divContent.style.maxHeight = `${divContent.scrollHeight}px`; divContent.style.maxHeight = `${divContent.scrollHeight}px`;
} }

View File

@@ -14,6 +14,6 @@
</button> </button>
</h1> </h1>
<h2 i18n>Categories</h2> <h2 i18n>Categories</h2>
<app-categories-menu (categoryClicked)="close()"></app-categories-menu> <app-categories-menu (categoryClicked)="close()"/>
</div> </div>
<div class="overlay {{ isOpened() ? 'displayed' : ''}}" (click)="close()"></div> <div class="overlay {{ isOpened() ? 'displayed' : ''}}" (click)="close()"></div>

View File

@@ -1,6 +1,5 @@
import {inject, Injectable} from '@angular/core'; import {inject, Injectable, Signal, signal} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar'; import {MatSnackBar} from '@angular/material/snack-bar';
import {BehaviorSubject, Observable} from 'rxjs';
import {CategoryRestService} from '../../core/rest-services/category/category.rest-service'; import {CategoryRestService} from '../../core/rest-services/category/category.rest-service';
import {Category} from '../../core/rest-services/category/model/category'; import {Category} from '../../core/rest-services/category/model/category';
@@ -20,68 +19,60 @@ export interface DisplayableSubCategory {
providedIn: 'root' providedIn: 'root'
}) })
export class SideMenuService { export class SideMenuService {
private categoryRestService = inject(CategoryRestService); readonly #categoryRestService = inject(CategoryRestService);
private snackBar = inject(MatSnackBar); readonly #snackBar = inject(MatSnackBar);
private categoriesSubject = new BehaviorSubject<DisplayableCategory[]>([]); #categories = signal<DisplayableCategory[]>([]);
private isLoadingSubject = new BehaviorSubject<boolean>(false); #isLoading = signal(false);
private isLoadedSubject = new BehaviorSubject<boolean>(false); isLoaded = signal(false);
private mapToDisplayableCategory(category: Category): DisplayableCategory { #mapToDisplayableCategory(category: Category): DisplayableCategory {
return { return {
id: category.id, id: category.id,
name: category.name, name: category.name,
subCategories: category.subCategories.map(subCategory => this.mapToDisplayableSubCategory(subCategory)), subCategories: category.subCategories.map(subCategory => this.#mapToDisplayableSubCategory(subCategory)),
isOpenned: false isOpenned: false
}; };
} }
private mapToDisplayableSubCategory(subCategory: Category): DisplayableSubCategory { #mapToDisplayableSubCategory(subCategory: Category): DisplayableSubCategory {
return { return {
id: subCategory.id, id: subCategory.id,
name: subCategory.name name: subCategory.name
} }
} }
private get categories(): DisplayableCategory[] { get categories(): Signal<DisplayableCategory[]> {
return this.categoriesSubject.value; return this.#categories.asReadonly();
} }
private save(categories: DisplayableCategory[]): void { get isLoading(): Signal<boolean> {
this.categoriesSubject.next(categories); return this.#isLoading.asReadonly();
}
get categories$(): Observable<DisplayableCategory[]> {
return this.categoriesSubject.asObservable();
}
get isLoading$(): Observable<boolean> {
return this.isLoadingSubject.asObservable();
} }
loadCategories(): void { loadCategories(): void {
this.isLoadingSubject.next(true); this.#isLoading.set(true);
this.isLoadedSubject.next(false); this.isLoaded.set(false);
this.categoryRestService.getCategories() this.#categoryRestService.getCategories()
.then(categories => { .then(categories => {
const displayableCategories = categories const displayableCategories = categories
.filter(category => category.subCategories?.length) .filter(category => category.subCategories?.length)
.map(category => this.mapToDisplayableCategory(category)); .map(category => this.#mapToDisplayableCategory(category));
this.categoriesSubject.next(displayableCategories); this.#categories.set(displayableCategories);
}) })
.catch(error => { .catch(error => {
const errorMessage = $localize`An error occured while loading categories.`; const errorMessage = $localize`An error occured while loading categories.`;
console.error(errorMessage, error); console.error(errorMessage, error);
this.snackBar.open(errorMessage, $localize`Close`, {duration: 5000}); this.#snackBar.open(errorMessage, $localize`Close`, {duration: 5000});
}) })
.finally(() => { .finally(() => {
this.isLoadingSubject.next(false); this.#isLoading.set(false);
this.isLoadedSubject.next(true); this.isLoaded.set(true);
}); });
} }
setOpenned(category: DisplayableCategory): void { setOpened(category: DisplayableCategory): void {
const categories = this.categories; const categories = this.#categories();
const matchingCategory = categories.find(categoryTemp => categoryTemp.id === category.id); const matchingCategory = categories.find(categoryTemp => categoryTemp.id === category.id);
if (matchingCategory) { if (matchingCategory) {
const actualOpennedCategory = categories.find(category => category.isOpenned); const actualOpennedCategory = categories.find(category => category.isOpenned);
@@ -91,7 +82,7 @@ export class SideMenuService {
categories.forEach(categoryTemp => categoryTemp.isOpenned = false); categories.forEach(categoryTemp => categoryTemp.isOpenned = false);
matchingCategory.isOpenned = true; matchingCategory.isOpenned = true;
} }
this.save(categories); this.#categories.set(categories);
} }
} }
} }