Creation of side-menu.

This commit is contained in:
Florian THIERRY
2024-04-02 16:18:03 +02:00
parent 0900df463a
commit c54e1c57d7
13 changed files with 258 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
package org.codiki.infrastructure.category.repository; package org.codiki.infrastructure.category.repository;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.codiki.infrastructure.category.model.CategoryEntity; import org.codiki.infrastructure.category.model.CategoryEntity;
@@ -16,4 +17,7 @@ public interface CategoryRepository extends JpaRepository<CategoryEntity, UUID>
) > 0 ) > 0
""", nativeQuery = true) """, nativeQuery = true)
boolean existsAnyAssociatedPublication(@Param("categoryId") UUID categoryId); boolean existsAnyAssociatedPublication(@Param("categoryId") UUID categoryId);
@Query("SELECT c FROM CategoryEntity c JOIN FETCH c.subCategories")
List<CategoryEntity> findAll();
} }

View File

@@ -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>

View File

@@ -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;
}
}
}
}
}

View 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`;
}
}

View 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);
}
}
}

View File

@@ -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'));
}
}

View File

@@ -0,0 +1,5 @@
export interface Category {
id: string;
name: string;
subCategories: Category[];
}

View 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();
}
}

View File

@@ -1 +1,2 @@
<h1>Welcome to Codiki application!</h1> <h1>Welcome to Codiki application!</h1>
<app-side-menu></app-side-menu>

View File

@@ -1,9 +1,10 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { SideMenuComponent } from '../../components/side-menu/side-menu.component';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
standalone: true, standalone: true,
imports: [], imports: [SideMenuComponent],
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.scss' styleUrl: './home.component.scss'
}) })

View File

@@ -2,8 +2,8 @@ import { Injectable, inject } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { copy } from '../../core/utils/ObjectUtils'; import { copy } from '../../core/utils/ObjectUtils';
import { FormError } from '../../core/model/FormError'; import { FormError } from '../../core/model/FormError';
import { UserRestService } from '../../core/rest-services/user.rest-service'; import { UserRestService } from '../../core/rest-services/user/user.rest-service';
import { LoginRequest } from '../../core/rest-services/model/login.model'; import { LoginRequest } from '../../core/rest-services/user/model/login.model';
import { AuthenticationService } from '../../core/service/authentication.service'; import { AuthenticationService } from '../../core/service/authentication.service';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router'; import { Router } from '@angular/router';