Skip to content

Commit

Permalink
Merge pull request #13 from winglang/fonts
Browse files Browse the repository at this point in the history
  • Loading branch information
Chriscbr authored Aug 11, 2023
2 parents ca82035 + 8eb42c9 commit 0d18392
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 73 deletions.
3 changes: 3 additions & 0 deletions website/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Gamja+Flower&family=Nanum+Pen+Script&display=swap" rel="stylesheet">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Expand Down
1 change: 0 additions & 1 deletion website/src/App.css

This file was deleted.

11 changes: 7 additions & 4 deletions website/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ function App() {
};

return (
<div className="App font-mono absolute inset-0">
<div className="App absolute inset-0">
<div className="w-full h-full flex flex-col items-center justify-center">
<div className="w-[30rem] min-h-[35rem] px-10 pt-10 pb-4 bg-sky-200 rounded-lg shadow-xl flex flex-col">
<div className="text-slate-700 font-bold text-3xl text-center">
<h1 className="title text-7xl pb-7 font-medium">
Battle of the Bites!
</h1>
<div className="text-2xl w-[30rem] min-h-[35rem] px-8 pt-8 pb-4 bg-sky-200 rounded-lg shadow-xl flex flex-col">
<div className="text-slate-700 text-4xl text-center">
{view === "voting" ? "Which is better?" : "Leaderboard"}
</div>

Expand All @@ -25,7 +28,7 @@ function App() {
{view === "leaderboard" && <LeaderboardView />}
</div>

<div className="w-full h-px bg-slate-300 my-4" />
<div className="w-full h-px bg-slate-400 my-4" />

<div className="w-full flex justify-center truncate">
{view === "voting" && (
Expand Down
120 changes: 88 additions & 32 deletions website/src/components/VoteItem.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,46 +16,101 @@ export interface VoteItemProps {
export const VoteItem = ({
name,
onClick,
imageUrl,
image,
disabled,
loading,
winner,
score,
}: VoteItemProps) => {
const rotateStyle = useMemo(() => {
return {
transform: winner && "rotateY(180deg)",
};
}, [winner]);

const safeName = name.replace(/[^a-z0-9]/gi, "");
const safeScore = score ?? 1500;
const isWinner = winner === name ? -1 : 1;

return (
<div className="text-center">
<h3 className="text-lg truncate h-8 text-slate-700">{name}</h3>
<div className="relative w-32 h-32 mx-auto rounded-lg truncate">
<div className="w-full h-full bg-sky-100 animate-pulse absolute shadow items-center justify-center flex opacity-50">
<SpinnerLoader />
</div>
{imageUrl !== "" && (
<img
className="w-full h-full object-fill absolute z-10"
src={imageUrl}
alt={name}
/>
<div
className={classnames(
"rounded-lg bg-white",
"transition-transform duration-300 w-full h-full transform"
)}
</div>
<div className="pt-3">
{!winner && (
<Button
onClick={onClick}
label="Vote"
loading={loading}
disabled={disabled}
/>
)}
{winner && (
<div
className={classnames(
"h-8",
winner === name ? "text-green-600" : "text-red-600"
)}
>
{winner === name ? "🥇" : "👎"} (Score: {Math.max(score!, 0)})
style={rotateStyle}
>
<h3
className="text-3xl truncate py-2 text-slate-700 px-2 h-12"
style={rotateStyle}
>
{winner && (winner === name ? "🥇" : "🥈")}
{!winner && name}
</h3>

<div
className="relative h-40 mx-auto rounded-b-lg truncate border-t-2 border-slate-500 bg-sky-100"
style={rotateStyle}
>
<div className="absolute inset-0 flex items-center justify-center opacity-50">
<SpinnerLoader />
</div>
)}

{winner && (
<div
className={classnames(
"w-full h-full absolute z-10 pt-5",
winner === name ? "bg-green-100" : "bg-red-100"
)}
>
<style>
{`#${safeName}::after {
content: counter(${safeName});
animation: ${safeName}-anim 1.5s linear;
counter-reset: ${safeName} ${score};
}
@keyframes ${safeName}-anim {
0% { counter-reset: ${safeName} ${safeScore + 15 * isWinner}; }
3% { counter-reset: ${safeName} ${safeScore + 14 * isWinner}; }
6% { counter-reset: ${safeName} ${safeScore + 13 * isWinner}; }
9% { counter-reset: ${safeName} ${safeScore + 12 * isWinner}; }
12% { counter-reset: ${safeName} ${safeScore + 11 * isWinner}; }
15% { counter-reset: ${safeName} ${safeScore + 10 * isWinner}; }
18% { counter-reset: ${safeName} ${safeScore + 9 * isWinner}; }
21% { counter-reset: ${safeName} ${safeScore + 8 * isWinner}; }
24% { counter-reset: ${safeName} ${safeScore + 7 * isWinner}; }
27% { counter-reset: ${safeName} ${safeScore + 6 * isWinner}; }
30% { counter-reset: ${safeName} ${safeScore + 5 * isWinner}; }
33% { counter-reset: ${safeName} ${safeScore + 4 * isWinner}; }
36% { counter-reset: ${safeName} ${safeScore + 3 * isWinner}; }
43% { counter-reset: ${safeName} ${safeScore + 2 * isWinner}; }
66% { counter-reset: ${safeName} ${safeScore + 1 * isWinner}; }
100% { counter-reset: ${safeName} ${safeScore}; }
}`}
</style>
Score: <span id={safeName}></span>
</div>
)}

{!winner && image && (
<img
className="w-full h-full object-cover absolute z-10"
src={`${image}`}
alt={name}
/>
)}
</div>
</div>

<div className="pt-6">
<Button
onClick={onClick}
label="Vote"
loading={loading}
disabled={disabled || winner !== undefined}
/>
</div>
</div>
);
Expand Down
13 changes: 5 additions & 8 deletions website/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@
@tailwind components;
@tailwind utilities;

.title {
font-family: 'Nanum Pen Script', cursive;
}

body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
font-family: 'Gamja Flower', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
30 changes: 28 additions & 2 deletions website/src/services/fetchChoices.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import { fetchConfig } from "./fetchConfig";

export interface Choice {
label: string;
imageSvg?: string;
}

const getImageSvg = async (label: string): Promise<string> => {
const url = `https://source.unsplash.com/featured/128x128/?${label}&category=food`;
const response = await fetch(url);
const blob = await response.blob();
return new Promise<string>((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", {
Expand All @@ -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;
};
16 changes: 8 additions & 8 deletions website/src/views/LeaderboardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,25 @@ export const LeaderboardView = () => {
}, []);

return (
<div className="max-h-[23rem] overflow-y-auto h-full">
<table className="divide-y divide-slate-300 min-w-[20rem] h-full">
<div className="max-h-[22rem] overflow-y-auto h-full">
<table className="divide-y divide-slate-400 min-w-[20rem] h-full">
<thead>
<tr>
<th
scope="col"
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-slate-900 sm:pl-0 w-full"
className="py-3.5 pl-4 pr-3 text-left text-xl font-semibold text-slate-900 sm:pl-0 w-full"
>
Name
</th>
<th
scope="col"
className="px-3 py-3.5 text-right text-sm font-semibold text-slate-900 w-32"
className="px-3 py-3.5 text-right text-xl font-semibold text-slate-900 w-32"
>
Score
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-300">
<tbody className="divide-y divide-slate-400">
{loading && (
<tr>
<td
Expand All @@ -50,12 +50,12 @@ export const LeaderboardView = () => {
.sort((a, b) => b.score - a.score)
.map((item, index) => (
<tr key={item.name}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-slate-900 sm:pl-0">
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-xl text-slate-900 sm:pl-0">
{item.name}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-slate-500 ">
<td className="whitespace-nowrap px-3 py-4 text-xl text-slate-500 ">
<div className="flex justify-end gap-x-2 leading-7">
<div className="text-xl">
<div>
{index === 0 && "🥇"}
{index === 1 && "🥈"}
{index === 2 && "🥉"}
Expand Down
37 changes: 19 additions & 18 deletions website/src/views/VotingView.tsx
Original file line number Diff line number Diff line change
@@ -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<string[]>(["", ""]);
const [choices, setChoices] = useState<Choice[]>([
{ label: "" },
{ label: "" },
]);
const [scores, setScores] = useState<number[]>([]);

const [loading, setLoading] = useState(true);

const [selectedWinnerIdx, setSelectedWinnerIdx] = useState<number>();
const [loadingScores, setLoadingScores] = useState(false);

useEffect(() => {
Expand All @@ -21,49 +23,48 @@ export const VotingView = () => {
}, []);

const [winner, setWinner] = useState<string>();
const selectWinner = async (winner: string) => {
const loser = choices.find((choice) => choice !== winner)!;
const [selectedChoice, setSelectedChoice] = useState<Choice>();

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);
};

const reset = async () => {
setWinner(undefined);
setLoading(true);
setScores([]);
setChoices([{ label: "" }, { label: "" }]);
const choices = await fetchChoices();
setChoices(choices);
setLoading(false);
};

return (
<div className="choices space-y-4">
<div className="flex gap-x-8">
<div className="flex">
{choices.map((choice, index) => (
<div className="w-1/2">
<div className="w-1/2 shrink-0 px-4">
<VoteItem
key={index}
name={choice}
imageUrl={
loading
? ""
: `https://source.unsplash.com/featured/128x128/?${choice}&category=food`
}
name={choice.label}
image={loading ? undefined : choice.imageSvg}
onClick={() => selectWinner(choice)}
disabled={loading || loadingScores}
loading={loadingScores && selectedWinnerIdx === index}
loading={loadingScores && selectedChoice?.label === choice.label}
winner={winner}
score={Math.floor(scores[index])}
/>
Expand Down

0 comments on commit 0d18392

Please sign in to comment.