7 Commits
28 changed files with 650 additions and 46 deletions
+4
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>
@@ -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();
}
}
@@ -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;
@@ -39,19 +42,21 @@ public class PublicationUseCases {
private final UserUseCases userUseCases;
public PublicationUseCases(
CategoryUseCases categoryUseCases,
Clock clock,
KeyGenerator keyGenerator,
PictureUseCases pictureUseCases,
PublicationCreationRequestValidator publicationCreationRequestValidator,
PublicationPort publicationPort,
PublicationSearchCriteriaFactory publicationSearchCriteriaFactory,
PublicationUpdateRequestValidator publicationUpdateRequestValidator,
UserUseCases userUseCases
CategoryUseCases categoryUseCases,
Clock clock,
KeyGenerator keyGenerator,
ParserService parserService,
PictureUseCases pictureUseCases,
PublicationCreationRequestValidator publicationCreationRequestValidator,
PublicationPort publicationPort,
PublicationSearchCriteriaFactory publicationSearchCriteriaFactory,
PublicationUpdateRequestValidator publicationUpdateRequestValidator,
UserUseCases userUseCases
) {
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) {
@@ -8,6 +8,7 @@ public record Publication(
String key,
String title,
String text,
String parsedText,
String description,
ZonedDateTime creationDate,
UUID illustrationId,
@@ -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,
@@ -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(),
@@ -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();
}
@@ -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,14 +8,21 @@ 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;
private final PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter;
public PublicationJpaAdapter(
PublicationRepository repository,
PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter
PublicationRepository repository,
PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter
) {
this.repository = repository;
this.publicationSearchCriteriaJpaAdapter = publicationSearchCriteriaJpaAdapter;
@@ -36,7 +37,7 @@ public class PublicationJpaAdapter implements PublicationPort {
@Override
public Optional<Publication> findById(UUID publicationId) {
return repository.findById(publicationId)
.map(PublicationEntity::toDomain);
.map(PublicationEntity::toDomain);
}
@Override
@@ -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,
@@ -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'
);
@@ -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,
+8
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>
@@ -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>
@@ -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%;
}
@@ -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',
})
@@ -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>
@@ -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;
}
}
}
}
}
@@ -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`;
}
}
@@ -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>
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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[];
}
@@ -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();
}
}
@@ -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';