Skip to content

Commit 208b8a3

Browse files
authored
[frontend]: picture showcase on homepage (#21)
* collage preview * cleanup
1 parent 6150804 commit 208b8a3

19 files changed

+572
-495
lines changed

docker-compose.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ services:
1818
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /tmp/group-challenge-cache
1919
IMGPROXY_BIND: ":8081"
2020
IMGPROXY_PROMETHEUS_BIND: ":8082"
21+
IMGPROXY_MAX_SRC_RESOLUTION: 30
2122
volumes:
2223
- type: bind
2324
source: /tmp/group-challenge-cache

frontend/package-lock.json

+245-229
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

+11-11
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,30 @@
1010
},
1111
"dependencies": {
1212
"@headlessui/react": "1.7.14",
13-
"@tanstack/react-query": "4.29.3",
14-
"date-fns": "2.29.3",
13+
"@tanstack/react-query": "4.29.7",
14+
"date-fns": "2.30.0",
1515
"react": "18.2.0",
16-
"react-date-picker": "10.0.3",
16+
"react-date-picker": "10.1.0",
1717
"react-dom": "18.2.0",
1818
"react-hook-form": "7.43.9",
1919
"react-icons": "4.8.0",
2020
"react-input-emoji": "5.0.2",
21-
"react-router": "6.10.0",
22-
"react-router-dom": "6.10.0",
23-
"react-toastify": "9.1.2",
21+
"react-router": "6.11.2",
22+
"react-router-dom": "6.11.2",
23+
"react-toastify": "9.1.3",
2424
"reconnecting-websocket": "4.4.0",
25-
"use-local-storage-state": "18.3.2"
25+
"use-local-storage-state": "18.3.3"
2626
},
2727
"devDependencies": {
28-
"@types/react": "18.0.37",
29-
"@types/react-dom": "18.0.11",
28+
"@types/react": "18.2.6",
29+
"@types/react-dom": "18.2.4",
3030
"@vitejs/plugin-react": "4.0.0",
3131
"@types/react-router": "5.1.20",
3232
"@types/react-router-dom": "5.3.3",
3333
"autoprefixer": "10.4.14",
3434
"postcss": "8.4.23",
35-
"tailwindcss": "3.3.1",
35+
"tailwindcss": "3.3.2",
3636
"typescript": "5.0.4",
37-
"vite": "4.3.0"
37+
"vite": "4.3.8"
3838
}
3939
}

frontend/src/App.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const EditParty = lazy(() => import('./party/edit/EditParty'));
1616
const OwnSubmissions = lazy(() => import('./party/submissions/OwnSubmissions'));
1717
const EditProfile = lazy(() => import('./user/EditProfile'));
1818
const Home = lazy(() => import('./home/Home'));
19+
const Collage = lazy(() => import('./collage/Collage'));
20+
const Statistics = lazy(() => import('./statistics/Statistics'));
1921

2022
const WithUser: FC = () => {
2123
return (
@@ -28,7 +30,9 @@ const WithUser: FC = () => {
2830
<Route path="edit/:id" element={<EditParty />} />
2931
<Route path="my-submissions/:id" element={<OwnSubmissions />} />
3032
</Route>
33+
<Route path="/collage" element={<Collage />} />
3134
<Route path="/profile" element={<EditProfile />} />
35+
<Route path="/statistics" element={<Statistics />} />
3236
<Route path="/*" element={<Home />} />
3337
</Routes>
3438
</Suspense>
@@ -48,7 +52,7 @@ const queryClient = new QueryClient({
4852
defaultOptions: {
4953
queries: {
5054
retry: 1,
51-
refetchOnWindowFocus: true,
55+
refetchOnWindowFocus: false,
5256
},
5357
},
5458
});
@@ -63,7 +67,7 @@ function App() {
6367
<QueryClientProvider client={queryClient}>
6468
<WebSocketContext.Provider value={{ webSocket }}>
6569
<div className={themeClass}>
66-
<div className="dark:bg-slate-800 dark:text-white min-h-screen">
70+
<div className="print:bg-white bg-slate-100 dark:bg-slate-800 dark:text-white min-h-screen">
6771
<Navigation />
6872
<ToastContainer position="bottom-right" className="text-black" />
6973
<div className="container mx-auto px-4">{session ? <WithUser /> : <WithoutUser />}</div>

frontend/src/Footer.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FunctionComponent } from 'react';
22

33
export const Footer: FunctionComponent = () => {
44
return (
5-
<footer className="m-4 p-4 md:flex md:items-center md:justify-between md:p-6">
5+
<footer className="m-4 p-4 md:flex md:items-center md:justify-between md:p-6 print:hidden">
66
<span className="text-sm sm:text-center">
77
<a href="/" className="hover:underline">
88
Group Challenge

frontend/src/collage/Collage.tsx

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { FC, useEffect, useRef, useState } from 'react';
2+
import { getThumbnailUrl, useParties } from '../api/api';
3+
import { sortSubmissions } from '../party/view/util';
4+
5+
const Collage: FC = () => {
6+
const { data: parties, isLoading, fetchNextPage, hasNextPage } = useParties();
7+
const element = useRef<HTMLDivElement>(null);
8+
const [imageHeight, setImageHeight] = useState(72);
9+
const [gap, setGap] = useState(0);
10+
const [padding, setPadding] = useState(0);
11+
12+
const [nImages, setNImages] = useState(25);
13+
14+
const submissions = sortSubmissions(
15+
(parties?.pages || []).flatMap((page) => page.data.flatMap((party) => party.submissions)).slice(0, nImages)
16+
);
17+
18+
useEffect(() => {
19+
if (hasNextPage && nImages > (submissions.length || 100000)) {
20+
fetchNextPage();
21+
}
22+
}, [nImages, parties?.pages.length, hasNextPage, submissions]);
23+
24+
if (isLoading || !parties) {
25+
return <></>;
26+
}
27+
28+
const nPages = !element.current
29+
? 1
30+
: Math.ceil(
31+
element.current.scrollHeight /
32+
element.current.scrollWidth /
33+
(element.current.offsetHeight / element.current.offsetWidth)
34+
);
35+
36+
return (
37+
<>
38+
<div className="print:hidden flex flex-col space-y-4">
39+
<span>Estimated number of pages: {nPages}</span>
40+
41+
<div>
42+
<label>Number of Images</label>
43+
<input
44+
className="text-black"
45+
type="number"
46+
value={nImages}
47+
min={1}
48+
max={1000}
49+
onChange={(e) => setNImages(+e.target.value)}
50+
/>
51+
</div>
52+
53+
<div>
54+
<label htmlFor="imageHeight">Image height</label>
55+
<input
56+
className="text-black"
57+
type="number"
58+
value={imageHeight}
59+
min={2}
60+
max={100}
61+
onChange={(e) => setImageHeight(+e.target.value)}
62+
/>
63+
</div>
64+
<div>
65+
<label htmlFor="gap">Gap</label>
66+
<input
67+
id="gap"
68+
className="text-black"
69+
type="number"
70+
value={gap}
71+
min={0}
72+
max={12}
73+
onChange={(e) => setGap(+e.target.value)}
74+
/>
75+
</div>
76+
<div>
77+
<label htmlFor="gap">Page Margin</label>
78+
<input
79+
id="margin"
80+
className="text-black"
81+
type="number"
82+
value={padding}
83+
min={0}
84+
max={100}
85+
onChange={(e) => setPadding(+e.target.value)}
86+
/>
87+
</div>
88+
</div>
89+
<div
90+
ref={element}
91+
className="flex flex-row flex-wrap justify-evenly content-start bg-white w-a4 h-a4 m-auto overflow-hidden"
92+
style={{
93+
gap: `${gap}px`,
94+
padding: `${padding}px`,
95+
}}
96+
>
97+
{submissions.map((submission) => (
98+
<img
99+
style={{
100+
height: `${imageHeight}px`,
101+
}}
102+
className="inline-block"
103+
key={submission.id}
104+
src={getThumbnailUrl(submission.imageId)}
105+
alt={submission.name}
106+
/>
107+
))}
108+
</div>
109+
</>
110+
);
111+
};
112+
113+
export default Collage;

frontend/src/home/Home.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Link } from 'react-router-dom';
12
import { useParties } from '../api/api';
23
import { PartyResponse } from '../api/api-models';
34
import { Footer } from '../Footer';
@@ -14,7 +15,7 @@ export function Home() {
1415

1516
const highlightedParty = visibleParties
1617
.filter((party) => !party.done)
17-
.sort((a, b) => new Date(b.endDate).getDate() - new Date(a.endDate).getDate())[0];
18+
.sort((a, b) => new Date(a.endDate).getDate() - new Date(b.endDate).getDate())[0];
1819

1920
return (
2021
<div className="flex flex-col justify-between">
@@ -25,6 +26,16 @@ export function Home() {
2526
<HighlightedParty party={highlightedParty} />
2627
</div>
2728
)}
29+
{!highlightedParty && (
30+
<Link
31+
to={'/party/create'}
32+
className="flex place-items-center space-x-2 text-blue-500 hover:opacity-75 cursor-pointer"
33+
>
34+
<div className="w-full h-36 border-2 font-bold text-xl rounded border-dashed border-gray-700 hover:bg-slate-200 hover:dark:bg-slate-700 flex items-center justify-center">
35+
Create New Challenge
36+
</div>
37+
</Link>
38+
)}
2839
<PartyTimelines parties={visibleParties} />
2940
{hasNextPage && (
3041
<button onClick={() => fetchNextPage()} className="hover:text-slate-500 px-4 py-2 rounded">

frontend/src/index.css

-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,3 @@
33
@tailwind base;
44
@tailwind components;
55
@tailwind utilities;
6-
7-
body {
8-
background-color: aliceblue;
9-
}

frontend/src/navigation/Navigation.tsx

+12-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { ThemeButton } from '../components/ThemeButton';
66
import { useSession } from '../user/session';
77
import { VERSION } from '../version';
88

9+
const enableBetaFeatures = false;
10+
911
function Navigation() {
1012
const [session, setSession] = useSession();
1113
const signOutAndRemoveSession = async () => {
@@ -18,7 +20,7 @@ function Navigation() {
1820
};
1921

2022
return (
21-
<header className="body-font">
23+
<header className="body-font print:hidden">
2224
<div className="container mx-auto flex flex-wrap p-5 mb-8 flex-col md:flex-row items-center justify-between border-b-2 border-gray-600">
2325
<div className="flex title-font font-medium items-center mb-4 md:mb-0 space-x-2">
2426
<Link to="/">
@@ -28,13 +30,19 @@ function Navigation() {
2830
<span className="text-sm font-bold pr-4 hover:text-gray-500 hover:underline">{VERSION}</span>
2931
</Link>
3032

31-
{session && (
33+
{session && enableBetaFeatures && (
3234
<>
3335
<Link
3436
className="hover:text-white hover:bg-blue-500 hover:outline-cyan-500 focus:ring-cyan-500 font-medium rounded px-5 py-1 text-center"
35-
to="/party/create"
37+
to="/collage"
38+
>
39+
Collage (beta)
40+
</Link>
41+
<Link
42+
className="hover:text-white hover:bg-blue-500 hover:outline-cyan-500 focus:ring-cyan-500 font-medium rounded px-5 py-1 text-center"
43+
to="/statistics"
3644
>
37-
Create a Challenge
45+
Statistics
3846
</Link>
3947
</>
4048
)}

frontend/src/party/HighlightedParty.tsx

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
11
import { formatRelative } from 'date-fns';
22
import { FaCalendarDay, FaTv, FaUserAlt } from 'react-icons/fa';
3-
import { usePartyStatus, useUser } from '../api/api';
3+
import { startParty, usePartyStatus, useUser } from '../api/api';
44
import { isPartyLive, PartyResponse } from '../api/api-models';
55
import { useSession } from '../user/session';
6-
import { PartyHostMenu } from './PartyHostMenu';
6+
import Button from '../components/Button';
7+
import { useMutation } from '@tanstack/react-query';
78

89
function HighlightedParty({ party }: { party: PartyResponse }) {
910
const partyStatus = usePartyStatus(party.id);
1011
const [session] = useSession();
1112
const { data: host } = useUser(party.userId);
13+
const { mutateAsync: startMutateAsync } = useMutation(startParty);
1214

1315
const isLive = partyStatus.isSuccess && isPartyLive(partyStatus.data);
1416
const isHost = party?.userId === session?.userId;
1517
const numOwnSubmissions = party.submissions.filter((submission) => submission.userId === session?.userId).length;
1618

19+
const onStartPartyButton = async () => {
20+
if (isLive) {
21+
return;
22+
}
23+
24+
if (window.confirm(`Do you want to start the party ${party.name}?`))
25+
await startMutateAsync({ partyId: party.id, sessionToken: session!.token });
26+
};
27+
1728
return (
1829
<div>
19-
<div className="relative bg-gradient-to-r from-blue-100 to-blue-300 transition-all duration-500 bg-size-200 rounded-md dark:from-slate-700 dark:to-slate-600 dark:text-white">
30+
<div className="relative transition-all duration-500 bg-size-200 rounded-md dark:bg-slate-700 dark:text-white">
2031
{isLive && (
2132
<span className="bg-blue-500 text-white font-bold px-3 py-3 tracking-widest flex uppercase justify-between rounded-t-md">
2233
<span className="flex items-center space-x-2">
@@ -69,7 +80,14 @@ function HighlightedParty({ party }: { party: PartyResponse }) {
6980
</div>
7081

7182
<div className="flex items-center space-x-4 w-full border-t border-white p-4 mt-4 dark:border-slate-500">
72-
{isHost && <PartyHostMenu party={party} />}
83+
{isHost && (
84+
<>
85+
{!isLive && <Button onClick={onStartPartyButton}>Start</Button>}
86+
<a className="hover:underline font-bold" href={'/party/edit/' + party.id}>
87+
Edit
88+
</a>
89+
</>
90+
)}
7391

7492
<a className="hover:underline font-bold" href={'/party/my-submissions/' + party.id}>
7593
Upload

0 commit comments

Comments
 (0)