1
0
Fork 0
mirror of https://codeberg.org/beerbrawl/beerbrawl.git synced 2024-09-23 01:30:52 +02:00

feat(#35): link frontend and backend

This commit is contained in:
MohammedKevin 2024-06-13 16:59:35 +02:00
parent 66f58f317e
commit a84f9059df
15 changed files with 239 additions and 138 deletions

View file

@ -3,13 +3,16 @@ package at.ac.tuwien.sepr.groupphase.backend.datagenerator;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentUpdateQualificationMatchDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentUpdateQualificationMatchDto.DrinksPickupDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.TournamentUpdateQualificationMatchDto.ScoreUpdateDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.SharedMediaCreateDto;
import at.ac.tuwien.sepr.groupphase.backend.entity.ApplicationUser;
import at.ac.tuwien.sepr.groupphase.backend.entity.BeerPongTable;
import at.ac.tuwien.sepr.groupphase.backend.entity.SharedMedia;
import at.ac.tuwien.sepr.groupphase.backend.entity.Team;
import at.ac.tuwien.sepr.groupphase.backend.entity.Tournament;
import at.ac.tuwien.sepr.groupphase.backend.entity.domainservice.MatchDomainService;
import at.ac.tuwien.sepr.groupphase.backend.repository.BeerPongTableRepository;
import at.ac.tuwien.sepr.groupphase.backend.repository.QualificationParticipationRepository;
import at.ac.tuwien.sepr.groupphase.backend.repository.SharedMediaRepository;
import at.ac.tuwien.sepr.groupphase.backend.repository.TeamRepository;
import at.ac.tuwien.sepr.groupphase.backend.repository.TournamentRepository;
import at.ac.tuwien.sepr.groupphase.backend.repository.UserRepository;
@ -23,10 +26,14 @@ import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
@ -50,6 +57,7 @@ public class TestDataGenerator {
private final TournamentQualificationService qualificationService;
private final TournamentKoPhaseService koPhaseService;
private final QualificationParticipationRepository qualificationParticipationRepository;
private final SharedMediaRepository sharedMediaRepository;
public void generateTestUser() {
userRepository.deleteAll();
@ -103,6 +111,48 @@ public class TestDataGenerator {
.toList();
beerPongTableRepository.saveAllAndFlush(tables3);
matchDomainService.scheduleQualiMatches(tournament3.getId());
List<Tournament> tournaments = new ArrayList<>();
tournaments.add(tournament);
tournaments.add(tournament2);
tournaments.add(tournament3);
addTestImagesToTournaments(tournaments);
}
private void addTestImagesToTournaments(List<Tournament> tournaments) {
tournaments.forEach(tournament -> {
try {
uploadImageToTournament(tournament, "John Smith", "World Chess Championship", "testimage.png");
uploadImageToTournament(tournament, "Emily Johnson", "Grand Slam Tennis Tournament", "testimage.png");
uploadImageToTournament(tournament, "Michael Brown", "Olympic Games 2024", "testimage.png");
uploadImageToTournament(tournament, "Jessica Martinez", "FIFA World Cup", "testimage.png");
uploadImageToTournament(tournament, "David Garcia", "NBA Finals", "testimage.png");
uploadImageToTournament(tournament, "Jane Doe", "Super Bowl", "testimage.png");
uploadImageToTournament(tournament, "Andrew Wilson", "Rugby World Cup", "testimage.png");
uploadImageToTournament(tournament, "Sophia Lee", "Australian Open", "testimage.png");
uploadImageToTournament(tournament, "Ethan Clark", "UEFA Champions League", "testimage.png");
uploadImageToTournament(tournament, "Olivia Scott", "Wimbledon", "testimage.png");
} catch (IOException e) {
LOGGER.error("Failed to upload test images to tournament {}", tournament.getName(), e);
}
});
}
private void uploadImageToTournament(Tournament tournament, String author, String title, String imagePath) throws IOException {
SharedMediaCreateDto data = new SharedMediaCreateDto();
data.setAuthor(author);
data.setTitle(title);
data.setTournamentId(tournament.getId());
byte[] imageBytes = Files.readAllBytes(new ClassPathResource(imagePath).getFile().toPath());
SharedMedia sharedMedia = new SharedMedia();
sharedMedia.setAuthor(author);
sharedMedia.setTitle(title);
sharedMedia.setImage(imageBytes);
sharedMedia.setTournament(tournament);
sharedMediaRepository.saveAndFlush(sharedMedia);
}
private void generateTestTournamentsWithFinishedQualificationAndStartedKoPhase() {

View file

@ -8,6 +8,7 @@ import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.SharedMediaMetadataDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.mapper.SharedMediaMapper;
import at.ac.tuwien.sepr.groupphase.backend.exception.NotFoundException;
import at.ac.tuwien.sepr.groupphase.backend.service.SharedMediaService;
import jakarta.annotation.security.PermitAll;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -39,6 +40,7 @@ public class SharedMediaEndpoint {
private final SharedMediaMapper sharedMediaMapper;
//public for upload of images
@PermitAll
@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public SharedMediaMetadataDto createSharedMedia(

View file

@ -1,18 +1,9 @@
package at.ac.tuwien.sepr.groupphase.backend.endpoint.dto;
import at.ac.tuwien.sepr.groupphase.backend.entity.SharedMedia;
public record SharedMediaMetadataDto(
Long id,
String author,
String title,
Long tournamentId) {
public static SharedMediaMetadataDto fromSharedMedia(SharedMedia e) {
return new SharedMediaMetadataDto(
e.getId(),
e.getAuthor(),
e.getTitle(),
e.getTournament() != null ? e.getTournament().getId() : null);
}
}

View file

@ -10,6 +10,7 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@ -17,15 +18,20 @@ import lombok.Setter;
@Getter
@Setter
public class SharedMedia {
private static final int MAX_IMAGE_SIZE = 2 * 1024 * 1024; // 2MB
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Size(max = 30, message = "Author can't be more than 30 characters long.")
private String author;
@Size(max = 50, message = "Title can't be more than 50 characters long.")
private String title;
@Lob
@Column(columnDefinition = "BLOB")
@Size(max = SharedMedia.MAX_IMAGE_SIZE)
private byte[] image;
@ManyToOne(fetch = FetchType.LAZY)

View file

@ -16,7 +16,7 @@ spring:
# Set this property to true if you want to see the executed queries
show-sql: false
hibernate:
ddl-auto: update
ddl-auto: create-drop
# Allows to fetch lazy properties outside of the original transaction. Although this sounds helpful, the property
# is disabled since it breaks the principle of least astonishment and leads to bad performance. To learn more,
# follow this link: https://bit.ly/2LaX9ku

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

View file

@ -13,6 +13,7 @@ import at.ac.tuwien.sepr.groupphase.backend.repository.TeamRepository;
import at.ac.tuwien.sepr.groupphase.backend.repository.TournamentRepository;
import at.ac.tuwien.sepr.groupphase.backend.repository.UserRepository;
import at.ac.tuwien.sepr.groupphase.backend.service.TournamentKoPhaseService;
import at.ac.tuwien.sepr.groupphase.backend.repository.*;
import at.ac.tuwien.sepr.groupphase.backend.service.TournamentQualificationService;
import at.ac.tuwien.sepr.groupphase.backend.service.TournamentService;
import at.ac.tuwien.sepr.groupphase.backend.service.TournamentTeamService;
@ -57,6 +58,7 @@ public class TestData {
@Autowired
private QualificationParticipationRepository qualificationParticipationRepository;
private SharedMediaRepository sharedMediaRepository;
protected String BASE_URI = "/api/v1";
protected String TOURNAMENT_BASE_URI = BASE_URI + "/tournaments";
@ -279,7 +281,8 @@ public class TestData {
beerPongTableRepository,
matchDomainService,
tournamentQualificationService, this.qualificationService, this.koPhaseService,
this.qualificationParticipationRepository);
this.qualificationParticipationRepository,
sharedMediaRepository);
dataGenerator.generateTestUser();
dataGenerator.generateTestTournaments();
}

View file

@ -1,20 +1,25 @@
<mat-card class="party-picture-card">
<div class="image-container">
<img class="images" src="{{picture?.source}}" alt="party pic" />
<div class="image-container">
<img class="images" src="{{ imageUrl }}" alt="party pic" />
</div>
<div class="middle-bar">
<p>Author: {{ picture?.author }}</p>
<p>{{ picture?.title }}</p>
</div>
<div class="bottom-bar">
<h5>11:00</h5>
<div>
<button
mat-icon-button
aria-label="Approve picture"
matTooltip="Approve picture"
(click)="openConfirmDialog()"
>
<mat-icon>check</mat-icon>
</button>
<button mat-icon-button aria-label="Reject picture" matTooltip="Reject picture">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="middle-bar">
<p>Author: {{picture?.author}}</p>
<p>{{picture?.title}}</p>
</div>
<div class="bottom-bar">
<h5>11:00</h5>
<div>
<button mat-icon-button aria-label="Approve picture" matTooltip="Approve picture" (click)="openConfirmDialog()">
<mat-icon>check</mat-icon>
</button>
<button mat-icon-button aria-label="Reject picture" matTooltip="Reject picture">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</mat-card>
</div>
</mat-card>

View file

@ -1,57 +1,56 @@
.party-picture-card {
width: 20rem;
padding-left: 2rem;
padding-right: 2rem;
padding-top: 2rem;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.2);
margin: 1rem;
display: flex;
border-radius: 0.5rem;
overflow: hidden;
justify-content: center;
align-items: center;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 20rem;
padding-left: 2rem;
padding-right: 2rem;
padding-top: 2rem;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.2);
margin: 1rem;
display: flex;
border-radius: 0.5rem;
overflow: hidden;
justify-content: center;
align-items: center;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.image-container {
width: 100%;
padding: 1rem;
height: 20rem; /* Fixed height for the image container */
background-color: rgb(235, 235, 235); /* Grey background */
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 1rem;
height: 20rem; /* Fixed height for the image container */
background-color: rgb(235, 235, 235); /* Grey background */
display: flex;
justify-content: center;
align-items: center;
}
.images {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
display: block;
object-fit: contain; /* Ensure the image is contained within the container */
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
display: block;
object-fit: contain; /* Ensure the image is contained within the container */
}
.bottom-bar{
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.bottom-bar {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.middle-bar{
width: 100%;
.middle-bar {
width: 100%;
}
h5 {
color: grey
color: grey;
}
p {
color: rgb(80, 80, 80)
color: rgb(80, 80, 80);
}

View file

@ -8,10 +8,9 @@ describe('PartyPictureCardComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PartyPictureCardComponent]
})
.compileComponents();
imports: [PartyPictureCardComponent],
}).compileComponents();
fixture = TestBed.createComponent(PartyPictureCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();

View file

@ -1,27 +1,62 @@
import { Component, Input } from '@angular/core';
import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { MatCard } from '@angular/material/card';
import { MatIcon } from '@angular/material/icon';
import { ConfirmationService } from '../../services/confirmation.service';
import { MatIconButton } from '@angular/material/button';
import { MatTooltip } from '@angular/material/tooltip';
interface PictureDTO {
source: string;
author: string;
title: string;
}
import { SharedMediaMetadataDto } from '../../../../openapi-generated';
import { SharedMediaEndpointService } from '@api';
import { ConfirmationService } from '../../services/confirmation.service';
@Component({
selector: 'app-party-picture-card',
standalone: true,
imports: [MatCard, MatIcon, MatIconButton, MatTooltip],
templateUrl: './party-picture-card.component.html',
styleUrl: './party-picture-card.component.scss',
styleUrls: ['./party-picture-card.component.scss'],
})
export class PartyPictureCardComponent {
@Input() picture: PictureDTO | undefined;
export class PartyPictureCardComponent implements OnInit, OnDestroy {
@Input() picture: SharedMediaMetadataDto | undefined;
imageUrl: string | undefined;
constructor(private confirmationService: ConfirmationService) {}
private observer: IntersectionObserver | undefined;
constructor(
private confirmationService: ConfirmationService,
private sharedMediaService: SharedMediaEndpointService,
private element: ElementRef,
) {}
ngOnInit(): void {
this.observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting && this.picture && this.picture.id) {
this.loadImage(this.picture.id);
this.observer?.unobserve(this.element.nativeElement);
}
});
});
this.observer.observe(this.element.nativeElement);
}
ngOnDestroy(): void {
if (this.observer) {
this.observer.disconnect();
}
}
loadImage(sharedMediaId: number): void {
this.sharedMediaService.getSharedMediaImage(sharedMediaId).subscribe(
(response: any) => {
// Create a blob URL from the received Blob
this.imageUrl = URL.createObjectURL(response);
},
(error: any) => {
// Handle errors
console.error('Error fetching shared media image:', error);
},
);
}
async openConfirmDialog(): Promise<void> {
if (await this.confirmationService.openConfirmationDialog('approve picture')) {

View file

@ -1,9 +1,9 @@
<app-header-card [title]="'Approve Party Pics'">
<button mat-raised-button color="primary" [routerLink]="'..'">Back to Overview</button>
<button mat-raised-button color="primary" [routerLink]="'..'">Back to Overview</button>
</app-header-card>
<div class="picture-grid">
@for (picture of getPictures(); track picture) {
<app-party-picture-card [picture]="picture"></app-party-picture-card>
}
@for (picture of getPictures(); track picture) {
<app-party-picture-card [picture]="picture"></app-party-picture-card>
}
</div>

View file

@ -1,22 +1,21 @@
.picture-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
gap: 1rem;
width: 100%;
padding: 1rem;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
gap: 1rem;
width: 100%;
padding: 1rem;
box-sizing: border-box;
}
.party-picture-card {
width: 100%;
padding: 2rem;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.2);
margin: 0;
display: flex;
border-radius: 0.5rem;
overflow: hidden;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
padding: 2rem;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.2);
margin: 0;
display: flex;
border-radius: 0.5rem;
overflow: hidden;
justify-content: center;
align-items: center;
position: relative;
}

View file

@ -8,10 +8,9 @@ describe('PartyPicsApproveComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PartyPicsApproveComponent]
})
.compileComponents();
imports: [PartyPicsApproveComponent],
}).compileComponents();
fixture = TestBed.createComponent(PartyPicsApproveComponent);
component = fixture.componentInstance;
fixture.detectChanges();

View file

@ -1,43 +1,56 @@
import { Component, input } from '@angular/core';
import { HeaderCardComponent } from '../header-card/header-card.component';
import { RouterModule } from '@angular/router';
import { Component, OnInit, Input, input } from '@angular/core';
import { MatButton } from '@angular/material/button';
import { MatCard } from '@angular/material/card';
import { HeaderCardComponent } from '../header-card/header-card.component';
import { PartyPictureCardComponent } from '../party-picture-card/party-picture-card.component';
interface PictureDTO {
source: string;
author: string;
title: string;
}
const pics = [
{
source: 'https://picsum.photos/200/300',
author: 'John Smith',
title: 'Sunset Over the Mountains',
},
{ source: 'https://picsum.photos/300/200', author: 'Jane Doe', title: 'Cityscape at Dusk' },
{ source: 'https://picsum.photos/300/300', author: 'Alice Johnson', title: 'Forest Pathway' },
{ source: 'https://picsum.photos/200/300', author: 'Robert Brown', title: 'Ocean Waves' },
{ source: 'https://picsum.photos/500/200', author: 'Emily Davis', title: 'Desert Mirage' },
{ source: 'https://picsum.photos/500/300', author: 'Michael Wilson', title: 'Winter Wonderland' },
{ source: 'https://picsum.photos/500/300', author: 'Sarah Thompson', title: 'Spring Blossoms' },
{ source: 'https://picsum.photos/300/200', author: 'David Martinez', title: 'Autumn Leaves' },
{ source: 'https://picsum.photos/300/300', author: 'Laura Taylor', title: 'Night Sky' },
];
import { SharedMediaEndpointService, SharedMediaMetadataDto } from '@api';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-party-pics-approve',
standalone: true,
imports: [MatButton, MatCard, HeaderCardComponent, RouterModule, PartyPictureCardComponent],
templateUrl: './party-pics-approve.component.html',
styleUrl: './party-pics-approve.component.scss',
styleUrls: ['./party-pics-approve.component.scss'],
})
export class PartyPicsApproveComponent {
export class PartyPicsApproveComponent implements OnInit {
tournamentId = input.required<number>();
pictures: SharedMediaMetadataDto[] = [];
getPictures(): PictureDTO[] {
return pics;
constructor(
private sharedMediaService: SharedMediaEndpointService,
private snackBar: MatSnackBar,
) {}
getPictures(): SharedMediaMetadataDto[] {
return this.pictures;
}
ngOnInit(): void {
this.fetchPictures();
}
fetchPictures(): void {
this.sharedMediaService.getSharedMediaByTournament(this.tournamentId()).subscribe({
next: (data: SharedMediaMetadataDto[]) => {
this.pictures = data;
},
error: error => {
this.defaultServiceErrorHandling(error);
},
});
}
private defaultServiceErrorHandling(error: any): void {
let errorMessage = '';
if (typeof error.error === 'object') {
errorMessage = error.error.error;
} else {
errorMessage = error.error;
}
this.snackBar.open(errorMessage, 'OK', {
duration: 5000,
});
}
}