Compare commits

7 Commits

Author SHA1 Message Date
Florian THIERRY
d324b94ddb Add SQL script to rebuild categories hierarchy. 2024-04-22 16:05:59 +02:00
Florian THIERRY
4985889c58 Styling header. 2024-04-22 16:05:35 +02:00
Florian THIERRY
7f5d52dce5 Add side menu for header. 2024-04-22 15:57:22 +02:00
Florian THIERRY
fae709a254 Fix class not found error. 2024-04-22 14:37:20 +02:00
Florian THIERRY
45355f6c42 Refactor publication parser location. 2024-04-22 14:22:36 +02:00
Florian THIERRY
db492b6316 Add parsed text to publication entities. 2024-04-22 14:13:17 +02:00
Florian THIERRY
c54e1c57d7 Creation of side-menu. 2024-04-02 16:18:03 +02:00
28 changed files with 650 additions and 46 deletions

View File

@@ -33,6 +33,10 @@
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>

View File

@@ -0,0 +1,144 @@
package org.codiki.application.publication;
import org.springframework.stereotype.Service;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;
@Service
public class ParserService {
private static final String REG_CODE = "\\[code lg=&quot;([a-z]+)&quot;\\](.*)\\[\\/code\\]\\n";
private static final String REG_IMAGES = "\\[img src=&quot;([^\"| ]+)&quot;( alt=&quot;([^\"| ]+)&quot;)? \\/\\]";
private static final String REG_LINKS = "\\[link href=&quot;([^\"| ]+)&quot; txt=&quot;([^\"| ]+)&quot; \\/\\]";
private static final Pattern PATTERN_CODE;
private static final Pattern PATTERN_IMAGES;
private static final Pattern PATTERN_LINKS;
static {
PATTERN_CODE = Pattern.compile(REG_CODE);
PATTERN_IMAGES = Pattern.compile(REG_IMAGES);
PATTERN_LINKS = Pattern.compile(REG_LINKS);
}
public String parse(String pSource) {
return unParseDolars(parseCode(parseHeaders(parseImages(parseLinks(parseBackSpaces(escapeHtml4(parseDolars(pSource))))))));
}
private String parseDolars(final String pSource) {
return pSource.replace("$", "£ø");
}
private String unParseDolars(final String pSource) {
return pSource.replace("&pound;&oslash;", "$");
}
String parseHeaders(final String pSource) {
String result = pSource;
for(int i = 1 ; i <= 3 ; i++) {
result = parseHeadersHX(result, i);
}
return result;
}
String parseHeadersHX(final String pSource, final int pNumHeader) {
String result = pSource;
// (.*)(\[hX\](.*)\[\/hX\])+(.*)
final String regex = concat("(.*)(\\[h", pNumHeader, "\\](.*)\\[\\/h", pNumHeader, "\\])+(.*)");
final Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(result);
while(matcher.find()) {
// \1<hX>\3</hX>\4
result = matcher.replaceFirst(concat(matcher.group(1),
"<h", pNumHeader, ">", matcher.group(3), "</h", pNumHeader, ">", matcher.group(4)));
matcher = pattern.matcher(result);
}
return result;
}
String parseBackSpaces(final String pSource) {
return pSource.replaceAll("\r?\n", "<br/>").replaceAll("\\[\\/code\\]<br\\/><br\\/>", "[/code]\n");
}
String parseImages(final String pSource) {
String result = pSource;
Matcher matcher = PATTERN_IMAGES.matcher(result);
while(matcher.find()) {
String altStr = matcher.group(3);
if(altStr == null) {
altStr = "";
}
result = matcher.replaceFirst(concat("<img src=\"", matcher.group(1), "\" alt=\"", altStr, "\" />"));
matcher = PATTERN_IMAGES.matcher(result);
}
return result;
}
String parseLinks(final String pSource) {
String result = pSource;
Matcher matcher = PATTERN_LINKS.matcher(result);
while(matcher.find()) {
result = matcher.replaceFirst(concat("<a href=\"", matcher.group(1), "\">", matcher.group(2), "</a>"));
matcher = PATTERN_LINKS.matcher(result);
}
return result;
}
protected String parseCode(final String pSource) {
String result = pSource;
Matcher matcher = PATTERN_CODE.matcher(result);
while(matcher.find()) {
// replace the '<br/>' in group by '\n'
String codeContent = matcher.group(2).replaceAll("<br\\/>", "\n");
if(codeContent.startsWith("\n")) {
codeContent = codeContent.substring(1);
}
result = matcher.replaceFirst(
concat(
"<pre class=\"line-numbers\"><code class=\"language-",
matcher.group(1),
"\">",
codeContent,
"</code></pre>"
)
);
matcher = PATTERN_CODE.matcher(result);
}
return result;
}
/**
* Concatenate the parameters to form just one single string.
*
* @param pArgs
* The strings to concatenate.
* @return The parameters concatenated.
*/
private static String concat(final Object... pArgs) {
final StringBuilder result = new StringBuilder();
for (final Object arg : pArgs) {
result.append(arg);
}
return result.toString();
}
}

View File

@@ -9,6 +9,8 @@ import java.util.UUID;
import static org.codiki.domain.publication.model.builder.AuthorBuilder.anAuthor;
import static org.codiki.domain.publication.model.builder.PublicationBuilder.aPublication;
import static org.springframework.util.ObjectUtils.isEmpty;
import org.codiki.application.category.CategoryUseCases;
import org.codiki.application.picture.PictureUseCases;
import org.codiki.application.user.UserUseCases;
@@ -31,6 +33,7 @@ public class PublicationUseCases {
private final CategoryUseCases categoryUseCases;
private final Clock clock;
private final KeyGenerator keyGenerator;
private final ParserService parserService;
private final PictureUseCases pictureUseCases;
private final PublicationCreationRequestValidator publicationCreationRequestValidator;
private final PublicationPort publicationPort;
@@ -42,6 +45,7 @@ public class PublicationUseCases {
CategoryUseCases categoryUseCases,
Clock clock,
KeyGenerator keyGenerator,
ParserService parserService,
PictureUseCases pictureUseCases,
PublicationCreationRequestValidator publicationCreationRequestValidator,
PublicationPort publicationPort,
@@ -52,6 +56,7 @@ public class PublicationUseCases {
this.categoryUseCases = categoryUseCases;
this.clock = clock;
this.keyGenerator = keyGenerator;
this.parserService = parserService;
this.publicationCreationRequestValidator = publicationCreationRequestValidator;
this.publicationPort = publicationPort;
this.publicationUpdateRequestValidator = publicationUpdateRequestValidator;
@@ -83,6 +88,7 @@ public class PublicationUseCases {
.withKey(keyGenerator.generateKey())
.withTitle(request.title())
.withText(request.text())
.withParsedText(parserService.parse(request.text()))
.withDescription(request.description())
.withCreationDate(ZonedDateTime.now(clock))
.withIllustrationId(request.illustrationId())
@@ -116,6 +122,7 @@ public class PublicationUseCases {
if (!isNull(request.text())) {
publicationBuilder.withText(request.text());
publicationBuilder.withParsedText(parserService.parse(request.text()));
}
if (!isNull(request.description())) {
@@ -163,7 +170,19 @@ public class PublicationUseCases {
}
public Optional<Publication> findById(UUID publicationId) {
return publicationPort.findById(publicationId);
return publicationPort.findById(publicationId)
.map(publication -> {
Publication result = publication;
if (isEmpty(publication.parsedText())) {
Publication editedPublication = aPublication()
.basedOn(publication)
.withParsedText(parserService.parse(publication.text()))
.build();
publicationPort.save(editedPublication);
result = editedPublication;
}
return result;
});
}
public List<Publication> searchPublications(String searchQuery) {

View File

@@ -8,6 +8,7 @@ public record Publication(
String key,
String title,
String text,
String parsedText,
String description,
ZonedDateTime creationDate,
UUID illustrationId,

View File

@@ -1,17 +1,17 @@
package org.codiki.domain.publication.model.builder;
import org.codiki.domain.publication.model.Author;
import org.codiki.domain.publication.model.Publication;
import java.time.ZonedDateTime;
import java.util.UUID;
import org.codiki.domain.category.model.Category;
import org.codiki.domain.publication.model.Author;
import org.codiki.domain.publication.model.Publication;
public class PublicationBuilder {
private UUID id;
private String key;
private String title;
private String text;
private String parsedText;
private String description;
private ZonedDateTime creationDate;
private UUID illustrationId;
@@ -30,6 +30,7 @@ public class PublicationBuilder {
.withKey(publication.key())
.withTitle(publication.title())
.withText(publication.text())
.withParsedText(publication.parsedText())
.withDescription(publication.description())
.withCreationDate(publication.creationDate())
.withIllustrationId(publication.illustrationId())
@@ -57,6 +58,11 @@ public class PublicationBuilder {
return this;
}
public PublicationBuilder withParsedText(String parsedText) {
this.parsedText = parsedText;
return this;
}
public PublicationBuilder withDescription(String description) {
this.description = description;
return this;
@@ -88,6 +94,7 @@ public class PublicationBuilder {
key,
title,
text,
parsedText,
description,
creationDate,
illustrationId,

View File

@@ -1,16 +1,16 @@
package org.codiki.exposition.publication.model;
import org.codiki.domain.publication.model.Publication;
import java.time.ZonedDateTime;
import java.util.UUID;
import org.codiki.domain.publication.model.Publication;
import org.codiki.exposition.category.model.CategoryDto;
public record PublicationDto(
UUID id,
String key,
String title,
String text,
String parsedText,
String description,
ZonedDateTime creationDate,
UUID illustrationId,
@@ -23,6 +23,7 @@ public record PublicationDto(
publication.key(),
publication.title(),
publication.text(),
publication.parsedText(),
publication.description(),
publication.creationDate(),
publication.illustrationId(),

View File

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

View File

@@ -1,11 +1,5 @@
package org.codiki.infrastructure.publication;
import static java.util.Collections.reverseOrder;
import static java.util.Comparator.comparingInt;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.codiki.domain.publication.model.Publication;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
import org.codiki.domain.publication.port.PublicationPort;
@@ -14,6 +8,13 @@ import org.codiki.infrastructure.publication.model.PublicationSearchResult;
import org.codiki.infrastructure.publication.repository.PublicationRepository;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static java.util.Collections.reverseOrder;
import static java.util.Comparator.comparingInt;
@Component
public class PublicationJpaAdapter implements PublicationPort {
private final PublicationRepository repository;

View File

@@ -1,22 +1,16 @@
package org.codiki.infrastructure.publication.model;
import java.time.ZonedDateTime;
import java.util.UUID;
import org.codiki.domain.publication.model.Publication;
import org.codiki.infrastructure.category.model.CategoryEntity;
import static jakarta.persistence.FetchType.LAZY;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.codiki.domain.publication.model.Publication;
import java.time.ZonedDateTime;
import java.util.UUID;
import static jakarta.persistence.FetchType.LAZY;
@Entity
@Table(name = "publication")
@@ -34,6 +28,8 @@ public class PublicationEntity {
@Column(nullable = false)
private String text;
@Column(nullable = false)
private String parsedText;
@Column(nullable = false)
private String description;
@Column(nullable = false)
private ZonedDateTime creationDate;
@@ -51,6 +47,7 @@ public class PublicationEntity {
publication.key(),
publication.title(),
publication.text(),
publication.parsedText(),
publication.description(),
publication.creationDate(),
publication.illustrationId(),
@@ -65,6 +62,7 @@ public class PublicationEntity {
key,
title,
text,
parsedText,
description,
creationDate,
illustrationId,

View File

@@ -10,3 +10,27 @@ insert into user_role values
insert into category values
('172fa901-3f4b-4540-92f3-1c15820e8ec9', 'Main category', null),
('3f4b4540-a901-92f3-1c15-8ec9172f820e', 'Sub category', '172fa901-3f4b-4540-92f3-1c15820e8ec9');
UPDATE public.category
SET parent_category_id='04347de5-2814-4aff-9fe9-51b34c1c743e'
WHERE id in (
'0f4c4d7c-2ccc-4725-88b6-672aa518da90',
'2cad9c28-ab5d-4c8f-b7da-70ff8bc02586'
);
UPDATE public.category
SET parent_category_id='3dec7c5a-e7d6-4b21-beb1-209cdf5be067'
WHERE id in (
'61f9fbf3-3340-4ea4-9661-04089377bb2e',
'753570cc-3403-4bac-b9da-6c19875d98b7',
'1515ff79-e42e-4d84-9496-6cdcf1cb74f2',
'b58bda0b-2f45-4c7a-8ece-1a206fb32a7a',
'49b4df8a-19f5-459b-b508-6b7c71332523'
);
UPDATE public.category
SET parent_category_id='41b2792e-6f65-48be-8718-82ac58101aa8'
WHERE id in (
'7234cd9e-3834-45c5-973b-1574f5c3c4c6',
'f46fb104-4f53-4732-b33b-6a3ef8c2c0a3'
);

View File

@@ -49,6 +49,7 @@ CREATE TABLE IF NOT EXISTS publication (
key VARCHAR(14) NOT NULL,
title VARCHAR NOT NULL,
text VARCHAR NOT NULL,
parsed_text VARCHAR,
description VARCHAR NOT NULL,
creation_date TIMESTAMP NOT NULL,
illustration_id UUID NOT NULL,

View File

@@ -19,6 +19,7 @@
<java-jwt.version>4.4.0</java-jwt.version>
<postgresql.version>42.7.0</postgresql.version>
<tika-core.version>2.9.0</tika-core.version>
<commons-lang3.version>3.14.0</commons-lang3.version>
</properties>
<modules>
@@ -78,6 +79,13 @@
<artifactId>tika-core</artifactId>
<version>${tika-core.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -1,5 +1,5 @@
<div>
<button type="button">
<button type="button" (click)="sideMenu.open()">
<mat-icon>menu</mat-icon>
</button>
<a [routerLink]="['/home']">
@@ -21,3 +21,4 @@
<a [routerLink]="['/login']">Login</a>
</ng-template>
</div>
<app-side-menu #sideMenu></app-side-menu>

View File

@@ -7,11 +7,10 @@ $headerHeight: 3.5em;
background-color: #3f51b5;
color: white;
position: relative;
border: 1px solid black;
height: $headerHeight;
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
div {
border: 1px solid black;
display: flex;
flex-direction: row;
justify-content: center;
@@ -25,6 +24,22 @@ $headerHeight: 3.5em;
gap: 1em;
padding: 0 1em;
button {
background-color: #3f51b5;
color: white;
border: none;
border-radius: 10em;
transition: background-color .2s ease-in-out;
display: flex;
justify-content: center;
align-items: center;
&:hover {
cursor: pointer;
background-color: #5c6bc0;
}
}
a {
display: flex;
flex-direction: row;
@@ -91,3 +106,7 @@ $headerHeight: 3.5em;
}
}
}
app-side-menu {
height: 100%;
}

View File

@@ -4,11 +4,12 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { AuthenticationService } from '../../core/service/authentication.service';
import { CommonModule } from '@angular/common';
import { SideMenuComponent } from '../side-menu/side-menu.component';
@Component({
selector: 'app-header',
standalone: true,
imports: [CommonModule, MatButtonModule, MatIconModule, RouterModule],
imports: [CommonModule, MatButtonModule, MatIconModule, RouterModule, SideMenuComponent],
templateUrl: './header.component.html',
styleUrl: './header.component.scss',
})

View File

@@ -0,0 +1,11 @@
<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' : ''}}">
<a [routerLink]="['/']" class="sub-category" *ngFor="let subCategory of category.subCategories">
{{subCategory.name}}
</a>
</div>
</div>

View File

@@ -0,0 +1,57 @@
:host {
display: flex;
flex-direction: column;
.category {
transition: background-color .2s ease-in-out;
&:hover {
cursor: pointer;
background-color: #5c6bc0;
}
.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 {
display: flex;
flex-direction: column;
overflow: hidden;
max-height: 0;
transition: max-height .2s ease-in-out;
background-color: #303f9f;
.sub-category {
padding: .5em 1em .5em 2em;
text-decoration: none;
color: inherit;
transition: background-color .2s ease-in-out;
&:hover {
background-color: #5c6bc0;
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
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";
import { RouterModule } from "@angular/router";
@Component({
selector: 'app-categories-menu',
standalone: true,
imports: [CommonModule, RouterModule, MatIconModule],
templateUrl: './categories-menu.component.html',
styleUrl: './categories-menu.component.scss'
})
export class CategoriesMenuComponent {
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,14 @@
<div class="menu {{ isOpenned ? 'displayed' : '' }}">
<h1>
<span>
<img src="assets/images/codiki.png" alt="logo"/>
Codiki
</span>
<button type="button" (click)="close()" matTooltip="Close the menu">
<mat-icon>close</mat-icon>
</button>
</h1>
<h2>Catégories</h2>
<app-categories-menu></app-categories-menu>
</div>
<div class="overlay {{ isOpenned ? 'displayed' : ''}}" (click)="close()"></div>

View File

@@ -0,0 +1,83 @@
:host {
.menu {
display: flex;
flex-direction: column;
background-color: #3f51b5;
color: white;
$categoriesMenuWidth: 20em;
position: fixed;
top: 0;
left: -$categoriesMenuWidth - 1em - 1;
bottom: 0;
transition: left .2s ease-in-out;
width: $categoriesMenuWidth;
z-index: 2;
padding: 1em 0;
&.displayed {
left: 0;
}
h1 {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 1em;
span {
display: flex;
flex-direction: row;
justify-content: start;
align-items: center;
gap: .5em;
img {
$imageSize: 1.2em;
width: $imageSize;
height: $imageSize;
}
}
button {
border-radius: 10em;
border: none;
display: flex;
justify-content: center;
align-items: center;
width: 2.5em;
height: 2.5em;
color: white;
background-color: #3f51b5;
transition: background-color .2s ease-in-out;
&:hover {
cursor: pointer;
background-color: #5c6bc0;
}
}
}
h2 {
padding: 0 1em;
}
}
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: .2;
&.displayed {
display: block;
}
}
}

View File

@@ -0,0 +1,23 @@
import {Component} from '@angular/core';
import {MatIconModule} from '@angular/material/icon';
import {CategoriesMenuComponent} from './categories-menu/categories-menu.component';
import {MatTooltipModule} from '@angular/material/tooltip';
@Component({
selector: 'app-side-menu',
standalone: true,
imports: [CategoriesMenuComponent, MatIconModule, MatTooltipModule],
templateUrl: './side-menu.component.html',
styleUrl: './side-menu.component.scss'
})
export class SideMenuComponent {
isOpenned: boolean = false;
open(): void {
this.isOpenned = true;
}
close(): void {
this.isOpenned = false;
}
}

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

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