Compare commits
7 Commits
0900df463a
...
d324b94ddb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d324b94ddb | ||
|
|
4985889c58 | ||
|
|
7f5d52dce5 | ||
|
|
fae709a254 | ||
|
|
45355f6c42 | ||
|
|
db492b6316 | ||
|
|
c54e1c57d7 |
@@ -33,6 +33,10 @@
|
|||||||
<groupId>com.auth0</groupId>
|
<groupId>com.auth0</groupId>
|
||||||
<artifactId>java-jwt</artifactId>
|
<artifactId>java-jwt</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter-api</artifactId>
|
<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="([a-z]+)"\\](.*)\\[\\/code\\]\\n";
|
||||||
|
private static final String REG_IMAGES = "\\[img src="([^\"| ]+)"( alt="([^\"| ]+)")? \\/\\]";
|
||||||
|
private static final String REG_LINKS = "\\[link href="([^\"| ]+)" txt="([^\"| ]+)" \\/\\]";
|
||||||
|
|
||||||
|
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("£ø", "$");
|
||||||
|
}
|
||||||
|
|
||||||
|
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.AuthorBuilder.anAuthor;
|
||||||
import static org.codiki.domain.publication.model.builder.PublicationBuilder.aPublication;
|
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.category.CategoryUseCases;
|
||||||
import org.codiki.application.picture.PictureUseCases;
|
import org.codiki.application.picture.PictureUseCases;
|
||||||
import org.codiki.application.user.UserUseCases;
|
import org.codiki.application.user.UserUseCases;
|
||||||
@@ -31,6 +33,7 @@ public class PublicationUseCases {
|
|||||||
private final CategoryUseCases categoryUseCases;
|
private final CategoryUseCases categoryUseCases;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final KeyGenerator keyGenerator;
|
private final KeyGenerator keyGenerator;
|
||||||
|
private final ParserService parserService;
|
||||||
private final PictureUseCases pictureUseCases;
|
private final PictureUseCases pictureUseCases;
|
||||||
private final PublicationCreationRequestValidator publicationCreationRequestValidator;
|
private final PublicationCreationRequestValidator publicationCreationRequestValidator;
|
||||||
private final PublicationPort publicationPort;
|
private final PublicationPort publicationPort;
|
||||||
@@ -39,19 +42,21 @@ public class PublicationUseCases {
|
|||||||
private final UserUseCases userUseCases;
|
private final UserUseCases userUseCases;
|
||||||
|
|
||||||
public PublicationUseCases(
|
public PublicationUseCases(
|
||||||
CategoryUseCases categoryUseCases,
|
CategoryUseCases categoryUseCases,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
KeyGenerator keyGenerator,
|
KeyGenerator keyGenerator,
|
||||||
PictureUseCases pictureUseCases,
|
ParserService parserService,
|
||||||
PublicationCreationRequestValidator publicationCreationRequestValidator,
|
PictureUseCases pictureUseCases,
|
||||||
PublicationPort publicationPort,
|
PublicationCreationRequestValidator publicationCreationRequestValidator,
|
||||||
PublicationSearchCriteriaFactory publicationSearchCriteriaFactory,
|
PublicationPort publicationPort,
|
||||||
PublicationUpdateRequestValidator publicationUpdateRequestValidator,
|
PublicationSearchCriteriaFactory publicationSearchCriteriaFactory,
|
||||||
UserUseCases userUseCases
|
PublicationUpdateRequestValidator publicationUpdateRequestValidator,
|
||||||
|
UserUseCases userUseCases
|
||||||
) {
|
) {
|
||||||
this.categoryUseCases = categoryUseCases;
|
this.categoryUseCases = categoryUseCases;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
this.keyGenerator = keyGenerator;
|
this.keyGenerator = keyGenerator;
|
||||||
|
this.parserService = parserService;
|
||||||
this.publicationCreationRequestValidator = publicationCreationRequestValidator;
|
this.publicationCreationRequestValidator = publicationCreationRequestValidator;
|
||||||
this.publicationPort = publicationPort;
|
this.publicationPort = publicationPort;
|
||||||
this.publicationUpdateRequestValidator = publicationUpdateRequestValidator;
|
this.publicationUpdateRequestValidator = publicationUpdateRequestValidator;
|
||||||
@@ -83,6 +88,7 @@ public class PublicationUseCases {
|
|||||||
.withKey(keyGenerator.generateKey())
|
.withKey(keyGenerator.generateKey())
|
||||||
.withTitle(request.title())
|
.withTitle(request.title())
|
||||||
.withText(request.text())
|
.withText(request.text())
|
||||||
|
.withParsedText(parserService.parse(request.text()))
|
||||||
.withDescription(request.description())
|
.withDescription(request.description())
|
||||||
.withCreationDate(ZonedDateTime.now(clock))
|
.withCreationDate(ZonedDateTime.now(clock))
|
||||||
.withIllustrationId(request.illustrationId())
|
.withIllustrationId(request.illustrationId())
|
||||||
@@ -116,6 +122,7 @@ public class PublicationUseCases {
|
|||||||
|
|
||||||
if (!isNull(request.text())) {
|
if (!isNull(request.text())) {
|
||||||
publicationBuilder.withText(request.text());
|
publicationBuilder.withText(request.text());
|
||||||
|
publicationBuilder.withParsedText(parserService.parse(request.text()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNull(request.description())) {
|
if (!isNull(request.description())) {
|
||||||
@@ -163,7 +170,19 @@ public class PublicationUseCases {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Publication> findById(UUID publicationId) {
|
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) {
|
public List<Publication> searchPublications(String searchQuery) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public record Publication(
|
|||||||
String key,
|
String key,
|
||||||
String title,
|
String title,
|
||||||
String text,
|
String text,
|
||||||
|
String parsedText,
|
||||||
String description,
|
String description,
|
||||||
ZonedDateTime creationDate,
|
ZonedDateTime creationDate,
|
||||||
UUID illustrationId,
|
UUID illustrationId,
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package org.codiki.domain.publication.model.builder;
|
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.time.ZonedDateTime;
|
||||||
import java.util.UUID;
|
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 {
|
public class PublicationBuilder {
|
||||||
private UUID id;
|
private UUID id;
|
||||||
private String key;
|
private String key;
|
||||||
private String title;
|
private String title;
|
||||||
private String text;
|
private String text;
|
||||||
|
private String parsedText;
|
||||||
private String description;
|
private String description;
|
||||||
private ZonedDateTime creationDate;
|
private ZonedDateTime creationDate;
|
||||||
private UUID illustrationId;
|
private UUID illustrationId;
|
||||||
@@ -30,6 +30,7 @@ public class PublicationBuilder {
|
|||||||
.withKey(publication.key())
|
.withKey(publication.key())
|
||||||
.withTitle(publication.title())
|
.withTitle(publication.title())
|
||||||
.withText(publication.text())
|
.withText(publication.text())
|
||||||
|
.withParsedText(publication.parsedText())
|
||||||
.withDescription(publication.description())
|
.withDescription(publication.description())
|
||||||
.withCreationDate(publication.creationDate())
|
.withCreationDate(publication.creationDate())
|
||||||
.withIllustrationId(publication.illustrationId())
|
.withIllustrationId(publication.illustrationId())
|
||||||
@@ -57,6 +58,11 @@ public class PublicationBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PublicationBuilder withParsedText(String parsedText) {
|
||||||
|
this.parsedText = parsedText;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public PublicationBuilder withDescription(String description) {
|
public PublicationBuilder withDescription(String description) {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
return this;
|
return this;
|
||||||
@@ -88,6 +94,7 @@ public class PublicationBuilder {
|
|||||||
key,
|
key,
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
|
parsedText,
|
||||||
description,
|
description,
|
||||||
creationDate,
|
creationDate,
|
||||||
illustrationId,
|
illustrationId,
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package org.codiki.exposition.publication.model;
|
package org.codiki.exposition.publication.model;
|
||||||
|
|
||||||
|
import org.codiki.domain.publication.model.Publication;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.codiki.domain.publication.model.Publication;
|
|
||||||
import org.codiki.exposition.category.model.CategoryDto;
|
|
||||||
|
|
||||||
public record PublicationDto(
|
public record PublicationDto(
|
||||||
UUID id,
|
UUID id,
|
||||||
String key,
|
String key,
|
||||||
String title,
|
String title,
|
||||||
String text,
|
String text,
|
||||||
|
String parsedText,
|
||||||
String description,
|
String description,
|
||||||
ZonedDateTime creationDate,
|
ZonedDateTime creationDate,
|
||||||
UUID illustrationId,
|
UUID illustrationId,
|
||||||
@@ -23,6 +23,7 @@ public record PublicationDto(
|
|||||||
publication.key(),
|
publication.key(),
|
||||||
publication.title(),
|
publication.title(),
|
||||||
publication.text(),
|
publication.text(),
|
||||||
|
publication.parsedText(),
|
||||||
publication.description(),
|
publication.description(),
|
||||||
publication.creationDate(),
|
publication.creationDate(),
|
||||||
publication.illustrationId(),
|
publication.illustrationId(),
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
package org.codiki.infrastructure.publication;
|
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.Publication;
|
||||||
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
|
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
|
||||||
import org.codiki.domain.publication.port.PublicationPort;
|
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.codiki.infrastructure.publication.repository.PublicationRepository;
|
||||||
import org.springframework.stereotype.Component;
|
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
|
@Component
|
||||||
public class PublicationJpaAdapter implements PublicationPort {
|
public class PublicationJpaAdapter implements PublicationPort {
|
||||||
private final PublicationRepository repository;
|
private final PublicationRepository repository;
|
||||||
private final PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter;
|
private final PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter;
|
||||||
|
|
||||||
public PublicationJpaAdapter(
|
public PublicationJpaAdapter(
|
||||||
PublicationRepository repository,
|
PublicationRepository repository,
|
||||||
PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter
|
PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter
|
||||||
) {
|
) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.publicationSearchCriteriaJpaAdapter = publicationSearchCriteriaJpaAdapter;
|
this.publicationSearchCriteriaJpaAdapter = publicationSearchCriteriaJpaAdapter;
|
||||||
@@ -36,7 +37,7 @@ public class PublicationJpaAdapter implements PublicationPort {
|
|||||||
@Override
|
@Override
|
||||||
public Optional<Publication> findById(UUID publicationId) {
|
public Optional<Publication> findById(UUID publicationId) {
|
||||||
return repository.findById(publicationId)
|
return repository.findById(publicationId)
|
||||||
.map(PublicationEntity::toDomain);
|
.map(PublicationEntity::toDomain);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
package org.codiki.infrastructure.publication.model;
|
package org.codiki.infrastructure.publication.model;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import jakarta.persistence.*;
|
||||||
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 lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
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
|
@Entity
|
||||||
@Table(name = "publication")
|
@Table(name = "publication")
|
||||||
@@ -34,6 +28,8 @@ public class PublicationEntity {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String text;
|
private String text;
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
private String parsedText;
|
||||||
|
@Column(nullable = false)
|
||||||
private String description;
|
private String description;
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private ZonedDateTime creationDate;
|
private ZonedDateTime creationDate;
|
||||||
@@ -51,6 +47,7 @@ public class PublicationEntity {
|
|||||||
publication.key(),
|
publication.key(),
|
||||||
publication.title(),
|
publication.title(),
|
||||||
publication.text(),
|
publication.text(),
|
||||||
|
publication.parsedText(),
|
||||||
publication.description(),
|
publication.description(),
|
||||||
publication.creationDate(),
|
publication.creationDate(),
|
||||||
publication.illustrationId(),
|
publication.illustrationId(),
|
||||||
@@ -65,6 +62,7 @@ public class PublicationEntity {
|
|||||||
key,
|
key,
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
|
parsedText,
|
||||||
description,
|
description,
|
||||||
creationDate,
|
creationDate,
|
||||||
illustrationId,
|
illustrationId,
|
||||||
|
|||||||
@@ -10,3 +10,27 @@ insert into user_role values
|
|||||||
insert into category values
|
insert into category values
|
||||||
('172fa901-3f4b-4540-92f3-1c15820e8ec9', 'Main category', null),
|
('172fa901-3f4b-4540-92f3-1c15820e8ec9', 'Main category', null),
|
||||||
('3f4b4540-a901-92f3-1c15-8ec9172f820e', 'Sub category', '172fa901-3f4b-4540-92f3-1c15820e8ec9');
|
('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,
|
key VARCHAR(14) NOT NULL,
|
||||||
title VARCHAR NOT NULL,
|
title VARCHAR NOT NULL,
|
||||||
text VARCHAR NOT NULL,
|
text VARCHAR NOT NULL,
|
||||||
|
parsed_text VARCHAR,
|
||||||
description VARCHAR NOT NULL,
|
description VARCHAR NOT NULL,
|
||||||
creation_date TIMESTAMP NOT NULL,
|
creation_date TIMESTAMP NOT NULL,
|
||||||
illustration_id UUID NOT NULL,
|
illustration_id UUID NOT NULL,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<java-jwt.version>4.4.0</java-jwt.version>
|
<java-jwt.version>4.4.0</java-jwt.version>
|
||||||
<postgresql.version>42.7.0</postgresql.version>
|
<postgresql.version>42.7.0</postgresql.version>
|
||||||
<tika-core.version>2.9.0</tika-core.version>
|
<tika-core.version>2.9.0</tika-core.version>
|
||||||
|
<commons-lang3.version>3.14.0</commons-lang3.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
@@ -78,6 +79,13 @@
|
|||||||
<artifactId>tika-core</artifactId>
|
<artifactId>tika-core</artifactId>
|
||||||
<version>${tika-core.version}</version>
|
<version>${tika-core.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
<version>${commons-lang3.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div>
|
<div>
|
||||||
<button type="button">
|
<button type="button" (click)="sideMenu.open()">
|
||||||
<mat-icon>menu</mat-icon>
|
<mat-icon>menu</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<a [routerLink]="['/home']">
|
<a [routerLink]="['/home']">
|
||||||
@@ -21,3 +21,4 @@
|
|||||||
<a [routerLink]="['/login']">Login</a>
|
<a [routerLink]="['/login']">Login</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
|
<app-side-menu #sideMenu></app-side-menu>
|
||||||
@@ -7,11 +7,10 @@ $headerHeight: 3.5em;
|
|||||||
background-color: #3f51b5;
|
background-color: #3f51b5;
|
||||||
color: white;
|
color: white;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid black;
|
|
||||||
height: $headerHeight;
|
height: $headerHeight;
|
||||||
|
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||||
|
|
||||||
div {
|
div {
|
||||||
border: 1px solid black;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -25,6 +24,22 @@ $headerHeight: 3.5em;
|
|||||||
gap: 1em;
|
gap: 1em;
|
||||||
padding: 0 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 {
|
a {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
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 { RouterModule } from '@angular/router';
|
||||||
import { AuthenticationService } from '../../core/service/authentication.service';
|
import { AuthenticationService } from '../../core/service/authentication.service';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SideMenuComponent } from '../side-menu/side-menu.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-header',
|
selector: 'app-header',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, MatButtonModule, MatIconModule, RouterModule],
|
imports: [CommonModule, MatButtonModule, MatIconModule, RouterModule, SideMenuComponent],
|
||||||
templateUrl: './header.component.html',
|
templateUrl: './header.component.html',
|
||||||
styleUrl: './header.component.scss',
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
frontend/src/app/components/side-menu/side-menu.component.ts
Normal file
23
frontend/src/app/components/side-menu/side-menu.component.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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