From 87f134c9f2714593890a530221df429122c4398b Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Tue, 10 Jun 2025 23:50:09 -0700 Subject: init --- frontend/src/App.css | 100 ++++++++++++++++++++++++++++++++++++++++ frontend/src/App.tsx | 64 +++++++++++++++++++++++++ frontend/src/Grid.tsx | 36 +++++++++++++++ frontend/src/index.css | 13 ++++++ frontend/src/index.tsx | 19 ++++++++ frontend/src/kaistevenson.svg | 1 + frontend/src/logo.svg | 1 + frontend/src/react-app-env.d.ts | 1 + frontend/src/reportWebVitals.ts | 15 ++++++ frontend/src/serverHelper.ts | 12 +++++ frontend/src/setupTests.ts | 5 ++ 11 files changed, 267 insertions(+) create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/Grid.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/index.tsx create mode 100644 frontend/src/kaistevenson.svg create mode 100644 frontend/src/logo.svg create mode 100644 frontend/src/react-app-env.d.ts create mode 100644 frontend/src/reportWebVitals.ts create mode 100644 frontend/src/serverHelper.ts create mode 100644 frontend/src/setupTests.ts (limited to 'frontend/src') diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..4f77d97 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,100 @@ +.App { + text-align: center; +} + +.Logo-container { + width: 100%; + height: 100%; + pointer-events: none; +} + +.App-logo { + height: 15vmin; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: Logo-rotate-in 2s cubic-bezier(0.755, 0.05, 0.855, 0.06) forwards; + } + .App-main { + opacity: 0; + animation: App-fade-in 0.65s ease-in 1.8s forwards; + } +} + +.App-bg { + background-color: #282c34; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.App-main { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.Grid { + background: #5262816f; + border-radius: 20px; + width: 80vmin; + height: 80vmin; + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(4, 1fr); + gap: 1%; +} + +.Fit-text { + font-size: calc(10px + 1vmin); + overflow: hidden; + text-overflow: clip; +} + +.Grid-square { + background: #acc1eb9d; + border: none; + border-radius: 10px; + width: 100%; + height: 100%; + font-size: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.Selected { + background: #d8deea9d; +} + +@keyframes Logo-rotate-in { + from { + transform: translate(-50%, -50%) rotate(0deg) scale(1); + opacity: 1; + } + to { + transform: translate(-50%, -50%) rotate(360deg) scale(5); + opacity: 0; + } +} + +@keyframes App-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..f6f5296 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useState } from "react"; +import logo from "./kaistevenson.svg"; +import "./App.css"; + +import { Grid } from "./Grid"; +import { getWords } from "./serverHelper"; + +export type GameState = { + words: { word: string; selected: boolean }[]; +}; + +const initializeGameState = async (): Promise => ({ + words: (await getWords()).map((word) => ({ word, selected: false })), +}); + +const Game = () => { + //state + const [gameState, setGameState] = useState(undefined); + + //initialize + useEffect(() => { + if (!gameState) { + initializeGameState() + .then((state) => setGameState(state)) + .catch(console.error); + } + }); + + //handlers + const selectWordHandler = (idx: number) => { + const candidateState = { ...gameState! }; + if (candidateState.words.filter((word) => word.selected).length === 4) { + if (!candidateState.words[idx].selected) { + //if we've already selected 4 words and we would select another, + //do nothing + return; + } + } + //FIXME don't mutate state + candidateState.words[idx].selected = !candidateState.words[idx].selected; + setGameState(candidateState); + console.log(`selected ${idx}`); + }; + + //display logic + return gameState ? ( + + ) : ( +

Loading...

+ ); +}; + +export const App = () => ( +
+
+
+ logo +
+
+ +
+
+
+); diff --git a/frontend/src/Grid.tsx b/frontend/src/Grid.tsx new file mode 100644 index 0000000..b35f757 --- /dev/null +++ b/frontend/src/Grid.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { GameState } from "./App"; + +const Tile = ({ + word, + selectWordHandler, +}: { + selectWordHandler: () => void; + word: GameState["words"][number]; +}) => ( + +); + +export const Grid = ({ + selectWordHandler, + words, +}: { + selectWordHandler: (idx: number) => void; + words: GameState["words"]; +}) => ( +
+ {new Array(16).fill(undefined).map((_, i) => ( + selectWordHandler(i)} + word={words[i]} + key={i} + /> + ))} +
+); diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..079f7c6 --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,19 @@ +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 +); +root.render( + + + +); + +// 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.svg b/frontend/src/kaistevenson.svg new file mode 100644 index 0000000..d1b244b --- /dev/null +++ b/frontend/src/kaistevenson.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/logo.svg b/frontend/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/frontend/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/frontend/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/src/reportWebVitals.ts b/frontend/src/reportWebVitals.ts new file mode 100644 index 0000000..49a2a16 --- /dev/null +++ b/frontend/src/reportWebVitals.ts @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000..8565fd6 --- /dev/null +++ b/frontend/src/serverHelper.ts @@ -0,0 +1,12 @@ +import axios from "axios"; + +const SERVER_URL = "http://localhost:4000"; + +export const getWords = async (): Promise<[...(string[] & { length: 9 })]> => { + const words = (await axios.get(`${SERVER_URL}/random-words`)) + .data as string[]; + if (words.length !== 16) { + throw new Error(`Got invalid words ${words} from server`); + } + return words; +}; diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/frontend/src/setupTests.ts @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; -- cgit v1.2.3-70-g09d2