Compare commits
7 Commits
0900df463a
...
d324b94ddb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d324b94ddb | ||
|
|
4985889c58 | ||
|
|
7f5d52dce5 | ||
|
|
fae709a254 | ||
|
|
45355f6c42 | ||
|
|
db492b6316 | ||
|
|
c54e1c57d7 |
@@ -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="([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.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) {
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 { 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';
|
||||
|
||||
Reference in New Issue
Block a user