diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e69de29..0000000 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/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/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", diff --git a/frontend/package.json b/frontend/package.json index 36fd83c..b5dfdcd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,11 +12,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", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9f410ce..ae9e83c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -66,7 +66,7 @@ function App() {
-
{session ? : }
+
{session ? : }
diff --git a/frontend/src/api/api-models.ts b/frontend/src/api/api-models.ts index cb6f2ad..d40faf6 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 = 'lobby', + RUNNING = 'running', + 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/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 3262e78..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} -
+ {submission?.name} ))}
diff --git a/frontend/src/party/timeline/PartyTimeline.tsx b/frontend/src/party/timeline/PartyTimeline.tsx index 3fba54e..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/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 new file mode 100644 index 0000000..f34902f --- /dev/null +++ b/frontend/src/party/view/SubmissionGallery.tsx @@ -0,0 +1,92 @@ +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 ( + + ); + })} + + ); +} + +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 7d12993..ea74e82 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'; @@ -53,11 +53,10 @@ const ViewPartyContent = ({ return
Unknown party or party status
; } - if (party.done || partyStatus.state === 'done') { + if (party.done || partyStatus.state === PartyStatusState.DONE) { return ; } - - if (partyStatus.state === 'open') { + if (partyStatus.state === PartyStatusState.OPEN) { onRedirect(); return null; } @@ -68,7 +67,7 @@ const ViewPartyContent = ({
); - if (partyStatus.state === 'waitinglobby') { + if (partyStatus.state === PartyStatusState.LOBBY) { return ( @@ -98,9 +97,9 @@ const ViewPartyContent = ({ ); } - if (partyStatus.state === 'prereveal') { + if (partyStatus.state === PartyStatusState.PRE_REVEAL) { return ( -
+

Thanks for voting!

Waiting for host...

@@ -112,7 +111,7 @@ const ViewPartyContent = ({ ); } - if (partyStatus.state === 'reveal') { + if (partyStatus.state === PartyStatusState.REVEAL) { return (
@@ -171,7 +170,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 === PartyStatusState.PRE_REVEAL); 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.name} {submission.description} -
-
Ø {avgRatingTwoDecimals(submission.votes)}
-
-
{totalRating(submission.votes)}
- -
-
{submission.votes.length} vote(s)
-
-
- ))} +
+
); 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 e44a1a3..2891bec 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 bae9255..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} 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}.
} - +
); } diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 802d333..e150b6a 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -9,6 +9,14 @@ export interface Change { } export const CHANGES: Change[] = [ + { + name: '0.15.0', + changes: [ + { description: 'Added gallery to party view', type: 'feature' }, + { description: 'Use more space for photo display in running party', type: 'note' }, + { description: 'Fixed party overview on homepage for Firefox', type: 'fix' }, + ], + }, { name: '0.14.2', changes: [