From 594e81f9e8fd4e2bf3d2039f19d64f3d0d3555c5 Mon Sep 17 00:00:00 2001 From: Chris Rybicki Date: Tue, 8 Aug 2023 19:01:39 -0400 Subject: [PATCH 1/7] fix bug --- website/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/App.tsx b/website/src/App.tsx index 77700bb..1b7ebcb 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -133,7 +133,7 @@ const Voting = (props: VotingProps) => {

{choices[1]}

- {renderVoteButtonOrOutcome(0)} + {renderVoteButtonOrOutcome(1)}
{ winner !== null && ( From 325b7dd52a2c71e4fb553e2138fc1ba41ad43c0d Mon Sep 17 00:00:00 2001 From: Chris Rybicki Date: Wed, 9 Aug 2023 09:31:00 -0400 Subject: [PATCH 2/7] better react --- website/package-lock.json | 3 ++ website/package.json | 3 ++ website/src/App.tsx | 56 +++++++++++++++++++++++++++++--------- website/src/index.css | 4 +++ website/tailwind.config.js | 11 ++++++++ 5 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 website/tailwind.config.js diff --git a/website/package-lock.json b/website/package-lock.json index 18c56df..36646bc 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -20,6 +20,9 @@ "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "tailwindcss": "^3.3.3" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/website/package.json b/website/package.json index 30ed611..4d81aaa 100644 --- a/website/package.json +++ b/website/package.json @@ -39,5 +39,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "tailwindcss": "^3.3.3" } } diff --git a/website/src/App.tsx b/website/src/App.tsx index 1b7ebcb..d185e59 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -91,15 +91,27 @@ interface VotingProps { swapViews: () => void; } +const Loading = ({visible} : {visible: boolean}) => { + return visible ?
Loading...
: null; +} + const Voting = (props: VotingProps) => { const [choices, setChoices] = useState([]); const [scores, setScores] = useState<(number | null)[]>([null, null]); + const [loading, setLoading] = useState(false); + const [loadingScores, setLoadingScores] = useState(false); + useEffect(() => { - fetchChoices().then((choices) => setChoices(choices)); + setLoading(true); + fetchChoices().then((choices) => { + setChoices(choices); + setLoading(false); + }); }, []); const [winner, setWinner] = useState(null); const selectWinner = async (winner: string, loser: string) => { + setLoadingScores(true); const { winner: winnerScore, loser: loserScore } = await submitVote(winner, loser); if (winner === choices[0]) { setScores([winnerScore, loserScore]); @@ -107,41 +119,58 @@ const Voting = (props: VotingProps) => { setScores([loserScore, winnerScore]); } setWinner(winner); + setLoadingScores(false); }; const reset = async () => { setWinner(null); + setLoading(true); setScores([null, null]); const choices = await fetchChoices(); setChoices(choices); + setLoading(false); }; const renderVoteButtonOrOutcome = (idx: number) => { + if (loadingScores) { + return + } if (winner === null) { - return ; + return ; + } else { + return

{winner === choices[idx] ? "Winner" : "Loser"} (new score: {scores[idx]})

} - return

{winner === choices[idx] ? "Winner" : "Loser"} (new score: {scores[idx]})

} return ( -
-

Which is better?

-
+
+

Which is better?

+
-

{choices[0]}

- {renderVoteButtonOrOutcome(0)} + + {!loading && ( +
+

{choices[0]}

+ {renderVoteButtonOrOutcome(0)} +
+ )}
-

{choices[1]}

- {renderVoteButtonOrOutcome(1)} -
+ + {!loading && ( +
+

{choices[1]}

+ {renderVoteButtonOrOutcome(1)} +
+ )} +
{ winner !== null && ( - + ) }
- +
) } @@ -150,6 +179,7 @@ type View = "voting" | "leaderboard"; function App() { let [view, setView] = useState("voting"); + const swapViews = () => { setView(view === "voting" ? "leaderboard" : "voting"); }; diff --git a/website/src/index.css b/website/src/index.css index ec2585e..17df0e7 100644 --- a/website/src/index.css +++ b/website/src/index.css @@ -1,3 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', diff --git a/website/tailwind.config.js b/website/tailwind.config.js new file mode 100644 index 0000000..c0958ec --- /dev/null +++ b/website/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} + From c679ba31c413030e7937c0812214c63bad9dbe9d Mon Sep 17 00:00:00 2001 From: polamoros Date: Wed, 9 Aug 2023 15:58:46 +0200 Subject: [PATCH 3/7] wip --- package-lock.json | 2 +- website/package-lock.json | 6 + website/package.json | 1 + website/src/App.tsx | 211 ++++------------------- website/src/components/Button.tsx | 36 ++++ website/src/components/SpinnerLoader.tsx | 33 ++++ website/src/components/VoteItem.tsx | 58 +++++++ website/src/services/fetchChoices.ts | 13 ++ website/src/services/fetchConfig.ts | 12 ++ website/src/services/fetchLeaderboard.ts | 16 ++ website/src/services/submitVote.ts | 14 ++ website/src/views/LeaderboardView.tsx | 76 ++++++++ website/src/views/VotingView.tsx | 87 ++++++++++ 13 files changed, 382 insertions(+), 183 deletions(-) create mode 100644 website/src/components/Button.tsx create mode 100644 website/src/components/SpinnerLoader.tsx create mode 100644 website/src/components/VoteItem.tsx create mode 100644 website/src/services/fetchChoices.ts create mode 100644 website/src/services/fetchConfig.ts create mode 100644 website/src/services/fetchLeaderboard.ts create mode 100644 website/src/services/submitVote.ts create mode 100644 website/src/views/LeaderboardView.tsx create mode 100644 website/src/views/VotingView.tsx diff --git a/package-lock.json b/package-lock.json index 39f72b6..9e506a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "wing-voting", + "name": "voting-app", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/website/package-lock.json b/website/package-lock.json index 36646bc..a702b5a 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -15,6 +15,7 @@ "@types/node": "^20.4.8", "@types/react": "^18.2.18", "@types/react-dom": "^18.2.7", + "classnames": "^2.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", @@ -5950,6 +5951,11 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clean-css": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", diff --git a/website/package.json b/website/package.json index 4d81aaa..94ff25e 100644 --- a/website/package.json +++ b/website/package.json @@ -10,6 +10,7 @@ "@types/node": "^20.4.8", "@types/react": "^18.2.18", "@types/react-dom": "^18.2.7", + "classnames": "^2.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", diff --git a/website/src/App.tsx b/website/src/App.tsx index d185e59..64fa4ab 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -1,195 +1,42 @@ -import React, { useState, useEffect } from "react"; +import { useState } from "react"; +import { LeaderboardView } from "./views/LeaderboardView"; +import { VotingView } from "./views/VotingView"; +import { Button } from "./components/Button"; -interface Config { - apiUrl: string; -} - -interface Entry { - name: string; - score: number; -} - -const fetchConfig = async () => { - const response = await fetch("./config.json"); - if (!response.ok) { - throw new Error('Failed to fetch config'); - } - const config: Config = await response.json(); - return config; -} - -const fetchLeaderboard = async () => { - const apiUrl = (await fetchConfig()).apiUrl; - const response = await fetch(apiUrl + "/leaderboard"); - if (!response.ok) { - throw new Error('Failed to fetch leaderboard data'); - } - const jsonData: Entry[] = await response.json(); - return jsonData; -} - -const fetchChoices = async () => { - const apiUrl = (await fetchConfig()).apiUrl; - const response = await fetch(apiUrl + "/requestChoices", { - method: "POST", - }); - if (!response.ok) { - throw new Error('Failed to request choices'); - } - const jsonData: string[] = await response.json(); - return jsonData; -} - -const submitVote = async (winner: string, loser: string) => { - const apiUrl = (await fetchConfig()).apiUrl; - const response = await fetch(apiUrl + "/selectWinner", { - method: "POST", - body: JSON.stringify({ winner, loser }), - }); - if (!response.ok) { - console.error('Failed to submit vote'); - } - const jsonData: { winner: number; loser: number; } = await response.json(); - return jsonData; -} - -interface LeaderboardProps { - swapViews: () => void; -} - -const Leaderboard = (props: LeaderboardProps) => { - const [entries, setEntries] = useState([]); - useEffect(() => { - fetchLeaderboard().then((items) => setEntries(items)); - }, []); - - return ( -
-

Leaderboard

- - - - - - - - - {entries.sort((a, b) => b.score - a.score).map((item) => ( - - - - - ))} - -
NameScore
{item.name}{item.score}
- -
- ); -}; - -interface VotingProps { - swapViews: () => void; -} +type View = "voting" | "leaderboard"; -const Loading = ({visible} : {visible: boolean}) => { - return visible ?
Loading...
: null; -} - -const Voting = (props: VotingProps) => { - const [choices, setChoices] = useState([]); - const [scores, setScores] = useState<(number | null)[]>([null, null]); - const [loading, setLoading] = useState(false); - const [loadingScores, setLoadingScores] = useState(false); - - useEffect(() => { - setLoading(true); - fetchChoices().then((choices) => { - setChoices(choices); - setLoading(false); - }); - }, []); +function App() { + let [view, setView] = useState("voting"); - const [winner, setWinner] = useState(null); - const selectWinner = async (winner: string, loser: string) => { - setLoadingScores(true); - const { winner: winnerScore, loser: loserScore } = await submitVote(winner, loser); - if (winner === choices[0]) { - setScores([winnerScore, loserScore]); - } else { - setScores([loserScore, winnerScore]); - } - setWinner(winner); - setLoadingScores(false); + const swapViews = () => { + setView(view === "voting" ? "leaderboard" : "voting"); }; - const reset = async () => { - setWinner(null); - setLoading(true); - setScores([null, null]); - const choices = await fetchChoices(); - setChoices(choices); - setLoading(false); - }; + return ( +
+
+
+
+ {view === "voting" ? "Which is better?" : "Leaderboard"} +
- const renderVoteButtonOrOutcome = (idx: number) => { - if (loadingScores) { - return - } - if (winner === null) { - return ; - } else { - return

{winner === choices[idx] ? "Winner" : "Loser"} (new score: {scores[idx]})

- } - } +
+ {view === "voting" && } + {view === "leaderboard" && } +
- return ( -
-

Which is better?

-
-
- - {!loading && ( -
-

{choices[0]}

- {renderVoteButtonOrOutcome(0)} -
- )} -
-
- - {!loading && ( -
-

{choices[1]}

- {renderVoteButtonOrOutcome(1)} -
- )} +
+ {view === "voting" && ( +
- { - winner !== null && ( - - ) - } +
-
- ) -} - -type View = "voting" | "leaderboard"; - -function App() { - let [view, setView] = useState("voting"); - - const swapViews = () => { - setView(view === "voting" ? "leaderboard" : "voting"); - }; - - switch (view) { - case "voting": - return ; - case "leaderboard": - return ; - } + ); } export default App; diff --git a/website/src/components/Button.tsx b/website/src/components/Button.tsx new file mode 100644 index 0000000..ac244f2 --- /dev/null +++ b/website/src/components/Button.tsx @@ -0,0 +1,36 @@ +import classnames from "classnames"; +import { SpinnerLoader } from "./SpinnerLoader"; + +export interface ButtonProps { + label: string; + primary?: boolean; + onClick: () => void; + loading?: boolean; + disabled?: boolean; +} + +export const Button = ({ + label, + primary = false, + onClick, + loading = false, + disabled = false, +}: ButtonProps) => { + return ( + + ); +}; diff --git a/website/src/components/SpinnerLoader.tsx b/website/src/components/SpinnerLoader.tsx new file mode 100644 index 0000000..69042e2 --- /dev/null +++ b/website/src/components/SpinnerLoader.tsx @@ -0,0 +1,33 @@ +import classNames from "classnames"; + +export interface SpinnerLoaderProps { + className?: string; +} + +export const SpinnerLoader = ({ className }: SpinnerLoaderProps) => { + return ( +
+ + Loading... +
+ ); +}; diff --git a/website/src/components/VoteItem.tsx b/website/src/components/VoteItem.tsx new file mode 100644 index 0000000..2e66107 --- /dev/null +++ b/website/src/components/VoteItem.tsx @@ -0,0 +1,58 @@ +import { Button } from "./Button"; +import classnames from "classnames"; + +export interface VoteItemProps { + name: string; + imageUrl: string; + onClick: () => void; + disabled?: boolean; + loading?: boolean; + winner?: string; + score?: number; +} + +export const VoteItem = ({ + name, + onClick, + imageUrl, + disabled, + loading, + winner, + score, +}: VoteItemProps) => { + return ( +
+
+

{name}

+
+
+ {imageUrl !== "" && ( + {name} + )} +
+ {!winner && ( +
+
+ ); +}; diff --git a/website/src/services/fetchChoices.ts b/website/src/services/fetchChoices.ts new file mode 100644 index 0000000..bb2020f --- /dev/null +++ b/website/src/services/fetchChoices.ts @@ -0,0 +1,13 @@ +import { fetchConfig } from "./fetchConfig"; + +export const fetchChoices = async () => { + const apiUrl = (await fetchConfig()).apiUrl; + const response = await fetch(apiUrl + "/requestChoices", { + method: "POST", + }); + if (!response.ok) { + throw new Error("Failed to request choices"); + } + const jsonData: string[] = await response.json(); + return jsonData; +}; diff --git a/website/src/services/fetchConfig.ts b/website/src/services/fetchConfig.ts new file mode 100644 index 0000000..cfdb896 --- /dev/null +++ b/website/src/services/fetchConfig.ts @@ -0,0 +1,12 @@ +export interface Config { + apiUrl: string; +} + +export const fetchConfig = async () => { + const response = await fetch("./config.json"); + if (!response.ok) { + throw new Error("Failed to fetch config"); + } + const config: Config = await response.json(); + return config; +}; diff --git a/website/src/services/fetchLeaderboard.ts b/website/src/services/fetchLeaderboard.ts new file mode 100644 index 0000000..fab5069 --- /dev/null +++ b/website/src/services/fetchLeaderboard.ts @@ -0,0 +1,16 @@ +import { fetchConfig } from "./fetchConfig"; + +export interface Entry { + name: string; + score: number; +} + +export const fetchLeaderboard = async () => { + const apiUrl = (await fetchConfig()).apiUrl; + const response = await fetch(apiUrl + "/leaderboard"); + if (!response.ok) { + throw new Error("Failed to fetch leaderboard data"); + } + const jsonData: Entry[] = await response.json(); + return jsonData; +}; diff --git a/website/src/services/submitVote.ts b/website/src/services/submitVote.ts new file mode 100644 index 0000000..fbdce51 --- /dev/null +++ b/website/src/services/submitVote.ts @@ -0,0 +1,14 @@ +import { fetchConfig } from "./fetchConfig"; + +export const submitVote = async (winner: string, loser: string) => { + const apiUrl = (await fetchConfig()).apiUrl; + const response = await fetch(apiUrl + "/selectWinner", { + method: "POST", + body: JSON.stringify({ winner, loser }), + }); + if (!response.ok) { + console.error("Failed to submit vote"); + } + const jsonData: { winner: number; loser: number } = await response.json(); + return jsonData; +}; diff --git a/website/src/views/LeaderboardView.tsx b/website/src/views/LeaderboardView.tsx new file mode 100644 index 0000000..2a662d4 --- /dev/null +++ b/website/src/views/LeaderboardView.tsx @@ -0,0 +1,76 @@ +import { useEffect, useState } from "react"; +import { Entry, fetchLeaderboard } from "../services/fetchLeaderboard"; +import { SpinnerLoader } from "../components/SpinnerLoader"; + +export const LeaderboardView = () => { + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchLeaderboard().then((items) => { + setEntries(items); + setLoading(false); + }); + }, []); + + return ( +
+
+
+
+ + + + + + + + + {loading && ( + + + + + )} + + {entries + .sort((a, b) => b.score - a.score) + .map((item, index) => ( + + + + + ))} + +
+ Name + + Score +
+ + + +
+ {item.name} + +
+
+ {index === 0 && "🥇"} + {index === 1 && "🥈"} + {index === 2 && "🥉"} +
+
{Math.max(item.score, 0)}
+
+
+
+
+
+
+ ); +}; diff --git a/website/src/views/VotingView.tsx b/website/src/views/VotingView.tsx new file mode 100644 index 0000000..0ea5f8e --- /dev/null +++ b/website/src/views/VotingView.tsx @@ -0,0 +1,87 @@ +import { useEffect, useState } from "react"; +import { Button } from "../components/Button"; +import { fetchChoices } from "../services/fetchChoices"; +import { submitVote } from "../services/submitVote"; +import { VoteItem } from "../components/VoteItem"; + +export const VotingView = () => { + const [choices, setChoices] = useState(["", ""]); + const [scores, setScores] = useState([]); + + const [loading, setLoading] = useState(true); + + const [selectedWinnerIdx, setSelectedWinnerIdx] = useState(); + const [loadingScores, setLoadingScores] = useState(false); + + useEffect(() => { + fetchChoices().then((choices) => { + setChoices(choices); + setLoading(false); + }); + }, []); + + const [winner, setWinner] = useState(); + const selectWinner = async (winner: string) => { + const loser = choices.find((choice) => choice !== winner)!; + + setLoadingScores(true); + setSelectedWinnerIdx(choices.indexOf(winner)); + const { winner: winnerScore, loser: loserScore } = await submitVote( + winner, + loser + ); + if (winner === choices[0]) { + setScores([winnerScore, loserScore]); + } else { + setScores([loserScore, winnerScore]); + } + setWinner(winner); + setLoadingScores(false); + }; + + const reset = async () => { + setWinner(undefined); + setLoading(true); + setScores([]); + const choices = await fetchChoices(); + setChoices(choices); + console.log(choices); + setLoading(false); + }; + + return ( +
+
+
+ {choices.map((choice, index) => ( +
+ selectWinner(choice)} + disabled={loadingScores} + loading={loadingScores && selectedWinnerIdx === index} + winner={winner} + score={scores[index]} + /> +
+ ))} +
+ {winner !== null && ( +
+
+ )} +
+
+ ); +}; From 4854367d960017234458ab9ab8e79f660b9fbd0b Mon Sep 17 00:00:00 2001 From: polamoros Date: Thu, 10 Aug 2023 14:02:50 +0200 Subject: [PATCH 4/7] wip --- website/src/App.tsx | 8 +- website/src/components/Button.tsx | 7 +- website/src/components/VoteItem.tsx | 25 +++--- website/src/views/LeaderboardView.tsx | 106 ++++++++++++-------------- website/src/views/VotingView.tsx | 57 +++++++------- 5 files changed, 102 insertions(+), 101 deletions(-) diff --git a/website/src/App.tsx b/website/src/App.tsx index 64fa4ab..8e0f40f 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -15,17 +15,19 @@ function App() { return (
-
+
{view === "voting" ? "Which is better?" : "Leaderboard"}
-
+
{view === "voting" && } {view === "leaderboard" && }
-
+
+ +
{view === "voting" && (
+ )}
); }; From d67ea7f3a817b5752d58820562116ec39ea2968f Mon Sep 17 00:00:00 2001 From: polamoros Date: Thu, 10 Aug 2023 15:15:44 +0200 Subject: [PATCH 5/7] wip --- website/src/components/SpinnerLoader.tsx | 2 +- website/src/views/LeaderboardView.tsx | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/website/src/components/SpinnerLoader.tsx b/website/src/components/SpinnerLoader.tsx index 69042e2..c0f4e86 100644 --- a/website/src/components/SpinnerLoader.tsx +++ b/website/src/components/SpinnerLoader.tsx @@ -10,7 +10,7 @@ export const SpinnerLoader = ({ className }: SpinnerLoaderProps) => {
- +
+
{loading && ( - - )} From 1a72f3998e4212ae6d78b3ef9a0784c881ab94d8 Mon Sep 17 00:00:00 2001 From: polamoros Date: Thu, 10 Aug 2023 15:16:28 +0200 Subject: [PATCH 6/7] wip --- website/src/views/LeaderboardView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/views/LeaderboardView.tsx b/website/src/views/LeaderboardView.tsx index 912c521..112127a 100644 --- a/website/src/views/LeaderboardView.tsx +++ b/website/src/views/LeaderboardView.tsx @@ -39,7 +39,7 @@ export const LeaderboardView = () => { colSpan={2} className="whitespace-nowrap px-3 py-4 text-sm text-slate-500 text-center w-32" > -
+
From 02ceb92629a9e7f6a7be74d111182a1a696c073b Mon Sep 17 00:00:00 2001 From: Chris Rybicki Date: Thu, 10 Aug 2023 21:22:09 -0400 Subject: [PATCH 7/7] remove log --- website/src/views/VotingView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/views/VotingView.tsx b/website/src/views/VotingView.tsx index 35af7b7..25867f3 100644 --- a/website/src/views/VotingView.tsx +++ b/website/src/views/VotingView.tsx @@ -45,7 +45,6 @@ export const VotingView = () => { setScores([]); const choices = await fetchChoices(); setChoices(choices); - console.log(choices); setLoading(false); };
{
- - - + +
+ +