Creation of side-menu.
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
<h1>Welcome to Codiki application!</h1>
|
||||||
|
<app-side-menu></app-side-menu>
|
||||||
@@ -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'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user