summaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2025-06-28 13:36:02 -0700
committerKai Stevenson <kai@kaistevenson.com>2025-06-28 13:36:02 -0700
commit452ac1d2a9e238684605acc8820a4dd365e70cf8 (patch)
tree744d0d4fe08edc06ad8c0693f9444021d0aa00ea /frontend/src
parent6475f534e7bf457113dcc82d94bfb7ac413f71c4 (diff)
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/App.css36
-rw-r--r--frontend/src/App.tsx68
-rw-r--r--frontend/src/Grid.tsx68
-rw-r--r--frontend/src/index.tsx6
-rw-r--r--frontend/src/kaistevenson_white.svg35
-rw-r--r--frontend/src/logo.svg1
-rw-r--r--frontend/src/reportWebVitals.ts15
-rw-r--r--frontend/src/serverHelper.ts16
8 files changed, 192 insertions, 53 deletions
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 6566fca..4046cce 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -43,10 +43,31 @@
justify-content: center;
}
-.App-main {
+.Game-main {
min-height: 100vh;
display: flex;
- flex-direction: column;
+ flex-direction: row;
+ gap: 5vmin;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+}
+
+.Game-controls {
+ height: 80vh;
+ width: 10vw;
+ background-color: var(--bg-secondary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+}
+
+.Game-reasons {
+ height: 80vh;
+ width: 10vw;
+ background-color: var(--bg-secondary);
+ display: flex;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
@@ -74,12 +95,12 @@
text-overflow: clip;
}
.Grid-square .Fit-text {
- font-size: calc(10px + 1vmin);
+ font-size: calc(10px + 0.8vmin);
overflow: hidden;
text-overflow: clip;
}
.Completed-group .Fit-text {
- font-size: calc(10px + 2vmin);
+ font-size: calc(10px + 1.5vmin);
font-style: bold;
overflow: hidden;
text-overflow: clip;
@@ -110,6 +131,13 @@
display: flex;
align-items: center;
justify-content: center;
+ cursor: pointer;
+}
+
+.Group-reason {
+ white-space: normal;
+ word-wrap: break-word; /* Or word-break: break-word; */
+ overflow-wrap: break-word;
}
.Selected {
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 4eb26bd..b3203bc 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,13 +1,27 @@
import React, { useEffect, useState } from "react";
-import logo from "./kaistevenson.svg";
+import logo from "./kaistevenson_white.svg";
import "./App.css";
import { Grid } from "./Grid";
-import { getWords } from "./serverHelper";
+import { getCategory, getWords } from "./serverHelper";
export type GameState = {
words: { word: string; selected: boolean; used: boolean }[];
- groups: { title: string; words: string[] }[];
+ groups: {
+ title: string;
+ reason: string;
+ words: string[];
+ flipped: boolean;
+ }[];
+};
+
+const loadingGameState: GameState = {
+ words: new Array(16).fill({
+ word: "Loading...",
+ selected: false,
+ used: false,
+ }),
+ groups: [],
};
const initializeGameState = async (): Promise<GameState> => ({
@@ -46,6 +60,12 @@ const Game = () => {
candidateState.words[idx].selected = !candidateState.words[idx].selected;
setGameState(candidateState);
};
+ const flipGroupHandler = (idx: number) => {
+ const candidateState = { ...gameState! };
+ //FIXME don't mutate state
+ candidateState.groups[idx].flipped = !candidateState.groups[idx].flipped;
+ setGameState(candidateState);
+ };
const submitSelectionHandler = () => {
const candidateState = { ...gameState! };
@@ -65,26 +85,40 @@ const Game = () => {
})
);
- //mock the server response for this selection
- const response = "Four letter verbs";
- const newGroup: GameState["groups"][number] = {
- title: response,
+ const loadingGroup: GameState["groups"][number] = {
+ title: "LOADING",
words: selectedWords,
+ reason: "LOADING",
+ flipped: false,
};
- candidateState.groups = [...candidateState.groups, newGroup];
- setGameState(candidateState);
+ setGameState((prevState) => ({
+ ...candidateState!,
+ groups: [...prevState!.groups, loadingGroup],
+ }));
+
+ getCategory(selectedWords).then(({ categoryName, reason }) => {
+ setGameState((prevState) => {
+ const updatedGroups = prevState!.groups.map((group) =>
+ group === loadingGroup
+ ? { ...group, title: categoryName, reason }
+ : group
+ );
+ return { ...candidateState!, groups: updatedGroups };
+ });
+ });
};
//display logic
- return gameState ? (
- <Grid
- selectWordHandler={selectWordHandler}
- submitSelectionHandler={submitSelectionHandler}
- gameState={gameState!}
- />
- ) : (
- <h1>Loading...</h1>
+ return (
+ <div className="Game-main">
+ <Grid
+ selectWordHandler={selectWordHandler}
+ flipGroupHandler={flipGroupHandler}
+ submitSelectionHandler={submitSelectionHandler}
+ gameState={gameState ?? loadingGameState}
+ />
+ </div>
);
};
diff --git a/frontend/src/Grid.tsx b/frontend/src/Grid.tsx
index f6df3a8..8d822f3 100644
--- a/frontend/src/Grid.tsx
+++ b/frontend/src/Grid.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { GameState } from "./App";
+import Typewriter from "typewriter-effect";
const Tile = ({
word,
@@ -23,20 +24,71 @@ const Tile = ({
export const Grid = ({
selectWordHandler,
submitSelectionHandler,
+ flipGroupHandler,
gameState,
}: {
selectWordHandler: (idx: number) => void;
+ flipGroupHandler: (idx: number) => void;
submitSelectionHandler: () => void;
gameState: GameState;
}) => {
- const groups = gameState.groups.map(({ title, words }) => (
- <div className="Completed-group">
- <pre>
- <h1 className="Fit-text">{title.toUpperCase()}</h1>
- <h2 className="Fit-text">{words.join(", ")}</h2>
- </pre>
- </div>
- ));
+ const groups = gameState.groups.map(
+ ({ words, title, reason, flipped }, idx) => (
+ <button onClick={() => flipGroupHandler(idx)} className="Completed-group">
+ {
+ <pre>
+ {flipped ? (
+ <Typewriter
+ key={"reason-tw"}
+ options={{
+ delay: 1,
+ wrapperClassName: "Group-reason",
+ cursor: "",
+ }}
+ onInit={(tw) => tw.typeString(reason).start()}
+ component={"p"}
+ ></Typewriter>
+ ) : (
+ <div>
+ {title !== "LOADING" ? (
+ <Typewriter
+ key={"header-tw"}
+ options={{
+ delay: 30,
+ wrapperClassName: "Group-header",
+ cursor: "",
+ }}
+ onInit={(tw) => tw.typeString(title).start()}
+ component={"h1"}
+ ></Typewriter>
+ ) : (
+ <Typewriter
+ key={"loading-tw"}
+ options={{
+ delay: 150,
+ wrapperClassName: "Group-header",
+ cursor: "",
+ }}
+ onInit={(tw) => tw.typeString("Loading...").start()}
+ component={"h1"}
+ ></Typewriter>
+ )}
+ <Typewriter
+ options={{
+ delay: 15,
+ wrapperClassName: "Group-content",
+ cursor: "",
+ }}
+ onInit={(tw) => tw.typeString(words.join(", ")).start()}
+ component={"h2"}
+ ></Typewriter>
+ </div>
+ )}
+ </pre>
+ }
+ </button>
+ )
+ );
const tiles = gameState.words.map((word, i) =>
!word.used ? (
<Tile
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 079f7c6..e728e31 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -2,7 +2,6 @@ import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { App } from "./App";
-import reportWebVitals from "./reportWebVitals";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
@@ -12,8 +11,3 @@ root.render(
<App />
</React.StrictMode>
);
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();
diff --git a/frontend/src/kaistevenson_white.svg b/frontend/src/kaistevenson_white.svg
new file mode 100644
index 0000000..8f80726
--- /dev/null
+++ b/frontend/src/kaistevenson_white.svg
@@ -0,0 +1,35 @@
+<svg width="100%" height="100%" viewBox="0 0 2000 2000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;">
+ <g id="Layer-2">
+ <g transform="matrix(-0.707107,0.707107,0.707107,0.707107,355.962,1644.04)">
+ <rect x="-1554.85" y="-644.038" width="1288.08" height="1288.08" style="fill:none;stroke:white;stroke-width:39px;"/>
+ </g>
+ <g transform="matrix(-0.573577,0.819152,0.819152,0.573577,692.108,1660.28)">
+ <rect x="-1232.62" y="-641.66" width="1030.3" height="1030.3" style="fill:none;stroke:white;stroke-width:39px;"/>
+ </g>
+ <g transform="matrix(-0.422618,0.906308,0.906308,0.422618,949.175,1580.94)">
+ <rect x="-960.341" y="-611.806" width="824.707" height="824.708" style="fill:none;stroke:white;stroke-width:39px;"/>
+ </g>
+ <g transform="matrix(-0.258818,0.965926,0.965926,0.258818,1120.74,1450.62)">
+ <rect x="-733.886" y="-563.131" width="659.748" height="659.748" style="fill:none;stroke:white;stroke-width:39px;"/>
+ </g>
+ <g transform="matrix(-0.0871564,0.996195,0.996195,0.0871564,1214.03,1305.67)">
+ <rect x="-549.711" y="-503.717" width="527.718" height="527.718" style="fill:none;stroke:white;stroke-width:39px;"/>
+ </g>
+ <g transform="matrix(0.0871565,0.996195,0.996195,-0.0871565,1244.67,1171.32)">
+ <rect x="-403.201" y="-440.018" width="422.412" height="422.413" style="fill:none;stroke:white;stroke-width:39px;"/>
+ </g>
+ <g transform="matrix(0.25882,0.965926,0.965926,-0.25882,1229.97,1061.62)">
+ <rect x="-287.396" y="-374.542" width="336.705" height="336.705" style="fill:none;stroke:white;stroke-width:39px;"/>
+ </g>
+ <g transform="matrix(0.42262,0.906307,0.906307,-0.42262,1189.72,983.401)">
+ <rect x="-199.795" y="-313.617" width="269.322" height="269.324" style="fill:none;stroke:white;stroke-width:39px;"/>
+ </g>
+ <g transform="matrix(0.573574,0.819154,0.819154,-0.573574,1138.16,935.578)">
+ <rect x="-134.261" y="-257.911" width="215.58" height="215.579" style="fill:none;stroke:white;stroke-width:39px;"/>
+ </g>
+ <g transform="matrix(0.707107,0.707107,0.707107,-0.707107,1086.23,913.769)">
+ <rect x="-86.231" y="-208.18" width="172.462" height="172.462" style="fill:none;stroke:white;stroke-width:39px;"/>
+ </g>
+ </g>
+</svg>
+
diff --git a/frontend/src/logo.svg b/frontend/src/logo.svg
deleted file mode 100644
index 9dfc1c0..0000000
--- a/frontend/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg> \ No newline at end of file
diff --git a/frontend/src/reportWebVitals.ts b/frontend/src/reportWebVitals.ts
deleted file mode 100644
index 49a2a16..0000000
--- a/frontend/src/reportWebVitals.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ReportHandler } from 'web-vitals';
-
-const reportWebVitals = (onPerfEntry?: ReportHandler) => {
- if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
- }
-};
-
-export default reportWebVitals;
diff --git a/frontend/src/serverHelper.ts b/frontend/src/serverHelper.ts
index 8565fd6..3181093 100644
--- a/frontend/src/serverHelper.ts
+++ b/frontend/src/serverHelper.ts
@@ -1,12 +1,24 @@
import axios from "axios";
-const SERVER_URL = "http://localhost:4000";
+const SERVER_URL = "";
export const getWords = async (): Promise<[...(string[] & { length: 9 })]> => {
- const words = (await axios.get(`${SERVER_URL}/random-words`))
+ const words = (await axios.get(`${SERVER_URL}/api/random-words`))
.data as string[];
if (words.length !== 16) {
throw new Error(`Got invalid words ${words} from server`);
}
return words;
};
+
+export const getCategory = async (
+ words: string[]
+): Promise<{ categoryName: string; reason: string }> => {
+ const response = (
+ await axios.post(`${SERVER_URL}/api/group-words`, {
+ words,
+ })
+ ).data;
+
+ return response;
+};