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

Merge branch 'feat/#37/public-picture-gallery' into 'development'

Feat/#37/public picture gallery

See merge request 2024ss-se-pr-group/24ss-se-pr-qse-11!125
This commit is contained in:
Moritz Kepplinger 2024-06-25 22:32:09 +00:00
commit f381cd30d6
15 changed files with 545 additions and 353 deletions

View file

@ -28,7 +28,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.List;

View file

@ -7,6 +7,7 @@ import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.SharedMediaCreateDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.SharedMediaMetadataDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.SharedMediaUpdateStateDto;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.mapper.SharedMediaMapper;
import at.ac.tuwien.sepr.groupphase.backend.entity.SharedMedia;
import at.ac.tuwien.sepr.groupphase.backend.exception.NotFoundException;
import at.ac.tuwien.sepr.groupphase.backend.service.SharedMediaService;
import jakarta.annotation.security.PermitAll;
@ -60,7 +61,16 @@ public class SharedMediaEndpoint {
@GetMapping(value = "/tournament/{tournamentId}", produces = "application/json")
public ResponseEntity<List<SharedMediaMetadataDto>> getSharedMediaByTournament(@PathVariable(name = "tournamentId") Long tournamentId) {
LOG.info("GET {}/tournament/{}", BASE_ENDPOINT, tournamentId);
var sharedMediaMetadataDtos = sharedMediaService.findAllByTournamentIdWithoutImage(tournamentId);
var sharedMediaMetadataDtos = sharedMediaService.findAllByTournamentIdWithoutImage(tournamentId, false);
return ResponseEntity.ok(sharedMediaMetadataDtos);
}
@PermitAll
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "/tournament/public/{tournamentId}", produces = "application/json")
public ResponseEntity<List<SharedMediaMetadataDto>> getPublicSharedMediaByTournament(@PathVariable(name = "tournamentId") Long tournamentId) {
LOG.info("GET {}/tournament/public/{}", BASE_ENDPOINT, tournamentId);
var sharedMediaMetadataDtos = sharedMediaService.findAllByTournamentIdWithoutImage(tournamentId, true);
return ResponseEntity.ok(sharedMediaMetadataDtos);
}
@ -80,6 +90,7 @@ public class SharedMediaEndpoint {
}
}
@Secured("ROLE_USER")
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "/image/{sharedMediaId}", produces = MediaType.IMAGE_JPEG_VALUE)
@ -96,6 +107,27 @@ public class SharedMediaEndpoint {
}
}
@PermitAll
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "/image/public/{sharedMediaId}", produces = MediaType.IMAGE_JPEG_VALUE)
public ResponseEntity<byte[]> getPublicSharedMediaImage(@PathVariable(name = "sharedMediaId") Long sharedMediaId) {
LOG.info("GET {}/image/public/{}", BASE_ENDPOINT, sharedMediaId);
try {
var image = sharedMediaService.findOne(sharedMediaId);
if (image.getState() != SharedMedia.MediaState.APPROVED) {
throw new AccessDeniedException("Image is not public");
}
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + sharedMediaId + ".jpg\"")
.body(image.getImage());
} catch (NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
} catch (AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
}
@Secured("ROLE_USER")
@ResponseStatus(HttpStatus.OK)
@PutMapping("/{sharedMediaId}")

View file

@ -22,6 +22,17 @@ public interface SharedMediaRepository extends JpaRepository<SharedMedia, Long>
+ "FROM SharedMedia sm WHERE sm.tournament.id = :tournamentId")
List<SharedMediaMetadataDto> findAllByTournamentIdWithoutImage(@Param("tournamentId") Long tournamentId);
/**
* Find all shared media by a specific tournament id without the image field.
*
* @param tournamentId The tournament id
* @return List of shared media entries for the given tournament without the image field
*/
@Query("SELECT new at.ac.tuwien.sepr.groupphase.backend.endpoint.dto.SharedMediaMetadataDto(sm.id, sm.author, sm.title, sm.state, sm.tournament.id) "
+ "FROM SharedMedia sm WHERE sm.tournament.id = :tournamentId AND sm.state = 'APPROVED'")
List<SharedMediaMetadataDto> findAllPublicByTournamentIdWithoutImage(@Param("tournamentId") Long tournamentId);
/**

View file

@ -18,7 +18,7 @@ public interface SharedMediaService {
* @param tournamentId The ID of the tournament
* @return List of shared media entries for the given tournament
*/
List<SharedMediaMetadataDto> findAllByTournamentIdWithoutImage(Long tournamentId);
List<SharedMediaMetadataDto> findAllByTournamentIdWithoutImage(Long tournamentId, boolean onlyApproved);
/**
* Create a shared media entry.

View file

@ -22,7 +22,6 @@ import java.util.List;
public class SharedMediaServiceImpl implements SharedMediaService {
private final SharedMediaRepository sharedMediaRepository;
private final TournamentRepository tournamentRepository;
@ -33,8 +32,12 @@ public class SharedMediaServiceImpl implements SharedMediaService {
}
@Override
public List<SharedMediaMetadataDto> findAllByTournamentIdWithoutImage(Long tournamentId) {
return sharedMediaRepository.findAllByTournamentIdWithoutImage(tournamentId);
public List<SharedMediaMetadataDto> findAllByTournamentIdWithoutImage(Long tournamentId, boolean onlyApproved) {
if (onlyApproved) {
return sharedMediaRepository.findAllPublicByTournamentIdWithoutImage(tournamentId);
} else {
return sharedMediaRepository.findAllByTournamentIdWithoutImage(tournamentId);
}
}
public SharedMedia create(SharedMediaCreateDto sharedMediaCreateDto, MultipartFile image) throws NotFoundException {

341
e2e/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -263,6 +263,132 @@ export class SharedMediaEndpointService {
);
}
/**
* @param tournamentId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public getPublicSharedMediaByTournament(tournamentId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<Array<SharedMediaMetadataDto>>;
public getPublicSharedMediaByTournament(tournamentId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<Array<SharedMediaMetadataDto>>>;
public getPublicSharedMediaByTournament(tournamentId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<Array<SharedMediaMetadataDto>>>;
public getPublicSharedMediaByTournament(tournamentId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<any> {
if (tournamentId === null || tournamentId === undefined) {
throw new Error('Required parameter tournamentId was null or undefined when calling getPublicSharedMediaByTournament.');
}
let localVarHeaders = this.defaultHeaders;
let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (localVarHttpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (localVarHttpHeaderAcceptSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
}
let localVarHttpContext: HttpContext | undefined = options && options.context;
if (localVarHttpContext === undefined) {
localVarHttpContext = new HttpContext();
}
let localVarTransferCache: boolean | undefined = options && options.transferCache;
if (localVarTransferCache === undefined) {
localVarTransferCache = true;
}
let responseType_: 'text' | 'json' | 'blob' = 'json';
if (localVarHttpHeaderAcceptSelected) {
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
responseType_ = 'json';
} else {
responseType_ = 'blob';
}
}
let localVarPath = `/api/v1/shared-media/tournament/public/${this.configuration.encodeParam({name: "tournamentId", value: tournamentId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`;
return this.httpClient.request<Array<SharedMediaMetadataDto>>('get', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
transferCache: localVarTransferCache,
reportProgress: reportProgress
}
);
}
/**
* @param sharedMediaId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public getPublicSharedMediaImage(sharedMediaId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'image/jpeg', context?: HttpContext, transferCache?: boolean}): Observable<Array<string>>;
public getPublicSharedMediaImage(sharedMediaId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'image/jpeg', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<Array<string>>>;
public getPublicSharedMediaImage(sharedMediaId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'image/jpeg', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<Array<string>>>;
public getPublicSharedMediaImage(sharedMediaId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'image/jpeg', context?: HttpContext, transferCache?: boolean}): Observable<any> {
if (sharedMediaId === null || sharedMediaId === undefined) {
throw new Error('Required parameter sharedMediaId was null or undefined when calling getPublicSharedMediaImage.');
}
let localVarHeaders = this.defaultHeaders;
let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (localVarHttpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'image/jpeg'
];
localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (localVarHttpHeaderAcceptSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
}
let localVarHttpContext: HttpContext | undefined = options && options.context;
if (localVarHttpContext === undefined) {
localVarHttpContext = new HttpContext();
}
let localVarTransferCache: boolean | undefined = options && options.transferCache;
if (localVarTransferCache === undefined) {
localVarTransferCache = true;
}
let responseType_: 'text' | 'json' | 'blob' = 'json';
if (localVarHttpHeaderAcceptSelected) {
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
responseType_ = 'json';
} else {
responseType_ = 'blob';
}
}
let localVarPath = `/api/v1/shared-media/image/public/${this.configuration.encodeParam({name: "sharedMediaId", value: sharedMediaId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`;
return this.httpClient.request<Array<string>>('get', `${this.configuration.basePath}${localVarPath}`,
{
context: localVarHttpContext,
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: localVarHeaders,
observe: observe,
transferCache: localVarTransferCache,
reportProgress: reportProgress
}
);
}
/**
* @param tournamentId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.

View file

@ -25,6 +25,7 @@
"@popperjs/core": "2.11.8",
"core-js": "3.36.1",
"jwt-decode": "4.0.0",
"ng-qrcode": "^18.0.0",
"puppeteer": "^22.11.2",
"replace-in-files-cli": "^2.2.0",
"rxjs": "7.8.1",
@ -7586,7 +7587,6 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true,
"engines": {
"node": ">=6"
}
@ -8972,6 +8972,11 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -9156,6 +9161,11 @@
"node": ">= 4"
}
},
"node_modules/encode-utf8": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz",
"integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw=="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@ -10593,7 +10603,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
@ -13236,7 +13245,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"dependencies": {
"p-locate": "^4.1.0"
},
@ -14063,6 +14071,19 @@
"node": ">= 0.4.0"
}
},
"node_modules/ng-qrcode": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/ng-qrcode/-/ng-qrcode-18.0.0.tgz",
"integrity": "sha512-qmtU6n8lxyTGxrtRbzXYPR7JNGwQ6SZXqnEUQksjmFsPLssZRchal9zlw4eUMPm/bRxy+hSV6dWe/59mTNtc8A==",
"dependencies": {
"qrcode": "^1.5.3",
"tslib": "^2.6.2"
},
"peerDependencies": {
"@angular/common": ">=18 <19",
"@angular/core": ">=18 <19"
}
},
"node_modules/nice-napi": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
@ -15105,7 +15126,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"dependencies": {
"p-try": "^2.0.0"
},
@ -15120,7 +15140,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"dependencies": {
"p-limit": "^2.2.0"
},
@ -15173,7 +15192,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"engines": {
"node": ">=6"
}
@ -15562,6 +15580,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@ -15954,6 +15980,79 @@
"node": ">=0.9"
}
},
"node_modules/qrcode": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz",
"integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==",
"dependencies": {
"dijkstrajs": "^1.0.1",
"encode-utf8": "^1.0.3",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/qrcode/node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/qrcode/node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
"node_modules/qrcode/node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
@ -16463,8 +16562,7 @@
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"node_modules/requires-port": {
"version": "1.0.0",
@ -17034,8 +17132,7 @@
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/set-function-length": {
"version": "1.2.2",
@ -19552,8 +19649,7 @@
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"dev": true
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
},
"node_modules/which-typed-array": {
"version": "1.1.15",
@ -19584,7 +19680,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@ -19649,7 +19744,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@ -19664,7 +19758,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@ -19675,8 +19768,7 @@
"node_modules/wrap-ansi/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/wrappy": {
"version": "1.0.2",

View file

@ -34,6 +34,7 @@
"@popperjs/core": "2.11.8",
"core-js": "3.36.1",
"jwt-decode": "4.0.0",
"ng-qrcode": "^18.0.0",
"puppeteer": "^22.11.2",
"replace-in-files-cli": "^2.2.0",
"rxjs": "7.8.1",

View file

@ -21,6 +21,7 @@ import { TournamentPublicViewComponent } from './components/tournament/tournamen
import { InfoscreenKnockoutPhaseTreeComponent } from './components/infoscreen-knockout-phase-tree/infoscreen-knockout-phase-tree.component';
import { EditKnockoutPhaseTreeComponent } from './components/tournament/edit-knockout-phase-tree/edit-knockout-phase-tree.component';
import { PartyPicsApproveComponent } from './components/party-pictures-approve/party-pics-approve.component';
import { TournamentPublicGalleryComponent } from './components/tournament/tournament-public-gallery/tournament-public-gallery.component';
import { ImageUploadComponent } from './components/image-upload/image-upload.component';
const routes: Routes = [
@ -84,6 +85,7 @@ const routes: Routes = [
{ path: 'tournaments/:tournamentId/signup', component: TeamSignupComponent },
{ path: 'tournaments/:tournamentId/upload-image', component: ImageUploadComponent },
{ path: 'tournaments/:tournamentId/live', component: TournamentPublicViewComponent },
{ path: 'tournaments/:tournamentId/public-gallery', component: TournamentPublicGalleryComponent },
{
path: 'infoscreen/:tournamentId/ko-phase',
component: InfoscreenKnockoutPhaseTreeComponent,

View file

@ -37,13 +37,13 @@
<mat-form-field class="full-width" [appearance]="'outline'">
<mat-label>Author</mat-label>
<input matInput [(ngModel)]="author" maxlength="50" />
<mat-hint align="end">{{ author?.length || 0 }}/50</mat-hint>
<input matInput [(ngModel)]="author" maxlength="30" />
<mat-hint align="end">{{ author?.length || 0 }}/30</mat-hint>
</mat-form-field>
<mat-form-field class="full-width" [appearance]="'outline'">
<mat-label>Title</mat-label>
<input matInput [(ngModel)]="title" maxlength="100" />
<mat-hint align="end">{{ title?.length || 0 }}/100</mat-hint>
<input matInput [(ngModel)]="title" maxlength="50" />
<mat-hint align="end">{{ title?.length || 0 }}/50</mat-hint>
</mat-form-field>
<div class="buttons">

View file

@ -7,8 +7,7 @@
<p>{{ picture?.title }}</p>
</div>
<div class="bottom-bar">
<h5>11:00</h5>
<h5></h5>
<div>
@if (picture?.state === 'PENDING' || picture?.state === 'REJECTED') {
<button

View file

@ -0,0 +1,27 @@
<div class="cards-container">
@for (card of cards; track card) {
@if (card?.image !== 'src') {
<mat-card
class="image-card"
[@cardAnimation]="card.animationState"
(@cardAnimation.done)="onAnimationEnd($event, card)"
>
<mat-card-header>
<mat-card-title>{{ card.author }}</mat-card-title>
</mat-card-header>
<img [src]="card.image" [alt]="card.alt" class="behave" />
<mat-card-footer class="centered">
<div>{{ card.title }}</div>
</mat-card-footer>
</mat-card>
}
}
</div>
@if (showQrCode && checkUploadLinkString()) {
<div class="qr-code-container">
<div class="centered">
<qr-code value="{{ linkToUpload }}" size="500" />
<div>Scan and contribute!</div>
</div>
</div>
}

View file

@ -0,0 +1,46 @@
.cards-container {
display: grid;
grid-template-columns: repeat(4, 25vw);
grid-template-rows: repeat(2, 50vh);
}
.card-row {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}
.image-card {
padding: 1rem;
margin: 1rem;
}
.behave {
object-fit: contain;
object-position: center;
margin: 10px;
height: calc(100% - 5rem);
max-width: calc(100% - 2rem);
}
.qr-code-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
justify-content: center;
font-weight: bold;
font-size: xx-large;
}
.centered {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-weight: bold;
}

View file

@ -0,0 +1,161 @@
import { Component, OnInit, inject, input } from '@angular/core';
import {
CommonModule,
Location,
LocationStrategy,
NgOptimizedImage,
PathLocationStrategy,
} from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { QrCodeModule } from 'ng-qrcode';
import { SharedMediaEndpointService, SharedMediaMetadataDto } from '@api';
@Component({
selector: 'app-tournament-public-gallery',
standalone: true,
imports: [CommonModule, MatCardModule, QrCodeModule, NgOptimizedImage],
templateUrl: './tournament-public-gallery.component.html',
styleUrl: './tournament-public-gallery.component.scss',
animations: [
trigger('cardAnimation', [
state(
'expand',
style({
transform: 'scale(1)',
}),
),
state(
'shrink',
style({
transform: 'scale(0.1)',
}),
),
transition('expand => shrink', [animate('0.8s')]),
transition('shrink => expand', [animate('1s')]),
]),
],
providers: [Location, { provide: LocationStrategy, useClass: PathLocationStrategy }],
})
export class TournamentPublicGalleryComponent implements OnInit {
linkToUpload: string = '';
cards: { image: string; alt: string; author: string; title: string; animationState: string }[] =
[];
showQrCode: boolean = false;
pictures: SharedMediaMetadataDto[] = [];
tournamentId = input<number>();
location = inject(Location);
sharedMediaService = inject(SharedMediaEndpointService);
ngOnInit() {
this.sharedMediaService.getPublicSharedMediaByTournament(this.tournamentId()!).subscribe({
next: (data: SharedMediaMetadataDto[]) => {
for (let i = 0; i < 8; i++) {
const card = {
image: 'src',
alt: 'Tournament Image',
author: 'Author',
title: 'Title',
animationState: 'expand',
};
this.loadImage(data[Math.floor(Math.random() * data.length)], card);
this.cards.push(card);
this.setRandomAnimationInterval(card);
}
this.pictures = data;
},
error: error => {
console.error('Error fetching pictures:', error);
},
});
this.setupQRCodeInterval();
this.fetchImages();
this.linkToUpload = this.location.prepareExternalUrl(
`#/tournaments/${this.tournamentId()}/upload-image`,
);
}
setupQRCodeInterval() {
setInterval(() => {
this.showQrCode = true;
setTimeout(() => {
this.showQrCode = false;
//this.fetchImages();
}, 7000);
}, 30000);
}
setRandomAnimationInterval(card: {
image: string;
alt: string;
author: string;
title: string;
animationState: string;
}) {
const expandedDuration = 4000 + Math.random() * 10000;
const shrinkDuration = 1500;
setTimeout(() => {
this.toggleAnimation(card, 'shrink');
this.setRandomAnimationInterval(card);
}, expandedDuration);
}
toggleAnimation(
card: { image: string; alt: string; author: string; title: string; animationState: string },
state: string,
) {
card.animationState = state;
}
onAnimationEnd(
_: AnimationEvent,
card: { image: string; alt: string; author: string; title: string; animationState: string },
) {
if (card.animationState === 'shrink') {
this.loadImage(this.pictures[Math.floor(Math.random() * this.pictures.length)], card);
card.animationState = 'expand';
}
}
fetchImages(): void {
this.sharedMediaService.getPublicSharedMediaByTournament(this.tournamentId()!).subscribe({
next: (data: SharedMediaMetadataDto[]) => {
this.pictures = data;
},
error: error => {
console.error('Error fetching pictures:', error);
},
});
}
checkUploadLinkString(): boolean {
return (
this.linkToUpload === '' || this.linkToUpload.endsWith(`${this.tournamentId()}/upload-image`)
);
}
loadImage(
imgEntry: SharedMediaMetadataDto,
card: { image: string; alt: string; author: string; title: string; animationState: string },
): void {
console.log(`Inside LoadImage ${card.image}`);
if (imgEntry?.id !== undefined) {
this.sharedMediaService.getPublicSharedMediaImage(imgEntry.id!).subscribe(
response => {
card.image = URL.createObjectURL(response as any); // eslint-disable-line
card.alt = imgEntry.title ?? 'Tournament Picture';
card.author = imgEntry.author ?? 'Mysterious KnowOne';
card.title = imgEntry.title ?? 'Tournament Picture';
},
error => {
// Handle errors
console.error('Error fetching shared media image:', error);
card.image = '';
card.alt = 'Error fetching specific image';
},
);
}
}
}