diff options
author | Kai Stevenson <kai@kaistevenson.com> | 2025-06-28 13:36:02 -0700 |
---|---|---|
committer | Kai Stevenson <kai@kaistevenson.com> | 2025-06-28 13:36:02 -0700 |
commit | 452ac1d2a9e238684605acc8820a4dd365e70cf8 (patch) | |
tree | 744d0d4fe08edc06ad8c0693f9444021d0aa00ea /frontend | |
parent | 6475f534e7bf457113dcc82d94bfb7ac413f71c4 (diff) |
Diffstat (limited to 'frontend')
-rw-r--r-- | frontend/package.json | 1 | ||||
-rw-r--r-- | frontend/src/App.css | 36 | ||||
-rw-r--r-- | frontend/src/App.tsx | 68 | ||||
-rw-r--r-- | frontend/src/Grid.tsx | 68 | ||||
-rw-r--r-- | frontend/src/index.tsx | 6 | ||||
-rw-r--r-- | frontend/src/kaistevenson_white.svg | 35 | ||||
-rw-r--r-- | frontend/src/logo.svg | 1 | ||||
-rw-r--r-- | frontend/src/reportWebVitals.ts | 15 | ||||
-rw-r--r-- | frontend/src/serverHelper.ts | 16 |
9 files changed, 193 insertions, 53 deletions
diff --git a/frontend/package.json b/frontend/package.json index bc61e8c..7feb939 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "react-dom": "^19.1.0", "react-scripts": "5.0.1", "typescript": "^4.9.5", + "typewriter-effect": "^2.22.0", "web-vitals": "^2.1.4" }, "scripts": { 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; +}; |