Creation of side-menu.
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
<h1>Codiki</h1>
|
||||
<h2>Catégories</h2>
|
||||
<div class="categories-container">
|
||||
<div class="category {{category.isOpenned ? 'openned' : ''}}" *ngFor="let category of categories$ | async">
|
||||
<div id="category-{{category.id}}" class="category-header" (click)="setOpenned(category)">
|
||||
{{category.name}}
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</div>
|
||||
<div class="sub-category-container {{category.isOpenned ? 'displayed' : ''}}">
|
||||
<div class="sub-category" *ngFor="let subCategory of category.subCategories">
|
||||
{{subCategory.name}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.categories-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.category {
|
||||
border: 1px solid blue;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: .5em 1em;
|
||||
|
||||
mat-icon {
|
||||
transition: transform .2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&.openned {
|
||||
.category-header {
|
||||
mat-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sub-category-container {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-category-container {
|
||||
border: 1px solid red;
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
transition: max-height .2s ease-in-out;
|
||||
|
||||
.sub-category {
|
||||
padding: .5em 1em .5em 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
frontend/src/app/components/side-menu/side-menu.component.ts
Normal file
51
frontend/src/app/components/side-menu/side-menu.component.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { DisplayableCategory, SideMenuService } from './side-menu.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-side-menu',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatIconModule],
|
||||
templateUrl: './side-menu.component.html',
|
||||
styleUrl: './side-menu.component.scss'
|
||||
})
|
||||
export class SideMenuComponent {
|
||||
private sideMenuService = inject(SideMenuService);
|
||||
|
||||
get categories$(): Observable<DisplayableCategory[]> {
|
||||
return this.sideMenuService.categories$;
|
||||
}
|
||||
|
||||
setOpenned(category: DisplayableCategory): void {
|
||||
if (category.isOpenned) {
|
||||
const categoryDiv = document.getElementById(`category-${category.id}`);
|
||||
if (categoryDiv) {
|
||||
this.closeAccordion(categoryDiv);
|
||||
}
|
||||
} else {
|
||||
const categoriesDivs = document.getElementsByClassName('category-header');
|
||||
Array.from(categoriesDivs)
|
||||
.map(category => category as HTMLElement)
|
||||
.forEach(categoryDiv => this.closeAccordion(categoryDiv));
|
||||
|
||||
const categoryDiv = document.getElementById(`category-${category.id}`);
|
||||
if (categoryDiv) {
|
||||
this.openAccordion(categoryDiv);
|
||||
}
|
||||
}
|
||||
|
||||
this.sideMenuService.setOpenned(category);
|
||||
}
|
||||
|
||||
private closeAccordion(categoryDiv: HTMLElement): void {
|
||||
const divContent = categoryDiv?.nextElementSibling as HTMLElement;
|
||||
divContent.style.maxHeight = '0';
|
||||
}
|
||||
|
||||
private openAccordion(categoryDiv: HTMLElement): void {
|
||||
const divContent = categoryDiv?.nextElementSibling as HTMLElement;
|
||||
divContent.style.maxHeight = `${divContent.scrollHeight}px`;
|
||||
}
|
||||
}
|
||||
86
frontend/src/app/components/side-menu/side-menu.service.ts
Normal file
86
frontend/src/app/components/side-menu/side-menu.service.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Injectable, OnDestroy, inject } from '@angular/core';
|
||||
import { CategoryService } from '../../core/service/category.service';
|
||||
import { BehaviorSubject, Observable, Subscription, map } from 'rxjs';
|
||||
import { Category } from '../../core/rest-services/category/model/category';
|
||||
|
||||
export interface DisplayableCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
subCategories: DisplayableSubCategory[];
|
||||
isOpenned: boolean;
|
||||
}
|
||||
|
||||
export interface DisplayableSubCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SideMenuService implements OnDestroy {
|
||||
private categoryService = inject(CategoryService);
|
||||
private categoriesSubject = new BehaviorSubject<DisplayableCategory[]>([]);
|
||||
private categoriesSubscription: Subscription | undefined;
|
||||
|
||||
constructor() {
|
||||
this.categoriesSubscription = this.categoryService.categories$
|
||||
.pipe(
|
||||
map(categories =>
|
||||
categories
|
||||
.filter(category => category.subCategories?.length)
|
||||
.map(category =>
|
||||
this.mapToDisplayableCategory(category)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(categories => this.categoriesSubject.next(categories));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.categoriesSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private mapToDisplayableCategory(category: Category): DisplayableCategory {
|
||||
return {
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
subCategories: category.subCategories.map(subCategory => this.mapToDisplayableSubCategory(subCategory)),
|
||||
isOpenned: false
|
||||
};
|
||||
}
|
||||
|
||||
private mapToDisplayableSubCategory(subCategory: Category): DisplayableSubCategory {
|
||||
return {
|
||||
id: subCategory.id,
|
||||
name: subCategory.name
|
||||
}
|
||||
}
|
||||
|
||||
get categories$(): Observable<DisplayableCategory[]> {
|
||||
return this.categoriesSubject.asObservable();
|
||||
}
|
||||
|
||||
private get categories(): DisplayableCategory[] {
|
||||
return this.categoriesSubject.value;
|
||||
}
|
||||
|
||||
private save(categories: DisplayableCategory[]): void {
|
||||
this.categoriesSubject.next(categories);
|
||||
}
|
||||
|
||||
setOpenned(category: DisplayableCategory): void {
|
||||
const categories = this.categories;
|
||||
const matchingCategory = categories.find(categoryTemp => categoryTemp.id === category.id);
|
||||
if (matchingCategory) {
|
||||
const actualOpennedCategory = categories.find(category => category.isOpenned);
|
||||
if (actualOpennedCategory && actualOpennedCategory.id === matchingCategory.id) {
|
||||
matchingCategory.isOpenned = false;
|
||||
} else {
|
||||
categories.forEach(categoryTemp => categoryTemp.isOpenned = false);
|
||||
matchingCategory.isOpenned = true;
|
||||
}
|
||||
this.save(categories);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { Category } from './model/category';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CategoryRestService {
|
||||
private httpClient = inject(HttpClient);
|
||||
|
||||
getCategories(): Promise<Category[]> {
|
||||
return lastValueFrom(this.httpClient.get<Category[]>('/api/categories'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
subCategories: Category[];
|
||||
}
|
||||
25
frontend/src/app/core/service/category.service.ts
Normal file
25
frontend/src/app/core/service/category.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { CategoryRestService } from '../rest-services/category/category.rest-service';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { Category } from '../rest-services/category/model/category';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CategoryService {
|
||||
private categoryRestService = inject(CategoryRestService);
|
||||
private categoriesSubject = new BehaviorSubject<Category[]>([]);
|
||||
|
||||
private get categories(): Category[] {
|
||||
return this.categoriesSubject.value;
|
||||
}
|
||||
|
||||
get categories$(): Observable<Category[]> {
|
||||
if (!this.categories?.length) {
|
||||
this.categoryRestService.getCategories()
|
||||
.then(categories => this.categoriesSubject.next(categories))
|
||||
.catch(error => console.error('An error occured while loading categories.', error));
|
||||
}
|
||||
return this.categoriesSubject.asObservable();
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
<h1>Welcome to Codiki application!</h1>
|
||||
<app-side-menu></app-side-menu>
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { SideMenuComponent } from '../../components/side-menu/side-menu.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
imports: [SideMenuComponent],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.scss'
|
||||
})
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Injectable, inject } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { copy } from '../../core/utils/ObjectUtils';
|
||||
import { FormError } from '../../core/model/FormError';
|
||||
import { UserRestService } from '../../core/rest-services/user.rest-service';
|
||||
import { LoginRequest } from '../../core/rest-services/model/login.model';
|
||||
import { UserRestService } from '../../core/rest-services/user/user.rest-service';
|
||||
import { LoginRequest } from '../../core/rest-services/user/model/login.model';
|
||||
import { AuthenticationService } from '../../core/service/authentication.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
Reference in New Issue
Block a user