diff --git a/website/src/App.tsx b/website/src/App.tsx index b463473..049d730 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -15,8 +15,10 @@ function App() { return (
-

Battle of the Bites!

-
+

+ Battle of the Bites! +

+
{view === "voting" ? "Which is better?" : "Leaderboard"}
diff --git a/website/src/components/VoteItem.tsx b/website/src/components/VoteItem.tsx index 13d5e3a..fb5ab93 100644 --- a/website/src/components/VoteItem.tsx +++ b/website/src/components/VoteItem.tsx @@ -1,10 +1,11 @@ import { Button } from "./Button"; import classnames from "classnames"; import { SpinnerLoader } from "./SpinnerLoader"; +import { useMemo } from "react"; export interface VoteItemProps { name: string; - imageUrl: string; + image?: string; onClick: () => void; disabled?: boolean; loading?: boolean; @@ -15,27 +16,64 @@ export interface VoteItemProps { export const VoteItem = ({ name, onClick, - imageUrl, + image, disabled, loading, winner, score, }: VoteItemProps) => { + const rotateStyle = useMemo(() => { + return { + transform: winner && "rotateY(180deg)", + }; + }, [winner]); + return (
-

{name}

-
-
- -
- {imageUrl !== "" && ( - {name} +
+

+ {winner && (winner === name ? "🥇" : "🥈")} + {!winner && name} +

+ +
+
+ +
+ + {winner && ( +
+ Score: {Math.max(score!, 0)} +
+ )} + + {!winner && image && ( + {name} + )} +
+
{!winner && (
); diff --git a/website/src/services/fetchChoices.ts b/website/src/services/fetchChoices.ts index bb2020f..89f830f 100644 --- a/website/src/services/fetchChoices.ts +++ b/website/src/services/fetchChoices.ts @@ -1,5 +1,24 @@ import { fetchConfig } from "./fetchConfig"; +export interface Choice { + label: string; + imageSvg?: string; +} + +const getImageSvg = async (label: string): Promise => { + const url = `https://source.unsplash.com/featured/128x128/?${label}&category=food`; + const response = await fetch(url); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + resolve(reader.result as string); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +}; + export const fetchChoices = async () => { const apiUrl = (await fetchConfig()).apiUrl; const response = await fetch(apiUrl + "/requestChoices", { @@ -8,6 +27,13 @@ export const fetchChoices = async () => { if (!response.ok) { throw new Error("Failed to request choices"); } - const jsonData: string[] = await response.json(); - return jsonData; + const labels: string[] = await response.json(); + + const choices = await Promise.all( + labels.map(async (label) => { + const imageSvg = await getImageSvg(label); + return { label, imageSvg }; + }) + ); + return choices; }; diff --git a/website/src/views/LeaderboardView.tsx b/website/src/views/LeaderboardView.tsx index 0662216..fb70137 100644 --- a/website/src/views/LeaderboardView.tsx +++ b/website/src/views/LeaderboardView.tsx @@ -14,7 +14,7 @@ export const LeaderboardView = () => { }, []); return ( -
+
diff --git a/website/src/views/VotingView.tsx b/website/src/views/VotingView.tsx index 468bdb2..8d91112 100644 --- a/website/src/views/VotingView.tsx +++ b/website/src/views/VotingView.tsx @@ -1,16 +1,18 @@ import { useEffect, useState } from "react"; import { Button } from "../components/Button"; -import { fetchChoices } from "../services/fetchChoices"; +import { Choice, fetchChoices } from "../services/fetchChoices"; import { submitVote } from "../services/submitVote"; import { VoteItem } from "../components/VoteItem"; export const VotingView = () => { - const [choices, setChoices] = useState(["", ""]); + const [choices, setChoices] = useState([ + { label: "" }, + { label: "" }, + ]); const [scores, setScores] = useState([]); const [loading, setLoading] = useState(true); - const [selectedWinnerIdx, setSelectedWinnerIdx] = useState(); const [loadingScores, setLoadingScores] = useState(false); useEffect(() => { @@ -21,21 +23,23 @@ export const VotingView = () => { }, []); const [winner, setWinner] = useState(); - const selectWinner = async (winner: string) => { - const loser = choices.find((choice) => choice !== winner)!; + const [selectedChoice, setSelectedChoice] = useState(); + + const selectWinner = async (winner: Choice) => { + setSelectedChoice(winner); + const loser = choices.find((choice) => choice.label !== winner.label)!; setLoadingScores(true); - setSelectedWinnerIdx(choices.indexOf(winner)); const { winner: winnerScore, loser: loserScore } = await submitVote( - winner, - loser + winner.label, + loser.label ); if (winner === choices[0]) { setScores([winnerScore, loserScore]); } else { setScores([loserScore, winnerScore]); } - setWinner(winner); + setWinner(winner.label); setLoadingScores(false); }; @@ -43,6 +47,7 @@ export const VotingView = () => { setWinner(undefined); setLoading(true); setScores([]); + setChoices([{ label: "" }, { label: "" }]); const choices = await fetchChoices(); setChoices(choices); setLoading(false); @@ -50,20 +55,16 @@ export const VotingView = () => { return (
-
+
{choices.map((choice, index) => ( -
+
selectWinner(choice)} disabled={loading || loadingScores} - loading={loadingScores && selectedWinnerIdx === index} + loading={loadingScores && selectedChoice?.label === choice.label} winner={winner} score={Math.floor(scores[index])} />