Mess commit.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package org.codiki.application.picture;
|
package org.codiki.application.picture;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -48,4 +49,11 @@ public class PictureUseCases {
|
|||||||
public boolean existsById(UUID pictureId) {
|
public boolean existsById(UUID pictureId) {
|
||||||
return picturePort.existsById(pictureId);
|
return picturePort.existsById(pictureId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Picture> getAllOfCurrentUser() {
|
||||||
|
User authenticatedUser = userUseCases.getAuthenticatedUser()
|
||||||
|
.orElseThrow(AuthenticationRequiredException::new);
|
||||||
|
|
||||||
|
return picturePort.findAllByPublisherId(authenticatedUser.id());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package org.codiki.domain.picture.model;
|
package org.codiki.domain.picture.model;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public record Picture(
|
public record Picture(
|
||||||
UUID id,
|
UUID id,
|
||||||
UUID publisherId,
|
UUID publisherId,
|
||||||
|
ZonedDateTime publishedAt,
|
||||||
File contentFile
|
File contentFile
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.codiki.domain.picture.model.builder;
|
package org.codiki.domain.picture.model.builder;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.codiki.domain.picture.model.Picture;
|
import org.codiki.domain.picture.model.Picture;
|
||||||
@@ -9,6 +10,7 @@ import org.codiki.domain.user.model.User;
|
|||||||
public class PictureBuilder {
|
public class PictureBuilder {
|
||||||
private UUID id;
|
private UUID id;
|
||||||
private UUID publisherId;
|
private UUID publisherId;
|
||||||
|
private ZonedDateTime publishedAt;
|
||||||
private File contentFile;
|
private File contentFile;
|
||||||
|
|
||||||
private PictureBuilder() {}
|
private PictureBuilder() {}
|
||||||
@@ -37,12 +39,17 @@ public class PictureBuilder {
|
|||||||
return withPublisherId(publisher.id());
|
return withPublisherId(publisher.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PictureBuilder withPublicationDate(ZonedDateTime publishedAt) {
|
||||||
|
this.publishedAt = publishedAt;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public PictureBuilder withContentFile(File contentFile) {
|
public PictureBuilder withContentFile(File contentFile) {
|
||||||
this.contentFile = contentFile;
|
this.contentFile = contentFile;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Picture build() {
|
public Picture build() {
|
||||||
return new Picture(id, publisherId, contentFile);
|
return new Picture(id, publisherId, publishedAt, contentFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.codiki.domain.picture.port;
|
package org.codiki.domain.picture.port;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -13,4 +14,6 @@ public interface PicturePort {
|
|||||||
void save(Picture picture);
|
void save(Picture picture);
|
||||||
|
|
||||||
void deleteById(UUID pictureId);
|
void deleteById(UUID pictureId);
|
||||||
|
|
||||||
|
List<Picture> findAllByPublisherId(UUID id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.codiki.exposition.picture;
|
package org.codiki.exposition.picture;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||||
@@ -8,6 +9,7 @@ import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
|
|||||||
import org.codiki.application.picture.PictureUseCases;
|
import org.codiki.application.picture.PictureUseCases;
|
||||||
import org.codiki.domain.picture.exception.PictureNotFoundException;
|
import org.codiki.domain.picture.exception.PictureNotFoundException;
|
||||||
import org.codiki.domain.picture.model.Picture;
|
import org.codiki.domain.picture.model.Picture;
|
||||||
|
import org.codiki.exposition.picture.model.PictureDto;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
@@ -44,4 +46,12 @@ public class PictureController {
|
|||||||
.orElseThrow(() -> new PictureNotFoundException(pictureId));
|
.orElseThrow(() -> new PictureNotFoundException(pictureId));
|
||||||
return new FileSystemResource(picture.contentFile());
|
return new FileSystemResource(picture.contentFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/current-user")
|
||||||
|
public List<PictureDto> getAllPicturesOfCurrentUser() {
|
||||||
|
return pictureUseCases.getAllOfCurrentUser()
|
||||||
|
.stream()
|
||||||
|
.map(PictureDto::new)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.codiki.exposition.picture.model;
|
||||||
|
|
||||||
|
import org.codiki.domain.picture.model.Picture;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record PictureDto(
|
||||||
|
UUID id,
|
||||||
|
ZonedDateTime publishedAt
|
||||||
|
) {
|
||||||
|
public PictureDto(Picture picture) {
|
||||||
|
this(picture.id(), picture.publishedAt());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.codiki.infrastructure.picture;
|
package org.codiki.infrastructure.picture;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -66,4 +67,12 @@ public class PictureJpaAdapter implements PicturePort {
|
|||||||
public void deleteById(UUID pictureId) {
|
public void deleteById(UUID pictureId) {
|
||||||
repository.deleteById(pictureId);
|
repository.deleteById(pictureId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Picture> findAllByPublisherId(UUID id) {
|
||||||
|
return repository.findAllByPublisherId(id)
|
||||||
|
.stream()
|
||||||
|
.map(PictureEntity::toDomain)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.codiki.infrastructure.picture.model;
|
package org.codiki.infrastructure.picture.model;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.codiki.domain.picture.model.Picture;
|
import org.codiki.domain.picture.model.Picture;
|
||||||
@@ -24,6 +25,8 @@ public class PictureEntity {
|
|||||||
private UUID id;
|
private UUID id;
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private UUID publisherId;
|
private UUID publisherId;
|
||||||
|
@Column(nullable = false)
|
||||||
|
private ZonedDateTime publishedAt;
|
||||||
|
|
||||||
public PictureEntity(Picture picture) {
|
public PictureEntity(Picture picture) {
|
||||||
id = picture.id();
|
id = picture.id();
|
||||||
@@ -31,6 +34,6 @@ public class PictureEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Picture toDomain() {
|
public Picture toDomain() {
|
||||||
return new Picture(id, publisherId, null);
|
return new Picture(id, publisherId, publishedAt, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package org.codiki.infrastructure.picture.repository;
|
package org.codiki.infrastructure.picture.repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.codiki.domain.picture.model.Picture;
|
||||||
import org.codiki.infrastructure.picture.model.PictureEntity;
|
import org.codiki.infrastructure.picture.model.PictureEntity;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface PictureRepository extends JpaRepository<PictureEntity, UUID> {
|
public interface PictureRepository extends JpaRepository<PictureEntity, UUID> {
|
||||||
|
List<PictureEntity> findAllByPublisherId(UUID id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ CREATE INDEX category_parent_category_id_idx ON category (parent_category_id);
|
|||||||
CREATE TABLE IF NOT EXISTS picture (
|
CREATE TABLE IF NOT EXISTS picture (
|
||||||
id UUID NOT NULL,
|
id UUID NOT NULL,
|
||||||
publisher_id UUID NOT NULL,
|
publisher_id UUID NOT NULL,
|
||||||
|
published_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
CONSTRAINT picture_pk PRIMARY KEY (id),
|
CONSTRAINT picture_pk PRIMARY KEY (id),
|
||||||
CONSTRAINT picture_publisher_id_fk FOREIGN KEY (publisher_id) REFERENCES "user" (id)
|
CONSTRAINT picture_publisher_id_fk FOREIGN KEY (publisher_id) REFERENCES "user" (id)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { ApplicationConfig } from '@angular/core';
|
import { ApplicationConfig } from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
import { provideHttpClient } from '@angular/common/http';
|
import { routes } from './app.routes';
|
||||||
|
import { JwtInterceptor } from './core/interceptor/jwt.interceptor';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideAnimationsAsync(),
|
provideAnimationsAsync(),
|
||||||
provideHttpClient()
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
23
frontend/src/app/core/interceptor/jwt.interceptor.ts
Normal file
23
frontend/src/app/core/interceptor/jwt.interceptor.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
|
||||||
|
import { AuthenticationService } from '../service/authentication.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtInterceptor implements HttpInterceptor {
|
||||||
|
private readonly authenticationService = inject(AuthenticationService);
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
const jwt = this.authenticationService.getToken();
|
||||||
|
|
||||||
|
if (jwt) {
|
||||||
|
const cloned = request.clone({
|
||||||
|
headers: request.headers.set('Authorization', `Bearer ${jwt}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
return next.handle(cloned);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.handle(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Picture {
|
||||||
|
id: string,
|
||||||
|
publishedAt: Date
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { HttpClient, HttpParams } from "@angular/common/http";
|
||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { Picture } from "./model/picture";
|
||||||
|
import { lastValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PictureRestService {
|
||||||
|
private readonly httpClient = inject(HttpClient);
|
||||||
|
|
||||||
|
getAllOfCurrentUser(): Promise<Picture[]> {
|
||||||
|
return lastValueFrom(this.httpClient.get<Picture[]>('/api/pictures/current-user'));
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadPicture(pictureFile: File): Promise<string> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", pictureFile);
|
||||||
|
return lastValueFrom(this.httpClient.post<string>('/api/pictures', formData));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,14 @@ export class AuthenticationService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAuthenticatedUser(): User | undefined {
|
||||||
|
return this.extractUserFromLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken(): string | undefined {
|
||||||
|
return localStorage.getItem(JWT_PARAM) ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private extractUserFromLocalStorage(): User | undefined {
|
private extractUserFromLocalStorage(): User | undefined {
|
||||||
let result: User | undefined = undefined;
|
let result: User | undefined = undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<button type="button" class="close" (click)="closeDialog()">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
<header>
|
||||||
|
<h1>Select an illustration:</h1>
|
||||||
|
</header>
|
||||||
|
<div class="picture-container">
|
||||||
|
@if (isLoading) {
|
||||||
|
<h2>Pictures loading...</h2>
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
|
} @else {
|
||||||
|
@if (pictures.length) {
|
||||||
|
@for(picture of pictures; track picture) {
|
||||||
|
<img src="/api/pictures/{{picture.id}}" (click)="selectPicture(picture)" matTooltip="Choose this illustration"/>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<h2>There is no any picture.</h2>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button type="button" class="secondary" matRipple (click)="closeDialog()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" (click)="fileUpload.click()" matRipple>
|
||||||
|
<mat-icon>upload_file</mat-icon>
|
||||||
|
Add new picture
|
||||||
|
</button>
|
||||||
|
<input type="file" (change)="uploadPicture($event)" #fileUpload/>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1em;
|
||||||
|
gap: 1em;
|
||||||
|
position: relative;
|
||||||
|
max-height: 90vh;
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1em;
|
||||||
|
right: 1em;
|
||||||
|
width: 2.5em;
|
||||||
|
height: 2.5em;
|
||||||
|
border-radius: 10em;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picture-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
max-height: 30em;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 10em;
|
||||||
|
padding: .5em 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 15em;
|
||||||
|
height: 10em;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 1em;
|
||||||
|
opacity: .9;
|
||||||
|
transition: opacity .2s ease-in-out, box-shadow .2s ease-in-out;
|
||||||
|
box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 5px 0 rgba(0,0,0,.32),0 2px 10px 0 rgba(0,0,0,.24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: .8em 1.2em;
|
||||||
|
border-radius: 10em;
|
||||||
|
border: none;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
transition: background-color .2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #5b6ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
color: #3f51b5;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f2f4ff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=file] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Component, inject, OnInit } from "@angular/core";
|
||||||
|
import { Picture } from "../../../core/rest-services/picture/model/picture";
|
||||||
|
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||||
|
import { MatSnackBar } from "@angular/material/snack-bar";
|
||||||
|
import { PictureRestService } from "../../../core/rest-services/picture/picture.rest-service";
|
||||||
|
import { MatIcon } from "@angular/material/icon";
|
||||||
|
import { MatDialogRef } from "@angular/material/dialog";
|
||||||
|
import {MatRippleModule} from '@angular/material/core';
|
||||||
|
import { MatTooltip } from "@angular/material/tooltip";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-picture-selection',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatProgressSpinnerModule, MatIcon, MatRippleModule, MatTooltip],
|
||||||
|
templateUrl: './picture-selection-dialog.component.html',
|
||||||
|
styleUrl: './picture-selection-dialog.component.scss',
|
||||||
|
})
|
||||||
|
export class PictureSelectionDialog implements OnInit {
|
||||||
|
private readonly pictureRestService = inject(PictureRestService);
|
||||||
|
private readonly snackBar = inject(MatSnackBar);
|
||||||
|
private readonly dialogRef = inject(MatDialogRef<PictureSelectionDialog>);
|
||||||
|
|
||||||
|
isLoading: boolean = false;
|
||||||
|
isLoaded: boolean = false;
|
||||||
|
pictures: Picture[] = [];
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.pictureRestService.getAllOfCurrentUser()
|
||||||
|
.then(pictures => {
|
||||||
|
this.pictures = pictures;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
const errorMessage = 'An error occured while loading pictures.';
|
||||||
|
console.error(errorMessage, error);
|
||||||
|
this.snackBar.open(errorMessage, 'Close', { duration: 5000 });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.isLoaded = true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPicture(picture: Picture): void {
|
||||||
|
console.log(picture.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDialog(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadPicture(file: any): void {
|
||||||
|
console.log("uploadFile", file);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { PictureRestService } from "../../../core/rest-services/picture/picture.rest-service";
|
||||||
|
import { MatSnackBar } from "@angular/material/snack-bar";
|
||||||
|
import { MatDialogRef } from "@angular/material/dialog";
|
||||||
|
import { PictureSelectionDialog } from "./picture-selection-dialog.component";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PictureSelectionDialogService {
|
||||||
|
private pictureRestService = inject(PictureRestService);
|
||||||
|
private snackBar = inject(MatSnackBar);
|
||||||
|
private readonly dialogRef = inject(MatDialogRef<PictureSelectionDialog>);
|
||||||
|
|
||||||
|
uploadPicture(pictureFile: File): void {
|
||||||
|
this.pictureRestService.uploadPicture(pictureFile)
|
||||||
|
.then(pictureId => {
|
||||||
|
this.dialogRef.close(pictureId);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
const errorMessage = 'An error occured while uploading a picture...';
|
||||||
|
console.error(errorMessage, error);
|
||||||
|
this.snackBar.open(errorMessage, 'Close', { duration: 5000 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,35 +2,36 @@
|
|||||||
<mat-spinner></mat-spinner>
|
<mat-spinner></mat-spinner>
|
||||||
}
|
}
|
||||||
@else {
|
@else {
|
||||||
<!-- <ng-template #afterLoadingPart> -->
|
|
||||||
@if (publication) {
|
@if (publication) {
|
||||||
<form [formGroup]="publicationEditionForm" (submit)="save()" ngNativeValidate>
|
<form [formGroup]="publicationEditionForm" (submit)="save()" ngNativeValidate>
|
||||||
<header>
|
<header>
|
||||||
<h1>Modification de l'article {{ publication.title }}</h1>
|
<h1>Modification de l'article {{ publication.title }}</h1>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<mat-tab-group dynamicHeight>
|
<mat-tab-group dynamicHeight>
|
||||||
<mat-tab label="Edition">
|
<mat-tab label="Edition">
|
||||||
<div class="form-content">
|
<div class="form-content">
|
||||||
<mat-form-field class="example-form-field">
|
<div class="first-part">
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
<mat-label>Title</mat-label>
|
<mat-label>Title</mat-label>
|
||||||
<input matInput type="text" formControlName="title" />
|
<input matInput type="text" formControlName="title" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
<mat-form-field class="example-form-field">
|
|
||||||
<mat-label>Picture</mat-label>
|
|
||||||
<input matInput type="text" formControlName="illustrationId" />
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field class="example-form-field">
|
|
||||||
<mat-label>Description</mat-label>
|
<mat-label>Description</mat-label>
|
||||||
<input matInput type="text" formControlName="description" />
|
<input matInput type="text" formControlName="description" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img src="/api/pictures/{{publication.illustrationId}}" (click)="displayPictureSectionDialog()" matTooltip="Click to change illustration"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<mat-form-field class="example-form-field">
|
<mat-form-field class="example-form-field">
|
||||||
<mat-label>Content</mat-label>
|
<mat-label>Content</mat-label>
|
||||||
<textarea matInput formControlName="text"></textarea>
|
<textarea matInput formControlName="text" class="text-input"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
@@ -55,9 +56,4 @@
|
|||||||
<h1>Publication failed to load...</h1>
|
<h1>Publication failed to load...</h1>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- <ng-template #loadingFailedMessage>
|
|
||||||
|
|
||||||
</ng-template> -->
|
|
||||||
<!-- </ng-template> -->
|
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,44 @@
|
|||||||
|
|
||||||
.form-content {
|
.form-content {
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
|
padding-bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1em;
|
gap: .5em;
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
textarea {
|
||||||
|
height: 20em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.first-part {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
div {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
flex: 1 0 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:nth-last-child {
|
||||||
|
img {
|
||||||
|
flex: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,11 +7,14 @@ import { MatTabsModule } from '@angular/material/tabs';
|
|||||||
import { debounceTime, map, Observable, Subscription } from 'rxjs';
|
import { debounceTime, map, Observable, Subscription } from 'rxjs';
|
||||||
import { Publication } from '../../core/rest-services/publications/model/publication';
|
import { Publication } from '../../core/rest-services/publications/model/publication';
|
||||||
import { PublicationEditionService } from './publication-edition.service';
|
import { PublicationEditionService } from './publication-edition.service';
|
||||||
|
import {MatDialogModule} from '@angular/material/dialog';
|
||||||
|
import { PictureSelectionDialog } from './picture-selection-dialog/picture-selection-dialog.component';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-publication-edition',
|
selector: 'app-publication-edition',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ReactiveFormsModule, MatInputModule, MatProgressSpinner, MatTabsModule, CommonModule],
|
imports: [ReactiveFormsModule, MatInputModule, MatProgressSpinner, MatTabsModule, MatDialogModule, CommonModule, PictureSelectionDialog, MatTooltipModule],
|
||||||
templateUrl: './publication-edition.component.html',
|
templateUrl: './publication-edition.component.html',
|
||||||
styleUrl: './publication-edition.component.scss',
|
styleUrl: './publication-edition.component.scss',
|
||||||
providers: [PublicationEditionService]
|
providers: [PublicationEditionService]
|
||||||
@@ -85,4 +88,8 @@ export class PublicationEditionComponent implements OnInit, OnDestroy {
|
|||||||
save(): void {
|
save(): void {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayPictureSectionDialog(): void {
|
||||||
|
this.publicationEditionService.displayPictureSectionDialog();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Location } from "@angular/common";
|
import { Location } from "@angular/common";
|
||||||
import { inject, Injectable } from "@angular/core";
|
import { inject, Injectable, OnDestroy } from "@angular/core";
|
||||||
import { MatSnackBar } from "@angular/material/snack-bar";
|
import { MatSnackBar } from "@angular/material/snack-bar";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { BehaviorSubject, Observable } from "rxjs";
|
import { BehaviorSubject, Observable, Subscription } from "rxjs";
|
||||||
import { Publication } from "../../core/rest-services/publications/model/publication";
|
import { Publication } from "../../core/rest-services/publications/model/publication";
|
||||||
import { PublicationRestService } from "../../core/rest-services/publications/publication.rest-service";
|
import { PublicationRestService } from "../../core/rest-services/publications/publication.rest-service";
|
||||||
import { copy } from '../../core/utils/ObjectUtils';
|
import { copy } from '../../core/utils/ObjectUtils';
|
||||||
|
import { MatDialog } from "@angular/material/dialog";
|
||||||
|
import { PictureSelectionDialog } from "./picture-selection-dialog/picture-selection-dialog.component";
|
||||||
|
|
||||||
|
|
||||||
const DEFAULT_PUBLICATION: Publication = {
|
const DEFAULT_PUBLICATION: Publication = {
|
||||||
id: '',
|
id: '',
|
||||||
@@ -25,14 +28,20 @@ const DEFAULT_PUBLICATION: Publication = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PublicationEditionService {
|
export class PublicationEditionService implements OnDestroy {
|
||||||
private activatedRoute = inject(ActivatedRoute);
|
private readonly activatedRoute = inject(ActivatedRoute);
|
||||||
private publicationRestService = inject(PublicationRestService);
|
private readonly publicationRestService = inject(PublicationRestService);
|
||||||
private location = inject(Location);
|
private readonly location = inject(Location);
|
||||||
private snackBar = inject(MatSnackBar);
|
private readonly snackBar = inject(MatSnackBar);
|
||||||
|
private readonly dialog = inject(MatDialog);
|
||||||
|
|
||||||
private isLoadingSubject = new BehaviorSubject<boolean>(false);
|
private isLoadingSubject = new BehaviorSubject<boolean>(false);
|
||||||
private publicationSubject = new BehaviorSubject<Publication>(copy(DEFAULT_PUBLICATION));
|
private publicationSubject = new BehaviorSubject<Publication>(copy(DEFAULT_PUBLICATION));
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
private get _publication(): Publication {
|
private get _publication(): Publication {
|
||||||
return this.publicationSubject.value;
|
return this.publicationSubject.value;
|
||||||
@@ -90,4 +99,22 @@ export class PublicationEditionService {
|
|||||||
publication.text = text;
|
publication.text = text;
|
||||||
this._save(publication);
|
this._save(publication);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editIllustrationId(pictureId: string): void {
|
||||||
|
const publication = this._publication;
|
||||||
|
publication.illustrationId = pictureId
|
||||||
|
this._save(publication);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayPictureSectionDialog(): void {
|
||||||
|
const dialogRef = this.dialog.open(PictureSelectionDialog);
|
||||||
|
|
||||||
|
const afterDialogCloseSubscription = dialogRef.afterClosed()
|
||||||
|
.subscribe(newPictureId => {
|
||||||
|
if (newPictureId) {
|
||||||
|
this.editIllustrationId(newPictureId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.subscriptions.push(afterDialogCloseSubscription);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user