From 452ac1d2a9e238684605acc8820a4dd365e70cf8 Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Sat, 28 Jun 2025 13:36:02 -0700 Subject: MVP --- frontend/package.json | 1 + frontend/src/App.css | 36 +++++++++++++++++--- frontend/src/App.tsx | 68 +++++++++++++++++++++++++++---------- frontend/src/Grid.tsx | 68 ++++++++++++++++++++++++++++++++----- frontend/src/index.tsx | 6 ---- frontend/src/kaistevenson_white.svg | 35 +++++++++++++++++++ frontend/src/logo.svg | 1 - frontend/src/reportWebVitals.ts | 15 -------- frontend/src/serverHelper.ts | 16 +++++++-- 9 files changed, 193 insertions(+), 53 deletions(-) create mode 100644 frontend/src/kaistevenson_white.svg delete mode 100644 frontend/src/logo.svg delete mode 100644 frontend/src/reportWebVitals.ts (limited to 'frontend') 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 => ({ @@ -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 ? ( - - ) : ( -

Loading...

+ return ( +
+ +
); }; 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 }) => ( -
-
-        

{title.toUpperCase()}

-

{words.join(", ")}

-
-
- )); + const groups = gameState.groups.map( + ({ words, title, reason, flipped }, idx) => ( + + ) + ); const tiles = gameState.words.map((word, i) => !word.used ? ( ); - -// 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ - \ 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; +}; -- cgit v1.2.3-70-g09d2