Compare commits

3 Commits

Author SHA1 Message Date
Florian THIERRY
d3041cf03d Styling publications on home page. 2024-06-04 13:55:26 +02:00
Florian THIERRY
58295398e0 Add endpoint to retrieve latest publications. 2024-06-04 13:06:20 +02:00
Florian THIERRY
067bf7885a Update side menu header. 2024-06-04 12:55:46 +02:00
20 changed files with 217 additions and 12 deletions

View File

@@ -3,6 +3,7 @@ package org.codiki.application.publication;
import static java.util.Objects.isNull;
import java.time.Clock;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@@ -190,4 +191,8 @@ public class PublicationUseCases {
return publicationPort.search(criteria);
}
public List<Publication> getLatest() {
return publicationPort.getLatest();
}
}

View File

@@ -15,4 +15,6 @@ public interface PublicationPort {
void delete(Publication publication);
List<Publication> search(List<PublicationSearchCriterion> criteria);
List<Publication> getLatest();
}

View File

@@ -48,6 +48,7 @@ public class SecurityConfiguration {
"/api/pictures/{pictureId}",
"/api/publications/{publicationId}",
"/api/publications",
"/api/publications/latest",
"/error"
).permitAll()
.requestMatchers(

View File

@@ -66,7 +66,7 @@ public class PublicationController {
@GetMapping
public List<PublicationDto> searchPublications(@RequestParam("query") String searchQuery) {
final List<PublicationDto> publications = publicationUseCases.searchPublications(searchQuery)
List<PublicationDto> publications = publicationUseCases.searchPublications(searchQuery)
.stream()
.map(PublicationDto::new)
.toList();
@@ -77,4 +77,18 @@ public class PublicationController {
return publications;
}
@GetMapping("/latest")
public List<PublicationDto> getLatestPublications() {
List<PublicationDto> publications = publicationUseCases.getLatest()
.stream()
.map(PublicationDto::new)
.toList();
if (isEmpty(publications)) {
throw new NoPublicationSearchResultException();
}
return publications;
}
}

View File

@@ -6,6 +6,7 @@ import org.codiki.domain.publication.port.PublicationPort;
import org.codiki.infrastructure.publication.model.PublicationEntity;
import org.codiki.infrastructure.publication.model.PublicationSearchResult;
import org.codiki.infrastructure.publication.repository.PublicationRepository;
import org.springframework.data.domain.Limit;
import org.springframework.stereotype.Component;
import java.util.List;
@@ -56,4 +57,12 @@ public class PublicationJpaAdapter implements PublicationPort {
.map(PublicationSearchResult::getPublication)
.toList();
}
@Override
public List<Publication> getLatest() {
return repository.getLatest(Limit.of(10))
.stream()
.map(PublicationEntity::toDomain)
.toList();
}
}

View File

@@ -1,9 +1,11 @@
package org.codiki.infrastructure.publication.repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.codiki.infrastructure.publication.model.PublicationEntity;
import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -16,4 +18,12 @@ public interface PublicationRepository extends JpaRepository<PublicationEntity,
WHERE p.id = :publicationId
""")
Optional<PublicationEntity> findById(@Param("publicationId") UUID publicationId);
@Query("""
SELECT p
FROM PublicationEntity p
JOIN FETCH p.author a
ORDER BY p.creationDate DESC
""")
List<PublicationEntity> getLatest(Limit limit);
}

View File

@@ -1,7 +1,7 @@
application:
pictures:
path: /Users/florian_thierry/Documents/Developpement/codiki-hexa/pictures-folder/
temp-path : /Users/florian_thierry/Documents/Developpement/codiki-hexa/pictures-folder/temp/
path: /Users/florian_thierry/Documents/Developpement/codiki-hexa/backend/pictures-folder/
temp-path : /Users/florian_thierry/Documents/Developpement/codiki-hexa/backend/pictures-folder/temp/
server:
port: 8987

View File

@@ -0,0 +1,11 @@
meta {
name: Get latest
type: http
seq: 6
}
get {
url: {{url}}/api/publications/latest
body: none
auth: none
}

View File

@@ -2,6 +2,10 @@
"/api": {
"target": "http://localhost:8987",
"secure": false
},
"/pictures": {
"target": "http://localhost:8987/api",
"secure": false
}
}

View File

@@ -33,6 +33,9 @@ $headerHeight: 3.5em;
display: flex;
justify-content: center;
align-items: center;
$buttonSize: 2.5em;
width: $buttonSize;
height: $buttonSize;
&:hover {
cursor: pointer;

View File

@@ -1,9 +1,9 @@
<div class="menu {{ isOpenned ? 'displayed' : '' }}">
<h1>
<span>
<a [routerLink]="['/home']">
<img src="assets/images/codiki.png" alt="logo"/>
Codiki
</span>
</a>
<button type="button" (click)="close()" matTooltip="Close the menu">
<mat-icon>close</mat-icon>
</button>

View File

@@ -26,12 +26,14 @@
align-items: center;
padding: 0 1em;
span {
a {
display: flex;
flex-direction: row;
justify-content: start;
align-items: center;
gap: .5em;
color: white;
text-decoration: none;
img {
$imageSize: 1.2em;

View File

@@ -2,11 +2,12 @@ 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';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-side-menu',
standalone: true,
imports: [CategoriesMenuComponent, MatIconModule, MatTooltipModule],
imports: [CategoriesMenuComponent, MatIconModule, MatTooltipModule, RouterModule],
templateUrl: './side-menu.component.html',
styleUrl: './side-menu.component.scss'
})

View File

@@ -0,0 +1,5 @@
export interface Author {
id: string;
name: string;
image: string;
}

View File

@@ -0,0 +1,14 @@
import { Author } from "./author";
export interface Publication {
id: string;
key: string;
title: string;
text: string;
parsedText: string;
description: string;
creationDate: Date;
illustrationId: string;
categoryId: string;
author: Author;
}

View File

@@ -0,0 +1,15 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { lastValueFrom } from 'rxjs';
import { Publication } from './model/publication';
@Injectable({
providedIn: 'root'
})
export class PublicationRestService {
private httpClient = inject(HttpClient);
getLatest(): Promise<Publication[]> {
return lastValueFrom(this.httpClient.get<Publication[]>('/api/publications/latest'));
}
}

View File

@@ -1 +1,15 @@
<h1>Welcome to Codiki application!</h1>
<h1>Last articles</h1>
<div class="publication-container">
<a *ngFor="let publication of publications$ | async" [routerLink]="['']" class="publication">
<img src="/pictures/{{publication.illustrationId}}"/>
<h1>{{publication.title}}</h1>
<h2>{{publication.description}}</h2>
<div class="footer">
<img src="/pictures/{{publication.author.image}}" [matTooltip]="publication.author.name"/>
Publication posted by {{publication.author.name}}
<span class="publication-date">
({{publication.creationDate}})
</span>
</div>
</a>
</div>

View File

@@ -3,4 +3,46 @@
flex-direction: column;
justify-content: center;
align-items: center;
.publication-container {
display: flex;
flex-direction: column;
gap: 2em;
max-width: 40em;
margin: auto;
.publication {
display: flex;
flex-direction: column;
border-radius: .5em;
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
img {
object-fit: cover;
height: 25em;
border-radius: .5em .5em 0 0;
}
h1 {
font-size: 2.4em;
}
h2 {
font-size: 1.6em;
}
.footer {
display: flex;
flex-direction: row;
align-items: center;
img {
border-radius: 10em;
width: 5em;
height: 5em;
object-fit: cover;
}
}
}
}
}

View File

@@ -1,12 +1,31 @@
import { Component } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { HomeService } from './home.service';
import { Observable } from 'rxjs';
import { Publication } from '../../core/rest-services/publications/model/publication';
import { RouterModule } from '@angular/router';
import { MatTooltipModule } from '@angular/material/tooltip';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-home',
standalone: true,
imports: [],
imports: [CommonModule, RouterModule, MatTooltipModule],
templateUrl: './home.component.html',
styleUrl: './home.component.scss'
styleUrl: './home.component.scss',
providers: [HomeService]
})
export class HomeComponent {
export class HomeComponent implements OnInit {
private homeService = inject(HomeService);
get isLoading$(): Observable<boolean> {
return this.homeService.isLoading$;
}
get publications$(): Observable<Publication[]> {
return this.homeService.publications$;
}
ngOnInit(): void {
this.homeService.startLatestPublicationsRetrieving();
}
}

View File

@@ -0,0 +1,34 @@
import { Injectable, inject } from "@angular/core";
import { PublicationRestService } from "../../core/rest-services/publications/publication.rest-service";
import { BehaviorSubject, Observable } from "rxjs";
import { MatSnackBar } from "@angular/material/snack-bar"
import { Publication } from "../../core/rest-services/publications/model/publication";
@Injectable()
export class HomeService {
private publicationRestService = inject(PublicationRestService);
private snackBar = inject(MatSnackBar);
private publicationsSubject = new BehaviorSubject<Publication[]>([]);
private isLoadingSubject = new BehaviorSubject<boolean>(false);
get isLoading$(): Observable<boolean> {
return this.isLoadingSubject.asObservable();
}
get publications$(): Observable<Publication[]> {
return this.publicationsSubject.asObservable();
}
startLatestPublicationsRetrieving(): void {
this.isLoadingSubject.next(true);
this.publicationRestService.getLatest()
.then(publications => this.publicationsSubject.next(publications))
.catch(error => {
this.snackBar.open('An error occurred while retrieving latest publications...');
console.error('An error occurred while retrieving latest publications...', error);
})
.finally(() => this.isLoadingSubject.next(false));
}
}