From 2c7013e432e002cf092fcb1af3f81ac5c2a81c82 Mon Sep 17 00:00:00 2001 From: Niklas Marion Date: Fri, 11 Oct 2024 22:44:40 +0200 Subject: [PATCH] use unpic with ipx --- backend/package.json | 5 +- backend/pnpm-lock.yaml | 31 +++- backend/src/images/image.entity.ts | 4 + backend/src/images/iptc.parser.ts | 4 + frontend/.env | 3 +- frontend/.env.development | 3 +- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 31 ++++ frontend/src/components/Image.tsx | 143 ------------------ frontend/src/components/PhotofinishModal.tsx | 23 +-- .../src/components/card/CompetitionCard.tsx | 5 +- .../src/components/card/PhotofinishCard.tsx | 10 +- frontend/src/config/index.ts | 1 + frontend/src/types/index.ts | 2 + 14 files changed, 97 insertions(+), 169 deletions(-) delete mode 100644 frontend/src/components/Image.tsx diff --git a/backend/package.json b/backend/package.json index 2410573..9f5ab6d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,11 +30,12 @@ "class-transformer": "^0.5.1", "csvtojson": "^2.0.10", "exifr": "^7.1.3", + "image-size": "^1.1.1", "ipx": "^3.0.1", + "prisma": "^5.20.0", "rxjs": "^7.8.1", "socket.io": "^4.8.0", - "zod": "^3.23.8", - "prisma": "^5.20.0" + "zod": "^3.23.8" }, "devDependencies": { "@nestjs/cli": "^10.4.5", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 88f55e8..a490b31 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(@nestjs/websockets@10.4.4)(reflect-metadata@0.1.14)(rxjs@7.8.1) '@nestjs/event-emitter': specifier: ^2.0.4 - version: 2.0.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4) + version: 2.0.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(@nestjs/websockets@10.4.4)(reflect-metadata@0.1.14)(rxjs@7.8.1)) '@nestjs/mapped-types': specifier: '*' version: 2.0.5(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(class-transformer@0.5.1)(reflect-metadata@0.1.14) @@ -28,10 +28,10 @@ importers: version: 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/websockets@10.4.4)(rxjs@7.8.1) '@nestjs/serve-static': specifier: ^4.0.2 - version: 4.0.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4)(express@4.21.0) + version: 4.0.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(@nestjs/websockets@10.4.4)(reflect-metadata@0.1.14)(rxjs@7.8.1))(express@4.21.0) '@nestjs/swagger': specifier: ^7.4.2 - version: 7.4.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4)(class-transformer@0.5.1)(reflect-metadata@0.1.14) + version: 7.4.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(@nestjs/websockets@10.4.4)(reflect-metadata@0.1.14)(rxjs@7.8.1))(class-transformer@0.5.1)(reflect-metadata@0.1.14) '@nestjs/websockets': specifier: ^10.4.4 version: 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4)(@nestjs/platform-socket.io@10.4.4)(reflect-metadata@0.1.14)(rxjs@7.8.1) @@ -50,6 +50,9 @@ importers: exifr: specifier: ^7.1.3 version: 7.1.3 + image-size: + specifier: ^1.1.1 + version: 1.1.1 ipx: specifier: ^3.0.1 version: 3.0.1 @@ -1596,6 +1599,11 @@ packages: image-meta@0.2.0: resolution: {integrity: sha512-ZBGjl0ZMEMeOC3Ns0wUF/5UdUmr3qQhBSCniT0LxOgGGIRHiNFOkMtIHB7EOznRU47V2AxPgiVP+s+0/UCU0Hg==} + image-size@1.1.1: + resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==} + engines: {node: '>=16.x'} + hasBin: true + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2107,6 +2115,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -2977,7 +2988,7 @@ snapshots: transitivePeerDependencies: - encoding - '@nestjs/event-emitter@2.0.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4)': + '@nestjs/event-emitter@2.0.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(@nestjs/websockets@10.4.4)(reflect-metadata@0.1.14)(rxjs@7.8.1))': dependencies: '@nestjs/common': 10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1) '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(@nestjs/websockets@10.4.4)(reflect-metadata@0.1.14)(rxjs@7.8.1) @@ -3036,7 +3047,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/serve-static@4.0.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4)(express@4.21.0)': + '@nestjs/serve-static@4.0.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(@nestjs/websockets@10.4.4)(reflect-metadata@0.1.14)(rxjs@7.8.1))(express@4.21.0)': dependencies: '@nestjs/common': 10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1) '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(@nestjs/websockets@10.4.4)(reflect-metadata@0.1.14)(rxjs@7.8.1) @@ -3044,7 +3055,7 @@ snapshots: optionalDependencies: express: 4.21.0 - '@nestjs/swagger@7.4.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4)(class-transformer@0.5.1)(reflect-metadata@0.1.14)': + '@nestjs/swagger@7.4.2(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(@nestjs/websockets@10.4.4)(reflect-metadata@0.1.14)(rxjs@7.8.1))(class-transformer@0.5.1)(reflect-metadata@0.1.14)': dependencies: '@microsoft/tsdoc': 0.15.0 '@nestjs/common': 10.4.4(class-transformer@0.5.1)(reflect-metadata@0.1.14)(rxjs@7.8.1) @@ -4265,6 +4276,10 @@ snapshots: image-meta@0.2.0: {} + image-size@1.1.1: + dependencies: + queue: 6.0.2 + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -4768,6 +4783,10 @@ snapshots: queue-microtask@1.2.3: {} + queue@6.0.2: + dependencies: + inherits: 2.0.4 + radix3@1.1.2: {} randombytes@2.1.0: diff --git a/backend/src/images/image.entity.ts b/backend/src/images/image.entity.ts index 420eed2..0199708 100644 --- a/backend/src/images/image.entity.ts +++ b/backend/src/images/image.entity.ts @@ -26,6 +26,10 @@ export class Image { event: TrackEvent | null; @ApiProperty({ type: () => [Athlete] }) athletes: Athlete[]; + @ApiProperty() + width: number; + @ApiProperty() + height: number; } export class Athlete { diff --git a/backend/src/images/iptc.parser.ts b/backend/src/images/iptc.parser.ts index c4cd73e..5db1692 100644 --- a/backend/src/images/iptc.parser.ts +++ b/backend/src/images/iptc.parser.ts @@ -4,6 +4,7 @@ import * as exifr from 'exifr'; import * as csv from 'csvtojson'; import * as path from 'path'; import { Athlete, TrackEvent, Image } from './image.entity'; +import sizeOf from 'image-size'; const captionSchema = z.object({ FirstName: z.string(), @@ -43,6 +44,7 @@ export async function parseIptcFromFile(file: string): Promise { const windSpeed = getWindSpeedFromCaption(parsedCaption); const athletes = getAthletesFromCaption(parsedCaption); const event = getEventFromTitle(title); + const { width, height } = sizeOf(file); return { lastModified, filename: path.parse(file).base, @@ -55,6 +57,8 @@ export async function parseIptcFromFile(file: string): Promise { athletes, windSpeed, event, + width, + height, }; } diff --git a/frontend/.env b/frontend/.env index d805af4..3c7d7ab 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,2 +1,3 @@ VITE_PHOTOFINISH_API_URL=https://photofinish-api.lcrehlingen.de -VITE_PHOTOFINISH_WEBSOCKET_URL=https://photofinish-api.lcrehlingen.de \ No newline at end of file +VITE_PHOTOFINISH_WEBSOCKET_URL=https://photofinish-api.lcrehlingen.de +VITE_IPX_URL=https://photofinish-api.lcrehlingen.de/_ipx/_/ \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development index 53ed2f8..1348a96 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,2 +1,3 @@ VITE_PHOTOFINISH_API_URL=/api/photofinish -VITE_PHOTOFINISH_WEBSOCKET_URL=http://localhost:3000 \ No newline at end of file +VITE_PHOTOFINISH_WEBSOCKET_URL=http://localhost:3000 +VITE_IPX_URL=http://localhost:3000/_ipx/_/ \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 18308f7..2505f71 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@generouted/react-router": "^1.19.6", + "@unpic/react": "^0.1.14", "axios": "^1.7.7", "qrcode": "^1.5.4", "react": "^18.3.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 33f994c..8625302 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@generouted/react-router': specifier: ^1.19.6 version: 1.19.6(react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(vite@5.4.8(@types/node@22.7.5)) + '@unpic/react': + specifier: ^0.1.14 + version: 0.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) axios: specifier: ^1.7.7 version: 1.7.7 @@ -605,6 +608,19 @@ packages: resolution: {integrity: sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==} engines: {node: ^16.0.0 || >=18.0.0} + '@unpic/core@0.0.49': + resolution: {integrity: sha512-tAqeJRMPF2TrZbSQe74OZ9O5DzKDDUoFwFbZUpjvLcgwGQ/8aleDCb2Iy3bHFJfzzYZ9iHN0hN1VpTlAGQd+ZA==} + + '@unpic/react@0.1.14': + resolution: {integrity: sha512-jQ2qJkLMDQqQIQ8THf1c9YCMgZVO1JhPltdqteTVF9gjGqzgM81LiCG/VjH+EQiZNT3Lk7bPlf7Sc4h/q3VXAg==} + peerDependencies: + next: ^13.0.0 || ^14.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + next: + optional: true + '@vitejs/plugin-react-swc@3.7.1': resolution: {integrity: sha512-vgWOY0i1EROUK0Ctg1hwhtC3SdcDjZcdit4Ups4aPkDcB1jYhmo+RMYWY87cmXMhvtD5uf8lV89j2w16vkdSVg==} peerDependencies: @@ -1613,6 +1629,9 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + unpic@3.18.0: + resolution: {integrity: sha512-JemzuG3nyKpEQ/DArrYM0l+LDSLLPYiUQvDfGXJY35+r0J0C984vPB4Zh8DyMVip102YSnTeZtZ6Q8OQegQDRQ==} + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -2155,6 +2174,16 @@ snapshots: '@typescript-eslint/types': 6.19.0 eslint-visitor-keys: 3.4.3 + '@unpic/core@0.0.49': + dependencies: + unpic: 3.18.0 + + '@unpic/react@0.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@unpic/core': 0.0.49 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@vitejs/plugin-react-swc@3.7.1(vite@5.4.8(@types/node@22.7.5))': dependencies: '@swc/core': 1.7.35 @@ -3194,6 +3223,8 @@ snapshots: undici-types@6.19.8: {} + unpic@3.18.0: {} + update-browserslist-db@1.1.1(browserslist@4.24.0): dependencies: browserslist: 4.24.0 diff --git a/frontend/src/components/Image.tsx b/frontend/src/components/Image.tsx deleted file mode 100644 index 2260924..0000000 --- a/frontend/src/components/Image.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { PHOTOFINISH_API_URL } from "@/config"; - -function getSizes({ - height, - width, - url, - format, - preSizes, - quality = 50, - }: { - height: number; - width: number; - url: string; - format: string; - preSizes?: string; - quality?: number; - }): { sizes: string; srcset: string; src: string } { - const hwRatio = width && height ? height / width : 0; - const variants = []; - - const sizes = [ - 48, 64, 96, 128, 256, 320, 384, 512, 640, 768, 1024, 1280, 1440, 1920, 2048, - 3840, 4096, 7680, - ]; - - for (const key in sizes) { - if (height < sizes[key]) { - continue; - } - const screenMaxWidth = sizes[key]; - const size = sizes[key] + "px"; - - const _cWidth = parseInt(size); - if (!screenMaxWidth || !_cWidth) { - continue; - } - - const _cHeight = hwRatio ? Math.round(_cWidth * hwRatio) : height; - variants.push({ - width: _cWidth, - size, - screenMaxWidth, - media: `(max-width: ${screenMaxWidth}px)`, - src: `${PHOTOFINISH_API_URL}/_ipx/q_${quality},f_${format},s_${_cWidth}x${_cHeight}${url}`, - }); - } - - variants.sort((v1, v2) => v1.screenMaxWidth - v2.screenMaxWidth); - - const defaultVar = variants[variants.length - 1]; - if (defaultVar) { - defaultVar.media = ""; - } - - return { - sizes: variants - .map( - (v) => `${v.media ? v.media + " " : ""}${preSizes ? preSizes : v.size}` - ) - .join(", "), - srcset: variants.map((v) => `${v.src} ${v.width}w`).join(", "), - src: defaultVar?.src, - }; - } - export default function Image({ - width, - height, - src, - alt, - className = "", - imgClassName, - style, - lazy, - sizes, - dataSrc, - quality, - }: { - width: number; - height: number; - src: string; - alt?: string; - className?: string; - imgClassName?: string; - style?: React.CSSProperties; - lazy?: boolean; - sizes?: string; - dataSrc?: string; - quality?: number; - }) { - const calcSizesWebp = getSizes({ - width, - height, - url: src, - format: "webp", - preSizes: sizes, - quality, - }); - const calcSizesJpg = getSizes({ - width, - height, - url: src, - format: "jpg", - preSizes: sizes, - quality, - }); - const calcSizesJpgFull = getSizes({ - width, - height, - url: src, - format: "jpg", - quality, - }); - return ( - - - - {alt} - - ); - } \ No newline at end of file diff --git a/frontend/src/components/PhotofinishModal.tsx b/frontend/src/components/PhotofinishModal.tsx index b9e1c33..3dab21a 100644 --- a/frontend/src/components/PhotofinishModal.tsx +++ b/frontend/src/components/PhotofinishModal.tsx @@ -1,32 +1,34 @@ -import { PHOTOFINISH_API_URL } from "@/config"; -import { Image } from "@/types"; +import { IPX_URL, PHOTOFINISH_API_URL } from "@/config"; +import { Image as ImageType } from "@/types"; +import { Image } from "@unpic/react"; import ReactModal from "react-modal"; export default function PhotofinishModal({ image, onClose, }: { - image: Image; + image: ImageType; onClose: () => void; }) { - const { eventId } = image; + const { eventId } = image; return ( {image && (
- {image.title}

- {image.title} + {`${PHOTOFINISH_API_URL}/_ipx/${eventId}/${image.filename}`}

{image.windSpeed && (
@@ -58,7 +60,8 @@ export default function PhotofinishModal({ )} {athlete.firstname} {athlete.lastname}{" "} - {athlete.nationality && `(${athlete.nationality.trim()})`} + {athlete.nationality && + `(${athlete.nationality.trim()})`} {athlete.time} diff --git a/frontend/src/components/card/CompetitionCard.tsx b/frontend/src/components/card/CompetitionCard.tsx index 5a0a183..714a636 100644 --- a/frontend/src/components/card/CompetitionCard.tsx +++ b/frontend/src/components/card/CompetitionCard.tsx @@ -1,7 +1,7 @@ import { Link, useSearchParams } from "react-router-dom"; -import Image from "../Image"; import ImageCard from "./ImageCard"; import { Event } from "@/types"; +import { Image } from "@unpic/react"; export default function CompetitionCard({ id, date, name, location }: Event) { const [searchParams] = useSearchParams(); @@ -10,7 +10,8 @@ export default function CompetitionCard({ id, date, name, location }: Event) { } > diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts index e2c00a5..61225b7 100644 --- a/frontend/src/config/index.ts +++ b/frontend/src/config/index.ts @@ -1,2 +1,3 @@ export const PHOTOFINISH_API_URL = import.meta.env.VITE_PHOTOFINISH_API_URL; export const PHOTOFINISH_WEBSOCKET_URL = import.meta.env.VITE_PHOTOFINISH_WEBSOCKET_URL; +export const IPX_URL = import.meta.env.VITE_IPX_URL; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 383f11a..e1e07c7 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -40,6 +40,8 @@ export interface Image { timestamp: number; title: string; windSpeed: string; + width: number; + height: number; } export interface TrackEvent {