From 327faf052e80e3896fa68732b2ce20fcaa1b5e02 Mon Sep 17 00:00:00 2001 From: Inga Lehne Date: Thu, 18 Jun 2026 13:34:10 +0200 Subject: [PATCH 1/8] use all available width for image --- frontend/src/App.tsx | 2 +- frontend/src/party/view/ViewPartySubmission.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 714a6e8..4711bd2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -68,7 +68,7 @@ function App() {
-
{session ? : }
+
{session ? : }
diff --git a/frontend/src/party/view/ViewPartySubmission.tsx b/frontend/src/party/view/ViewPartySubmission.tsx index bae9255..0010793 100644 --- a/frontend/src/party/view/ViewPartySubmission.tsx +++ b/frontend/src/party/view/ViewPartySubmission.tsx @@ -42,7 +42,7 @@ function ViewPartySubmission({ return (
-
+
Date: Thu, 18 Jun 2026 14:05:01 +0200 Subject: [PATCH 2/8] fix overview for firefox --- frontend/src/party/PartyOverviewItem.tsx | 10 ++++------ frontend/src/party/timeline/PartyTimeline.tsx | 2 +- frontend/src/party/view/ViewPartySubmission.tsx | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/src/party/PartyOverviewItem.tsx b/frontend/src/party/PartyOverviewItem.tsx index 3262e78..c38b1e5 100644 --- a/frontend/src/party/PartyOverviewItem.tsx +++ b/frontend/src/party/PartyOverviewItem.tsx @@ -13,8 +13,8 @@ function PartyOverviewItem({ party }: { party: PartyResponse }) { const submissions = sortSubmissions(party.submissions); return ( -
-
+
+
-
+
{submissions.slice(0, 3).map((submission) => ( -
- {submission?.name} -
+ {submission?.name} ))}
diff --git a/frontend/src/party/timeline/PartyTimeline.tsx b/frontend/src/party/timeline/PartyTimeline.tsx index 3fba54e..567a7ee 100644 --- a/frontend/src/party/timeline/PartyTimeline.tsx +++ b/frontend/src/party/timeline/PartyTimeline.tsx @@ -44,7 +44,7 @@ export const PartyTimeline: FunctionComponent<{ year: number; parties: PartyResp

{year}

{monthsWithSumissions.map((month) => ( -
+

{month}

{timeline.get(month)!.map((party) => (
diff --git a/frontend/src/party/view/ViewPartySubmission.tsx b/frontend/src/party/view/ViewPartySubmission.tsx index 0010793..2cead1a 100644 --- a/frontend/src/party/view/ViewPartySubmission.tsx +++ b/frontend/src/party/view/ViewPartySubmission.tsx @@ -48,7 +48,7 @@ function ViewPartySubmission({ startAt={new Date(partyStatus.current!.startTime)} onFinish={onTimer} /> -
+
Date: Tue, 23 Jun 2026 14:26:25 +0200 Subject: [PATCH 3/8] add gallery for leaderboard --- .gitignore | 4 + api/pkg/group-challenge/auth/auth.go | 2 +- frontend/package-lock.json | 74 +++++++-------- frontend/package.json | 4 +- frontend/src/api/api-models.ts | 11 ++- frontend/src/collage/Collage.tsx | 2 +- frontend/src/index.css | 2 +- frontend/src/party/view/SubmissionGallery.tsx | 93 +++++++++++++++++++ frontend/src/party/view/ViewParty.tsx | 6 +- .../src/party/view/ViewPartyLeaderboard.tsx | 42 ++------- .../src/party/view/rewards/PartyPosition.tsx | 4 +- 11 files changed, 158 insertions(+), 86 deletions(-) create mode 100644 frontend/src/party/view/SubmissionGallery.tsx diff --git a/.gitignore b/.gitignore index e69de29..5e05600 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules + +### IntelliJ IDEA ### +.idea diff --git a/api/pkg/group-challenge/auth/auth.go b/api/pkg/group-challenge/auth/auth.go index f3ce01c..f010edb 100644 --- a/api/pkg/group-challenge/auth/auth.go +++ b/api/pkg/group-challenge/auth/auth.go @@ -11,7 +11,7 @@ import ( "golang.org/x/crypto/bcrypt" ) -// CreateUser creates a user with and hashes the password +// CreateUser creates a user with username and email and hashes the password func CreateUser(username, password, email string) *models.User { userModel := &models.User{ Username: username, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5ffc413..36fce2a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,11 +11,13 @@ "@headlessui/react": "2.2.10", "@tanstack/react-query": "5.101.0", "date-fns": "4.4.0", + "photoswipe": "^5.4.4", "react": "19.2.7", "react-date-picker": "12.0.2", "react-dom": "19.2.7", "react-hook-form": "7.79.0", "react-icons": "5.6.0", + "react-photoswipe-gallery": "^4.1.2", "react-router": "7.17.0", "react-router-dom": "7.17.0", "react-toastify": "11.1.0", @@ -377,9 +379,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -397,9 +396,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -417,9 +413,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -437,9 +430,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -457,9 +447,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -477,9 +464,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -708,9 +692,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -728,9 +709,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -748,9 +726,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -768,9 +743,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1273,9 +1245,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1297,9 +1266,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1321,9 +1287,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1345,9 +1308,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1480,6 +1440,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/photoswipe": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.4.tgz", + "integrity": "sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==", + "license": "MIT", + "engines": { + "node": ">= 0.12.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1671,6 +1640,16 @@ "react": "*" } }, + "node_modules/react-photoswipe-gallery": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-photoswipe-gallery/-/react-photoswipe-gallery-4.1.2.tgz", + "integrity": "sha512-QfohXZVY3pXe0q5/+iH1G6Yx2xr5GsBxALT5S3c9GP1HLCechG4MG3IMXaoujatn/trG5CIlY44fmiuzW1vjjA==", + "license": "MIT", + "peerDependencies": { + "photoswipe": ">= 5.2.2", + "react": ">= 16.8.0" + } + }, "node_modules/react-router": { "version": "7.17.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", @@ -2745,6 +2724,11 @@ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true }, + "photoswipe": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.4.tgz", + "integrity": "sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==" + }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2843,6 +2827,12 @@ "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", "requires": {} }, + "react-photoswipe-gallery": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-photoswipe-gallery/-/react-photoswipe-gallery-4.1.2.tgz", + "integrity": "sha512-QfohXZVY3pXe0q5/+iH1G6Yx2xr5GsBxALT5S3c9GP1HLCechG4MG3IMXaoujatn/trG5CIlY44fmiuzW1vjjA==", + "requires": {} + }, "react-router": { "version": "7.17.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b7b6899..2d59749 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,11 +12,13 @@ "@headlessui/react": "2.2.10", "@tanstack/react-query": "5.101.0", "date-fns": "4.4.0", + "photoswipe": "^5.4.4", "react": "19.2.7", "react-date-picker": "12.0.2", "react-dom": "19.2.7", "react-hook-form": "7.79.0", "react-icons": "5.6.0", + "react-photoswipe-gallery": "^4.1.2", "react-router": "7.17.0", "react-router-dom": "7.17.0", "react-toastify": "11.1.0", @@ -26,10 +28,10 @@ "use-sound": "5.0.0" }, "devDependencies": { + "@tailwindcss/vite": "4.3.1", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.2", - "@tailwindcss/vite": "4.3.1", "typescript": "6.0.3", "vite": "8.0.16" } diff --git a/frontend/src/api/api-models.ts b/frontend/src/api/api-models.ts index cb6f2ad..fc85d2d 100644 --- a/frontend/src/api/api-models.ts +++ b/frontend/src/api/api-models.ts @@ -55,7 +55,14 @@ export interface PartySubmissionFormData { description: string; } -export type PartyStatusState = 'open' | 'waitinglobby' | 'submissions' | 'prereveal' | 'reveal' | 'done'; +export enum PartyStatusState { + OPEN = 'open', + LOBBY = 'waitinglobby', + RUNNING = 'submissions', + PRE_REVEAL = 'prereveal', + REVEAL = 'reveal', + DONE = 'done', +} export interface PartyStatusResponse { current?: { @@ -72,7 +79,7 @@ export interface PartyStatusResponse { } export function isPartyLive(partyStatus?: PartyStatusResponse) { - return partyStatus?.state !== 'open' && partyStatus?.state !== 'done'; + return partyStatus?.state !== PartyStatusState.OPEN && partyStatus?.state !== PartyStatusState.DONE; } // session diff --git a/frontend/src/collage/Collage.tsx b/frontend/src/collage/Collage.tsx index 836337c..137c11d 100644 --- a/frontend/src/collage/Collage.tsx +++ b/frontend/src/collage/Collage.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useRef, useState } from 'react'; + import { FC, useEffect, useRef, useState } from 'react'; import { getThumbnailUrl, useParties } from '../api/api'; import { sortSubmissions } from '../party/view/util'; diff --git a/frontend/src/index.css b/frontend/src/index.css index 46b6a01..0b34e73 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,2 +1,2 @@ @import 'react-toastify/dist/ReactToastify.css'; -@import 'tailwindcss'; +@import 'tailwindcss'; \ No newline at end of file diff --git a/frontend/src/party/view/SubmissionGallery.tsx b/frontend/src/party/view/SubmissionGallery.tsx new file mode 100644 index 0000000..d6a37cd --- /dev/null +++ b/frontend/src/party/view/SubmissionGallery.tsx @@ -0,0 +1,93 @@ +import 'photoswipe/dist/photoswipe.css' + +import {useEffect, useState} from 'react' +import {FaStar} from 'react-icons/fa'; +import {Gallery, Item} from 'react-photoswipe-gallery' +import {PartySubmissionResponse} from "../../api/api-models"; +import {getImageUrl, getThumbnailUrl} from '../../api/api'; +import PartyPosition from "./rewards/PartyPosition"; +import {avgRatingTwoDecimals, totalRating} from "./util"; + +interface ImageDimensions { + width: number; + height: number; +} + +function useImageDimensions(imageUrls: string[], imageIds: string[]): Map { + const [dimensions, setDimensions] = useState>(new Map()); + + useEffect(() => { + const newDimensions = new Map(); + let loadedCount = 0; + + imageUrls.forEach((url, idx) => { + const img = new Image(); + img.src = url; + img.onload = () => { + newDimensions.set(imageIds[idx], {width: img.naturalWidth, height: img.naturalHeight}); + loadedCount++; + if (loadedCount === imageUrls.length) { + setDimensions(new Map(newDimensions)); + } + }; + img.onerror = () => { + // Fallback dimensions + newDimensions.set(imageIds[idx], {width: 800, height: 600}); + loadedCount++; + if (loadedCount === imageUrls.length) { + setDimensions(new Map(newDimensions)); + } + }; + }); + }, imageUrls); + + return dimensions; +} + +function SubmissionGallery({submissions}: { submissions: PartySubmissionResponse[] }) { + const originalUrls = submissions.map((s) => getImageUrl(s.imageId)); + const imageIds = submissions.map((s) => s.id); + const dimensions = useImageDimensions(originalUrls, imageIds); + + return ( + + {submissions.map((submission, i) => { + const currentDimensions = dimensions.get(submission.id); + return ( +
+ + + {({ref, open}) => ( + + )} + +
+
+ {submission.name} {submission.description} +
+
Ø {avgRatingTwoDecimals(submission.votes)}
+
+
{totalRating(submission.votes)}
+ +
+
{submission.votes.length} vote(s)
+
+
+ ); + })} +
+ ); +} + +export default SubmissionGallery; \ No newline at end of file diff --git a/frontend/src/party/view/ViewParty.tsx b/frontend/src/party/view/ViewParty.tsx index 621f4dc..f9c0901 100644 --- a/frontend/src/party/view/ViewParty.tsx +++ b/frontend/src/party/view/ViewParty.tsx @@ -11,7 +11,7 @@ import { } from '../../api/api'; import ViewPartySubmission from './ViewPartySubmission'; import ViewPartyLeaderboard from './ViewPartyLeaderboard'; -import { PartyResponse, PartyStatusResponse } from '../../api/api-models'; +import { PartyResponse, PartyStatusResponse, PartyStatusState } from '../../api/api-models'; import Button from '../../components/Button'; import { useMutation } from '@tanstack/react-query'; import { useSession } from '../../user/session'; @@ -176,7 +176,9 @@ function ViewParty() { const isHost = session!.userId === partyUserId; const showControlButtons = - isHost && (partyStatusState === 'submissions' || partyStatusState === 'reveal' || partyStatusState === 'prereveal'); + isHost && (partyStatusState === PartyStatusState.RUNNING || + partyStatusState === PartyStatusState.REVEAL || + partyStatusState === ); return (
diff --git a/frontend/src/party/view/ViewPartyLeaderboard.tsx b/frontend/src/party/view/ViewPartyLeaderboard.tsx index 6e6edad..46b949a 100644 --- a/frontend/src/party/view/ViewPartyLeaderboard.tsx +++ b/frontend/src/party/view/ViewPartyLeaderboard.tsx @@ -1,11 +1,9 @@ -import { FaStar } from 'react-icons/fa'; -import { getImageUrl, getThumbnailUrl } from '../../api/api'; -import { PartyResponse } from '../../api/api-models'; -import PartyPosition from './rewards/PartyPosition'; +import {PartyResponse} from '../../api/api-models'; -import { totalRating, avgRatingTwoDecimals, sortSubmissions } from './util'; -import { useSession } from '../../user/session'; +import {sortSubmissions} from './util'; +import {useSession} from '../../user/session'; import LinkButton from '../../components/LinkButton'; +import SubmissionGallery from "./SubmissionGallery"; function ViewPartyLeaderboard({ party }: { party: PartyResponse }) { const [session] = useSession(); @@ -13,38 +11,14 @@ function ViewPartyLeaderboard({ party }: { party: PartyResponse }) { const sortedSubmissions = sortSubmissions(party.submissions); return ( -
-
+
+

{party.name}

{isHost && }
-
- {sortedSubmissions.map((submission, i) => ( -
- - -
-
- {submission.name} {submission.description} -
-
Ø {avgRatingTwoDecimals(submission.votes)}
-
-
{totalRating(submission.votes)}
- -
-
{submission.votes.length} vote(s)
-
-
- ))} +
+
); diff --git a/frontend/src/party/view/rewards/PartyPosition.tsx b/frontend/src/party/view/rewards/PartyPosition.tsx index 1a5205d..3e539a9 100644 --- a/frontend/src/party/view/rewards/PartyPosition.tsx +++ b/frontend/src/party/view/rewards/PartyPosition.tsx @@ -3,12 +3,12 @@ import TrophyUrl from '/src/assets/trophy.svg'; function PartyPosition({ position }: { position: number }) { return ( - <> +
{position === 0 && } {position === 1 && } {position === 2 && } {position > 2 &&
{position + 1}.
} - +
); } From 9eec86d36daf0982a5f8b15cae78507a9b4e2706 Mon Sep 17 00:00:00 2001 From: Inga Lehne Date: Fri, 26 Jun 2026 14:37:44 +0200 Subject: [PATCH 4/8] [misc] rename states, add ids, fix formatting --- .../liveparty/live-party-state.go | 8 +- .../group-challenge/liveparty/live-party.go | 16 +- frontend/src/api/api-models.ts | 4 +- frontend/src/components/ReactionPicker.tsx | 56 ++++--- frontend/src/party/PartyOverviewItem.tsx | 6 +- frontend/src/party/timeline/PartyTimeline.tsx | 2 +- frontend/src/party/view/ReactionBubbles.tsx | 2 +- frontend/src/party/view/SubmissionGallery.tsx | 156 +++++++++--------- frontend/src/party/view/ViewParty.tsx | 53 +++--- frontend/src/party/view/ViewPartyReveal.tsx | 8 +- .../src/party/view/ViewPartyStartPage.tsx | 2 +- .../src/party/view/ViewPartySubmission.tsx | 10 +- 12 files changed, 157 insertions(+), 166 deletions(-) diff --git a/api/pkg/group-challenge/liveparty/live-party-state.go b/api/pkg/group-challenge/liveparty/live-party-state.go index 99744bc..10431f1 100644 --- a/api/pkg/group-challenge/liveparty/live-party-state.go +++ b/api/pkg/group-challenge/liveparty/live-party-state.go @@ -30,7 +30,7 @@ func (liveParty *LiveParty) Previous() { return } - if liveParty.Status.State == LivePartyStateSubmissions { + if liveParty.Status.State == LivePartyStateRunning { liveParty.prevSubmission() return } @@ -68,8 +68,8 @@ func (liveParty *LiveParty) prevReveal() { } func (liveParty *LiveParty) Next() { - if liveParty.Status.State == LivePartyStateWaitingLobby { - liveParty.Status.State = LivePartyStateSubmissions + if liveParty.Status.State == LivePartyStateLobby { + liveParty.Status.State = LivePartyStateRunning } if liveParty.Status.State == LivePartyStatePreReveal { @@ -77,7 +77,7 @@ func (liveParty *LiveParty) Next() { liveParty.Status.State = LivePartyStateReveal } - if liveParty.Status.State == LivePartyStateSubmissions { + if liveParty.Status.State == LivePartyStateRunning { liveParty.nextSubmission() return } diff --git a/api/pkg/group-challenge/liveparty/live-party.go b/api/pkg/group-challenge/liveparty/live-party.go index e0e9e96..87fe48d 100644 --- a/api/pkg/group-challenge/liveparty/live-party.go +++ b/api/pkg/group-challenge/liveparty/live-party.go @@ -11,13 +11,13 @@ import ( ) var ( - MaxStars = 6 - LivePartyStateOpen = "open" - LivePartyStateWaitingLobby = "waitinglobby" - LivePartyStateSubmissions = "submissions" - LivePartyStatePreReveal = "prereveal" - LivePartyStateReveal = "reveal" - LivePartyStateDone = "done" + MaxStars = 6 + LivePartyStateOpen = "open" + LivePartyStateLobby = "lobby" + LivePartyStateRunning = "running" + LivePartyStatePreReveal = "prereveal" + LivePartyStateReveal = "reveal" + LivePartyStateDone = "done" ) type SubmissionStatus struct { @@ -57,7 +57,7 @@ func createLiveParty(party *models.Party, con *pg.DB, livePartyConfig config.Liv PartyStartTime: time.Now(), SubmissionTimeMs: livePartyConfig.DefaultTimePerSubmissionSeconds * 1000, Participants: 1, - State: LivePartyStateWaitingLobby, + State: LivePartyStateLobby, Votes: []*models.Vote{}, }, ParticipantsUserIDs: []uuid.UUID{party.UserID}, diff --git a/frontend/src/api/api-models.ts b/frontend/src/api/api-models.ts index fc85d2d..d40faf6 100644 --- a/frontend/src/api/api-models.ts +++ b/frontend/src/api/api-models.ts @@ -57,8 +57,8 @@ export interface PartySubmissionFormData { export enum PartyStatusState { OPEN = 'open', - LOBBY = 'waitinglobby', - RUNNING = 'submissions', + LOBBY = 'lobby', + RUNNING = 'running', PRE_REVEAL = 'prereveal', REVEAL = 'reveal', DONE = 'done', diff --git a/frontend/src/components/ReactionPicker.tsx b/frontend/src/components/ReactionPicker.tsx index a4695bd..75345d2 100644 --- a/frontend/src/components/ReactionPicker.tsx +++ b/frontend/src/components/ReactionPicker.tsx @@ -17,34 +17,36 @@ const ReactionPicker: FunctionComponent<{ onReaction: (reaction: string) => void ); return ( -
-
setAppState((old) => ({ ...old, reactionPickerOpen: true }))} - > - {!appState?.reactionPickerOpen && } -
- {appState?.reactionPickerOpen && ( -
- setTextInput(e.target.value)} - onKeyUp={(event) => { - if (event.key === 'Enter') { - onTextSubmit(textInput); - } else if (event.key === 'Escape') { - setAppState((old) => ({ ...old, reactionPickerOpen: false })); - } - }} - /> - +
+
+
setAppState((old) => ({ ...old, reactionPickerOpen: true }))} + > + {!appState?.reactionPickerOpen && }
- )} + {appState?.reactionPickerOpen && ( +
+ setTextInput(e.target.value)} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onTextSubmit(textInput); + } else if (event.key === 'Escape') { + setAppState((old) => ({ ...old, reactionPickerOpen: false })); + } + }} + /> + +
+ )} +
); }; diff --git a/frontend/src/party/PartyOverviewItem.tsx b/frontend/src/party/PartyOverviewItem.tsx index c38b1e5..fb9208b 100644 --- a/frontend/src/party/PartyOverviewItem.tsx +++ b/frontend/src/party/PartyOverviewItem.tsx @@ -13,8 +13,8 @@ function PartyOverviewItem({ party }: { party: PartyResponse }) { const submissions = sortSubmissions(party.submissions); return ( -
-
+
+
-
+
{submissions.slice(0, 3).map((submission) => ( {submission?.name} ))} diff --git a/frontend/src/party/timeline/PartyTimeline.tsx b/frontend/src/party/timeline/PartyTimeline.tsx index 567a7ee..a6e9697 100644 --- a/frontend/src/party/timeline/PartyTimeline.tsx +++ b/frontend/src/party/timeline/PartyTimeline.tsx @@ -44,7 +44,7 @@ export const PartyTimeline: FunctionComponent<{ year: number; parties: PartyResp

{year}

{monthsWithSumissions.map((month) => ( -
+

{month}

{timeline.get(month)!.map((party) => (
diff --git a/frontend/src/party/view/ReactionBubbles.tsx b/frontend/src/party/view/ReactionBubbles.tsx index 0ff34fa..8fb085e 100644 --- a/frontend/src/party/view/ReactionBubbles.tsx +++ b/frontend/src/party/view/ReactionBubbles.tsx @@ -36,7 +36,7 @@ const ReactionBubbles: FunctionComponent<{ partyId: string }> = ({ partyId }) => }, [reactions]); return ( -
+
{reactions.map((reaction, i) => (
{reaction.reaction}
diff --git a/frontend/src/party/view/SubmissionGallery.tsx b/frontend/src/party/view/SubmissionGallery.tsx index d6a37cd..800868a 100644 --- a/frontend/src/party/view/SubmissionGallery.tsx +++ b/frontend/src/party/view/SubmissionGallery.tsx @@ -1,93 +1,93 @@ -import 'photoswipe/dist/photoswipe.css' +import 'photoswipe/dist/photoswipe.css'; -import {useEffect, useState} from 'react' -import {FaStar} from 'react-icons/fa'; -import {Gallery, Item} from 'react-photoswipe-gallery' -import {PartySubmissionResponse} from "../../api/api-models"; -import {getImageUrl, getThumbnailUrl} from '../../api/api'; -import PartyPosition from "./rewards/PartyPosition"; -import {avgRatingTwoDecimals, totalRating} from "./util"; +import { useEffect, useState } from 'react'; +import { FaStar } from 'react-icons/fa'; +import { Gallery, Item } from 'react-photoswipe-gallery'; +import { PartySubmissionResponse } from '../../api/api-models'; +import { getImageUrl, getThumbnailUrl } from '../../api/api'; +import PartyPosition from './rewards/PartyPosition'; +import { avgRatingTwoDecimals, totalRating } from './util'; interface ImageDimensions { - width: number; - height: number; + width: number; + height: number; } function useImageDimensions(imageUrls: string[], imageIds: string[]): Map { - const [dimensions, setDimensions] = useState>(new Map()); + const [dimensions, setDimensions] = useState>(new Map()); - useEffect(() => { - const newDimensions = new Map(); - let loadedCount = 0; + useEffect(() => { + const newDimensions = new Map(); + let loadedCount = 0; - imageUrls.forEach((url, idx) => { - const img = new Image(); - img.src = url; - img.onload = () => { - newDimensions.set(imageIds[idx], {width: img.naturalWidth, height: img.naturalHeight}); - loadedCount++; - if (loadedCount === imageUrls.length) { - setDimensions(new Map(newDimensions)); - } - }; - img.onerror = () => { - // Fallback dimensions - newDimensions.set(imageIds[idx], {width: 800, height: 600}); - loadedCount++; - if (loadedCount === imageUrls.length) { - setDimensions(new Map(newDimensions)); - } - }; - }); - }, imageUrls); + imageUrls.forEach((url, idx) => { + const img = new Image(); + img.src = url; + img.onload = () => { + newDimensions.set(imageIds[idx], { width: img.naturalWidth, height: img.naturalHeight }); + loadedCount++; + if (loadedCount === imageUrls.length) { + setDimensions(new Map(newDimensions)); + } + }; + img.onerror = () => { + // Fallback dimensions + newDimensions.set(imageIds[idx], { width: 800, height: 600 }); + loadedCount++; + if (loadedCount === imageUrls.length) { + setDimensions(new Map(newDimensions)); + } + }; + }); + }, imageUrls); - return dimensions; + return dimensions; } -function SubmissionGallery({submissions}: { submissions: PartySubmissionResponse[] }) { - const originalUrls = submissions.map((s) => getImageUrl(s.imageId)); - const imageIds = submissions.map((s) => s.id); - const dimensions = useImageDimensions(originalUrls, imageIds); +function SubmissionGallery({ submissions }: { submissions: PartySubmissionResponse[] }) { + const originalUrls = submissions.map((s) => getImageUrl(s.imageId)); + const imageIds = submissions.map((s) => s.id); + const dimensions = useImageDimensions(originalUrls, imageIds); - return ( - - {submissions.map((submission, i) => { - const currentDimensions = dimensions.get(submission.id); - return ( -
- - - {({ref, open}) => ( - - )} - -
-
- {submission.name} {submission.description} -
-
Ø {avgRatingTwoDecimals(submission.votes)}
-
-
{totalRating(submission.votes)}
- -
-
{submission.votes.length} vote(s)
-
-
- ); - })} -
- ); + return ( + + {submissions.map((submission, i) => { + const currentDimensions = dimensions.get(submission.id); + return ( + + ); + })} + + ); } export default SubmissionGallery; \ No newline at end of file diff --git a/frontend/src/party/view/ViewParty.tsx b/frontend/src/party/view/ViewParty.tsx index f9c0901..42aa886 100644 --- a/frontend/src/party/view/ViewParty.tsx +++ b/frontend/src/party/view/ViewParty.tsx @@ -1,14 +1,6 @@ import { useCallback, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router'; -import { - joinParty, - nextPartySubmissions, - previousPartySubmissions, - sendReaction, - useParty, - usePartyStatus, - votePartySubmissions, -} from '../../api/api'; +import { joinParty, nextPartySubmissions, previousPartySubmissions, sendReaction, useParty, usePartyStatus, votePartySubmissions } from '../../api/api'; import ViewPartySubmission from './ViewPartySubmission'; import ViewPartyLeaderboard from './ViewPartyLeaderboard'; import { PartyResponse, PartyStatusResponse, PartyStatusState } from '../../api/api-models'; @@ -23,13 +15,13 @@ import ReactionPicker from '../../components/ReactionPicker'; import { usePreloadNextImage } from './util'; const ViewPartyContent = ({ - partyStatus, - isHost, - party, - onRating, - onNextButton, - onRedirect, -}: { + partyStatus, + isHost, + party, + onRating, + onNextButton, + onRedirect + }: { partyStatus: PartyStatusResponse; isHost: boolean; party: PartyResponse; @@ -52,22 +44,19 @@ const ViewPartyContent = ({ return
Unknown party or party status
; } - if (party.done || partyStatus.state === 'done') { + const reactionPicker = ( + + ); + + if (party.done || partyStatus.state === PartyStatusState.DONE) { return ; } - - if (partyStatus.state === 'open') { + if (partyStatus.state === PartyStatusState.OPEN) { onRedirect(); return null; } - const reactionPicker = ( -
- -
- ); - - if (partyStatus.state === 'waitinglobby') { + if (partyStatus.state === PartyStatusState.LOBBY) { return ( @@ -97,9 +86,9 @@ const ViewPartyContent = ({ ); } - if (partyStatus.state === 'prereveal') { + if (partyStatus.state === PartyStatusState.PRE_REVEAL) { return ( -
+

Thanks for voting!

Waiting for host...

@@ -111,7 +100,7 @@ const ViewPartyContent = ({ ); } - if (partyStatus.state === 'reveal') { + if (partyStatus.state === PartyStatusState.REVEAL) { return (
@@ -177,8 +166,8 @@ function ViewParty() { const isHost = session!.userId === partyUserId; const showControlButtons = isHost && (partyStatusState === PartyStatusState.RUNNING || - partyStatusState === PartyStatusState.REVEAL || - partyStatusState === ); + partyStatusState === PartyStatusState.REVEAL || + partyStatusState === PartyStatusState.PRE_REVEAL); return (
diff --git a/frontend/src/party/view/ViewPartyReveal.tsx b/frontend/src/party/view/ViewPartyReveal.tsx index b5a3563..9ef0a3b 100644 --- a/frontend/src/party/view/ViewPartyReveal.tsx +++ b/frontend/src/party/view/ViewPartyReveal.tsx @@ -14,9 +14,9 @@ function ViewPartyReveal({ party, partyStatus }: { party: PartyResponse; partySt const votes = getSubmissionVotes(partyStatus, currentSumbission); return ( -
+ -
+

{currentSumbission.name} {currentSumbission.description}

-
+
diff --git a/frontend/src/party/view/ViewPartyStartPage.tsx b/frontend/src/party/view/ViewPartyStartPage.tsx index 357c713..fb5eb32 100644 --- a/frontend/src/party/view/ViewPartyStartPage.tsx +++ b/frontend/src/party/view/ViewPartyStartPage.tsx @@ -13,7 +13,7 @@ function ViewPartyStartPage({ onPartyStart: () => any; }) { return ( -
+

- {partyName} -

Share link:

diff --git a/frontend/src/party/view/ViewPartySubmission.tsx b/frontend/src/party/view/ViewPartySubmission.tsx index 2cead1a..2899aa7 100644 --- a/frontend/src/party/view/ViewPartySubmission.tsx +++ b/frontend/src/party/view/ViewPartySubmission.tsx @@ -41,15 +41,15 @@ function ViewPartySubmission({ }, [rating, onRating]); return ( -
+
- -
+

{partyStatus.current!.position + 1} @@ -75,7 +75,7 @@ function ViewPartySubmission({ {partySubmission.description}

-
+
{getSubmissionVotes(partyStatus, partySubmission).length} From d22658c6b1104b87c424051b343f13e5a7440be3 Mon Sep 17 00:00:00 2001 From: Inga Lehne Date: Wed, 30 Sep 2026 10:24:01 +0200 Subject: [PATCH 5/8] package log --- frontend/package-lock.json | 86 ++++++++++++++------------------------ 1 file changed, 32 insertions(+), 54 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1435e74..9876d7c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,11 +11,13 @@ "@headlessui/react": "2.2.10", "@tanstack/react-query": "5.104.0", "date-fns": "4.4.0", + "photoswipe": "^5.4.4", "react": "19.3.0", "react-date-picker": "12.1.0", "react-dom": "19.3.0", "react-hook-form": "7.89.0", "react-icons": "5.7.0", + "react-photoswipe-gallery": "^4.1.2", "react-router": "8.4.0", "react-router-dom": "7.18.4", "react-toastify": "11.1.0", @@ -347,9 +349,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -367,9 +366,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -387,9 +383,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -407,9 +400,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -427,9 +417,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -447,9 +434,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -659,9 +643,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -679,9 +660,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -699,9 +677,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -719,9 +694,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1567,9 +1539,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1591,9 +1560,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1615,9 +1581,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1639,9 +1602,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1774,6 +1734,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/photoswipe": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.4.tgz", + "integrity": "sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==", + "license": "MIT", + "engines": { + "node": ">= 0.12.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1972,6 +1941,16 @@ "react": "*" } }, + "node_modules/react-photoswipe-gallery": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-photoswipe-gallery/-/react-photoswipe-gallery-4.1.2.tgz", + "integrity": "sha512-QfohXZVY3pXe0q5/+iH1G6Yx2xr5GsBxALT5S3c9GP1HLCechG4MG3IMXaoujatn/trG5CIlY44fmiuzW1vjjA==", + "license": "MIT", + "peerDependencies": { + "photoswipe": ">= 5.2.2", + "react": ">= 16.8.0" + } + }, "node_modules/react-router": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.4.0.tgz", @@ -2474,9 +2453,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2498,9 +2474,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2522,9 +2495,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2546,9 +2516,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3459,6 +3426,11 @@ "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true }, + "photoswipe": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.4.tgz", + "integrity": "sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==" + }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3557,6 +3529,12 @@ "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", "requires": {} }, + "react-photoswipe-gallery": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-photoswipe-gallery/-/react-photoswipe-gallery-4.1.2.tgz", + "integrity": "sha512-QfohXZVY3pXe0q5/+iH1G6Yx2xr5GsBxALT5S3c9GP1HLCechG4MG3IMXaoujatn/trG5CIlY44fmiuzW1vjjA==", + "requires": {} + }, "react-router": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.4.0.tgz", From 8fe3d9479fc00af783d843ff1de000221aa4d092 Mon Sep 17 00:00:00 2001 From: Inga Lehne Date: Wed, 30 Sep 2026 13:26:34 +0200 Subject: [PATCH 6/8] [technical] ids unique per page --- frontend/src/party/timeline/PartyTimeline.tsx | 2 +- frontend/src/party/view/SubmissionGallery.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/party/timeline/PartyTimeline.tsx b/frontend/src/party/timeline/PartyTimeline.tsx index a6e9697..4d262da 100644 --- a/frontend/src/party/timeline/PartyTimeline.tsx +++ b/frontend/src/party/timeline/PartyTimeline.tsx @@ -44,7 +44,7 @@ export const PartyTimeline: FunctionComponent<{ year: number; parties: PartyResp

{year}

{monthsWithSumissions.map((month) => ( -
+

{month}

{timeline.get(month)!.map((party) => (
diff --git a/frontend/src/party/view/SubmissionGallery.tsx b/frontend/src/party/view/SubmissionGallery.tsx index 800868a..f34902f 100644 --- a/frontend/src/party/view/SubmissionGallery.tsx +++ b/frontend/src/party/view/SubmissionGallery.tsx @@ -54,7 +54,7 @@ function SubmissionGallery({ submissions }: { submissions: PartySubmissionRespon {submissions.map((submission, i) => { const currentDimensions = dimensions.get(submission.id); return ( -