Compare commits
3 Commits
d324b94ddb
...
d3041cf03d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3041cf03d | ||
|
|
58295398e0 | ||
|
|
067bf7885a |
@@ -3,6 +3,7 @@ package org.codiki.application.publication;
|
|||||||
import static java.util.Objects.isNull;
|
import static java.util.Objects.isNull;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -190,4 +191,8 @@ public class PublicationUseCases {
|
|||||||
|
|
||||||
return publicationPort.search(criteria);
|
return publicationPort.search(criteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Publication> getLatest() {
|
||||||
|
return publicationPort.getLatest();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,4 +15,6 @@ public interface PublicationPort {
|
|||||||
void delete(Publication publication);
|
void delete(Publication publication);
|
||||||
|
|
||||||
List<Publication> search(List<PublicationSearchCriterion> criteria);
|
List<Publication> search(List<PublicationSearchCriterion> criteria);
|
||||||
|
|
||||||
|
List<Publication> getLatest();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public class SecurityConfiguration {
|
|||||||
"/api/pictures/{pictureId}",
|
"/api/pictures/{pictureId}",
|
||||||
"/api/publications/{publicationId}",
|
"/api/publications/{publicationId}",
|
||||||
"/api/publications",
|
"/api/publications",
|
||||||
|
"/api/publications/latest",
|
||||||
"/error"
|
"/error"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
.requestMatchers(
|
.requestMatchers(
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public class PublicationController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<PublicationDto> searchPublications(@RequestParam("query") String searchQuery) {
|
public List<PublicationDto> searchPublications(@RequestParam("query") String searchQuery) {
|
||||||
final List<PublicationDto> publications = publicationUseCases.searchPublications(searchQuery)
|
List<PublicationDto> publications = publicationUseCases.searchPublications(searchQuery)
|
||||||
.stream()
|
.stream()
|
||||||
.map(PublicationDto::new)
|
.map(PublicationDto::new)
|
||||||
.toList();
|
.toList();
|
||||||
@@ -77,4 +77,18 @@ public class PublicationController {
|
|||||||
|
|
||||||
return publications;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.codiki.domain.publication.port.PublicationPort;
|
|||||||
import org.codiki.infrastructure.publication.model.PublicationEntity;
|
import org.codiki.infrastructure.publication.model.PublicationEntity;
|
||||||
import org.codiki.infrastructure.publication.model.PublicationSearchResult;
|
import org.codiki.infrastructure.publication.model.PublicationSearchResult;
|
||||||
import org.codiki.infrastructure.publication.repository.PublicationRepository;
|
import org.codiki.infrastructure.publication.repository.PublicationRepository;
|
||||||
|
import org.springframework.data.domain.Limit;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -56,4 +57,12 @@ public class PublicationJpaAdapter implements PublicationPort {
|
|||||||
.map(PublicationSearchResult::getPublication)
|
.map(PublicationSearchResult::getPublication)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Publication> getLatest() {
|
||||||
|
return repository.getLatest(Limit.of(10))
|
||||||
|
.stream()
|
||||||
|
.map(PublicationEntity::toDomain)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package org.codiki.infrastructure.publication.repository;
|
package org.codiki.infrastructure.publication.repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.codiki.infrastructure.publication.model.PublicationEntity;
|
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.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
@@ -16,4 +18,12 @@ public interface PublicationRepository extends JpaRepository<PublicationEntity,
|
|||||||
WHERE p.id = :publicationId
|
WHERE p.id = :publicationId
|
||||||
""")
|
""")
|
||||||
Optional<PublicationEntity> findById(@Param("publicationId") UUID 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
application:
|
application:
|
||||||
pictures:
|
pictures:
|
||||||
path: /Users/florian_thierry/Documents/Developpement/codiki-hexa/pictures-folder/
|
path: /Users/florian_thierry/Documents/Developpement/codiki-hexa/backend/pictures-folder/
|
||||||
temp-path : /Users/florian_thierry/Documents/Developpement/codiki-hexa/pictures-folder/temp/
|
temp-path : /Users/florian_thierry/Documents/Developpement/codiki-hexa/backend/pictures-folder/temp/
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8987
|
port: 8987
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Get latest
|
||||||
|
type: http
|
||||||
|
seq: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{url}}/api/publications/latest
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
"/api": {
|
"/api": {
|
||||||
"target": "http://localhost:8987",
|
"target": "http://localhost:8987",
|
||||||
"secure": false
|
"secure": false
|
||||||
|
},
|
||||||
|
"/pictures": {
|
||||||
|
"target": "http://localhost:8987/api",
|
||||||
|
"secure": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +33,9 @@ $headerHeight: 3.5em;
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
$buttonSize: 2.5em;
|
||||||
|
width: $buttonSize;
|
||||||
|
height: $buttonSize;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<div class="menu {{ isOpenned ? 'displayed' : '' }}">
|
<div class="menu {{ isOpenned ? 'displayed' : '' }}">
|
||||||
<h1>
|
<h1>
|
||||||
<span>
|
<a [routerLink]="['/home']">
|
||||||
<img src="assets/images/codiki.png" alt="logo"/>
|
<img src="assets/images/codiki.png" alt="logo"/>
|
||||||
Codiki
|
Codiki
|
||||||
</span>
|
</a>
|
||||||
<button type="button" (click)="close()" matTooltip="Close the menu">
|
<button type="button" (click)="close()" matTooltip="Close the menu">
|
||||||
<mat-icon>close</mat-icon>
|
<mat-icon>close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -26,12 +26,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
|
|
||||||
span {
|
a {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
$imageSize: 1.2em;
|
$imageSize: 1.2em;
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import {Component} from '@angular/core';
|
|||||||
import {MatIconModule} from '@angular/material/icon';
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
import {CategoriesMenuComponent} from './categories-menu/categories-menu.component';
|
import {CategoriesMenuComponent} from './categories-menu/categories-menu.component';
|
||||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-side-menu',
|
selector: 'app-side-menu',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CategoriesMenuComponent, MatIconModule, MatTooltipModule],
|
imports: [CategoriesMenuComponent, MatIconModule, MatTooltipModule, RouterModule],
|
||||||
templateUrl: './side-menu.component.html',
|
templateUrl: './side-menu.component.html',
|
||||||
styleUrl: './side-menu.component.scss'
|
styleUrl: './side-menu.component.scss'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface Author {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -3,4 +3,46 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [],
|
imports: [CommonModule, RouterModule, MatTooltipModule],
|
||||||
templateUrl: './home.component.html',
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
frontend/src/app/pages/home/home.service.ts
Normal file
34
frontend/src/app/pages/home/home.service.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user