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 | |
parent | 6475f534e7bf457113dcc82d94bfb7ac413f71c4 (diff) |
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | api/.env | 1 | ||||
-rw-r--r-- | api/ai.ts | 93 | ||||
-rw-r--r-- | api/index.ts | 52 | ||||
-rw-r--r-- | api/package.json (renamed from backend/package.json) | 5 | ||||
-rw-r--r-- | api/tsconfig.json | 10 | ||||
-rw-r--r-- | api/tsup.config.ts (renamed from backend/tsup.config.ts) | 2 | ||||
-rw-r--r-- | api/wordlist.ts (renamed from backend/src/wordlist.ts) | 0 | ||||
-rw-r--r-- | backend/src/index.ts | 23 | ||||
-rw-r--r-- | backend/tsconfig.json | 8 | ||||
-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 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | pnpm-lock.yaml | 234 | ||||
-rw-r--r-- | pnpm-workspace.yaml | 2 | ||||
-rw-r--r-- | vercel.json | 4 |
23 files changed, 570 insertions, 114 deletions
@@ -1,3 +1,5 @@ .DS_Store **/dist node_modules +.vercel +.env.local diff --git a/api/.env b/api/.env new file mode 100644 index 0000000..6a558ef --- /dev/null +++ b/api/.env @@ -0,0 +1 @@ +OPENAI_API="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"
\ No newline at end of file diff --git a/api/ai.ts b/api/ai.ts new file mode 100644 index 0000000..b2a3ee7 --- /dev/null +++ b/api/ai.ts @@ -0,0 +1,93 @@ +import { OpenAI } from "openai"; + +const basePrompt = ` +You are the backend logic for a game like the New York Times’ *Connections*. + +A player gives you four words. Your job is to return the **most clever, satisfying category** that all four words fit into — as if you were designing a high-quality puzzle. + +🎯 Output must be JSON: +{"categoryName": "Short Title (≤5 words)", "reason": "Brief, clear explanation why each word fits"} + +🧠 Your answer should feel: +- **Clever and insightful** (not generic) +- **Tight and specific** (all 4 words must fit cleanly) +- **Surprising but satisfying** (think lateral thinking, not just surface meaning) + +🧩 Great connections are often based on: +1. **Grammar or structure** (e.g., homonyms, stress-shifting words) +2. **Wordplay** (prefixes, rhymes, common idioms) +3. **Cultural patterns** (slang, media, jokes, Jeopardy-style trivia) +4. **Domain-specific themes** (tech terms, sports slang, myth references) + +💡 Think like a puzzle maker. Test your idea: +- Would a smart, skeptical puzzle fan say “Ohhh, nice”? +- Does **each word** clearly belong? +- If not, scrap it and try another approach. + +Avoid weak categories like: +- “Verbs” ❌ (too broad) +- “Things you can flip” ❌ (tenuous logic) +- “Nice things” ❌ (vague) + +✔ Examples of great answers: +[record, permit, insult, reject] → "Stress-Shifting Words" +[day, head, toe, man] → "___ to ___" +[brew, java, mud, rocketfuel] → "Slang for Coffee" +[duck, bank, mail, plant] → "Nouns That Are Verbs" +[transexual, muslims, media, taxes] → "Fox News Scapegoats" + +⚠️ Don’t overreach. Do not invent connections — if it’s not clean, try a new angle. + +Mandatory check before submitting: +- Word 1: does it clearly fit? +- Word 2: does it clearly fit? +- Word 3: does it clearly fit? +- Word 4: does it clearly fit? + +If even one doesn’t, the category is wrong. Start over. + +Keep it **fun**, **tight**, and **clever**. Never lazy. Never vague. + +🎯 Output must be JSON: +{"categoryName": "Short Title (≤5 words)", "reason": "Brief, clear explanation why each word fits"} +`; + +let client: OpenAI; + +const getCompletion = async ({ + messages, +}: { + messages: string[]; +}): Promise<string> => { + if (!client) { + client = new OpenAI({ apiKey: process.env.OPENAI_API! }); + } + const completion = await client.chat.completions.create({ + model: "gpt-4.1", + messages: messages.map((message) => ({ + role: "developer", + content: message, + })), + }); + return completion.choices[0].message.content!; +}; + +export const getGroupName = async ( + words: string[] +): Promise<{ + categoryName: string; + reason: string; +}> => { + let candidate: { categoryName: string; reason: string }; + const messages = [ + basePrompt, + `Now, given these four words: ${[words.join(", ")]}`, + ]; + candidate = JSON.parse(await getCompletion({ messages })); + if (!candidate.categoryName || !candidate.reason) { + throw new Error(`Got invalid response!`); + } + console.log(`Got candidate: ${JSON.stringify(candidate)}`); + + return candidate!; +}; diff --git a/api/index.ts b/api/index.ts new file mode 100644 index 0000000..59c4920 --- /dev/null +++ b/api/index.ts @@ -0,0 +1,52 @@ +import dotenv from "dotenv"; +import express, { + type Request, + type Response, + type NextFunction, +} from "express"; +import cors from "cors"; +import { WORDLIST } from "./wordlist"; +import { getGroupName as getCategoryName } from "./ai"; +import { rateLimit } from "express-rate-limit"; + +dotenv.config(); + +const PORT = 4000; + +const limiter = rateLimit({ + windowMs: 1 * 60 * 1000, //5 minute + limit: 20, // 20 requests per minute +}); + +const app = express(); +app.use(cors()); +app.use(express.json()); +app.use(limiter); + +app.set("trust proxy", "loopback"); // specify a single subnet + +app.get("/api/", (req, res) => { + res.send("HEALTHY"); +}); + +app.get("/api/random-words", (req, res) => { + let words: string[] = []; + while (words.length < 16) { + const candidateWord = WORDLIST[Math.round(Math.random() * WORDLIST.length)]; + if (candidateWord.length < 4) { + continue; + } + words.push(candidateWord); + } + res.send(words); +}); + +app.post("/api/group-words", async (req, res) => { + res.send(await getCategoryName(req.body.words)); +}); + +app.listen(PORT, () => { + console.log("Initialized"); +}); + +export default app; diff --git a/backend/package.json b/api/package.json index e92b78d..cafccef 100644 --- a/backend/package.json +++ b/api/package.json @@ -7,7 +7,10 @@ }, "dependencies": { "cors": "^2.8.5", - "express": "^5.1.0" + "dotenv": "^16.5.0", + "express": "^5.1.0", + "express-rate-limit": "^7.5.0", + "openai": "^5.3.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/api/tsconfig.json b/api/tsconfig.json new file mode 100644 index 0000000..c11147e --- /dev/null +++ b/api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "lib": ["ES2022", "DOM"], + "module": "nodenext", + "moduleResolution": "nodenext", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["**/*.ts"] +} diff --git a/backend/tsup.config.ts b/api/tsup.config.ts index 0300243..e8085ad 100644 --- a/backend/tsup.config.ts +++ b/api/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/index.ts"], + entry: ["index.ts"], splitting: false, sourcemap: true, clean: true, diff --git a/backend/src/wordlist.ts b/api/wordlist.ts index f71e8f3..f71e8f3 100644 --- a/backend/src/wordlist.ts +++ b/api/wordlist.ts diff --git a/backend/src/index.ts b/backend/src/index.ts deleted file mode 100644 index 58c5080..0000000 --- a/backend/src/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import express from "express"; -import cors from "cors"; -import { WORDLIST } from "./wordlist"; - -const PORT = 4000; -const app = express(); -app.use(cors()); - -app.get("/healthcheck", (req, res) => { - res.send("HEALTHY"); -}); - -app.get("/random-words", (req, res) => { - res.send( - new Array(16) - .fill(0) - .map(() => WORDLIST[Math.round(Math.random() * WORDLIST.length)]) - ); -}); - -app.listen(PORT, () => { - console.log("Initialized"); -}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json deleted file mode 100644 index 2e42f37..0000000 --- a/backend/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "lib": ["ES2022"], - "module": "es2022", - "allowSyntheticDefaultImports": true - }, - "include": ["src/**/*"] -} 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; +}; diff --git a/package.json b/package.json index 648fc65..7340fe7 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "build": "pnpm -F disconnected-frontend build && pnpm -F disconnected-backend build", - "clean": "rm -rf backend/node_modules && rm -rf frontend/node_modules" + "clean": "rm -rf api/node_modules && rm -rf frontend/node_modules" }, "devDependencies": { "tsup": "^8.5.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d63ed7..97c5aa3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,14 +16,23 @@ importers: specifier: ^8.5.0 version: 8.5.0(jiti@1.21.7)(postcss@8.5.4)(typescript@5.8.3)(yaml@2.8.0) - backend: + api: dependencies: cors: specifier: ^2.8.5 version: 2.8.5 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 express: specifier: ^5.1.0 version: 5.1.0 + express-rate-limit: + specifier: ^7.5.0 + version: 7.5.0(express@5.1.0) + openai: + specifier: ^5.3.0 + version: 5.3.0(ws@8.18.2) devDependencies: '@types/cors': specifier: ^2.8.19 @@ -69,10 +78,13 @@ importers: version: 19.1.0(react@19.1.0) react-scripts: specifier: 5.0.1 - version: 5.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.4))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.4))(@types/babel__core@7.20.5)(esbuild@0.25.5)(eslint@8.57.1)(react@19.1.0)(type-fest@0.21.3)(typescript@4.9.5) + version: 5.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.4))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.4))(@types/babel__core@7.20.5)(esbuild@0.25.5)(eslint@8.57.1)(react@19.1.0)(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5))(type-fest@0.21.3)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 + typewriter-effect: + specifier: ^2.22.0 + version: 2.22.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) web-vitals: specifier: ^2.1.4 version: 2.1.4 @@ -796,6 +808,10 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@csstools/normalize.css@12.1.1': resolution: {integrity: sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ==} @@ -1173,6 +1189,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} @@ -1460,6 +1479,18 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1760,6 +1791,10 @@ packages: resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} engines: {node: '>=0.4.0'} + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + acorn@7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} @@ -1851,6 +1886,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -2314,6 +2352,9 @@ packages: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2563,6 +2604,10 @@ packages: resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2628,6 +2673,10 @@ packages: resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} engines: {node: '>=10'} + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2938,6 +2987,12 @@ packages: resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + express-rate-limit@7.5.0: + resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==} + engines: {node: '>= 16'} + peerDependencies: + express: ^4.11 || 5 || ^5.0.0-beta.1 + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} @@ -3966,6 +4021,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} @@ -4215,6 +4273,18 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openai@5.3.0: + resolution: {integrity: sha512-VIKmoF7y4oJCDOwP/oHXGzM69+x0dpGFmN9QmYO+uPbLFOmmnwO+x1GbsgUtI+6oraxomGZ566Y421oYVu191w==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.8.3: resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} engines: {node: '>= 0.8.0'} @@ -5583,6 +5653,20 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.1: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -5678,6 +5762,12 @@ packages: engines: {node: '>=14.17'} hasBin: true + typewriter-effect@2.22.0: + resolution: {integrity: sha512-01HCRYY462wT8Fxps/epwGCioZd/GMXY0aLKhFKrfJ5Xhgf54/SiDx7Oq7PoES5kGqOEAdW8FS8HYVM2WSvfhQ==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} @@ -5756,6 +5846,9 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + v8-to-istanbul@8.1.1: resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==} engines: {node: '>=10.12.0'} @@ -6020,6 +6113,10 @@ packages: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -6921,6 +7018,11 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + optional: true + '@csstools/normalize.css@12.1.1': {} '@csstools/postcss-cascade-layers@1.1.1(postcss@8.5.4)': @@ -7148,7 +7250,7 @@ snapshots: jest-util: 28.1.3 slash: 3.0.0 - '@jest/core@27.5.1': + '@jest/core@27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5))': dependencies: '@jest/console': 27.5.1 '@jest/reporters': 27.5.1 @@ -7162,7 +7264,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 27.5.1 - jest-config: 27.5.1 + jest-config: 27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)) jest-haste-map: 27.5.1 jest-message-util: 27.5.1 jest-regex-util: 27.5.1 @@ -7329,6 +7431,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + optional: true + '@leichtgewicht/ip-codec@2.0.5': {} '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': @@ -7587,6 +7695,18 @@ snapshots: '@trysound/sax@0.2.0': {} + '@tsconfig/node10@1.0.11': + optional: true + + '@tsconfig/node12@1.0.11': + optional: true + + '@tsconfig/node14@1.0.3': + optional: true + + '@tsconfig/node16@1.0.4': + optional: true + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -7750,7 +7870,7 @@ snapshots: '@types/serve-index@1.9.4': dependencies: - '@types/express': 4.17.23 + '@types/express': 5.0.3 '@types/serve-static@1.15.8': dependencies: @@ -7977,6 +8097,11 @@ snapshots: acorn-walk@7.2.0: {} + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + optional: true + acorn@7.4.1: {} acorn@8.15.0: {} @@ -8052,6 +8177,9 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arg@4.1.3: + optional: true + arg@5.0.2: {} argparse@1.0.10: @@ -8618,6 +8746,9 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 + create-require@1.1.1: + optional: true + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -8853,6 +8984,9 @@ snapshots: diff-sequences@27.5.1: {} + diff@4.0.2: + optional: true + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -8922,6 +9056,8 @@ snapshots: dotenv@10.0.0: {} + dotenv@16.5.0: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9129,7 +9265,7 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.4))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.4))(eslint@8.57.1)(jest@27.5.1)(typescript@4.9.5): + eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.4))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.4))(eslint@8.57.1)(jest@27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)))(typescript@4.9.5): dependencies: '@babel/core': 7.27.4 '@babel/eslint-parser': 7.27.5(@babel/core@7.27.4)(eslint@8.57.1) @@ -9141,7 +9277,7 @@ snapshots: eslint: 8.57.1 eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.4))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.4))(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(jest@27.5.1)(typescript@4.9.5) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(jest@27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)))(typescript@4.9.5) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) @@ -9211,13 +9347,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(jest@27.5.1)(typescript@4.9.5): + eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(jest@27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)))(typescript@4.9.5): dependencies: '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.57.1)(typescript@4.9.5) eslint: 8.57.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5) - jest: 27.5.1 + jest: 27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)) transitivePeerDependencies: - supports-color - typescript @@ -9395,6 +9531,10 @@ snapshots: jest-matcher-utils: 27.5.1 jest-message-util: 27.5.1 + express-rate-limit@7.5.0(express@5.1.0): + dependencies: + express: 5.1.0 + express@4.21.2: dependencies: accepts: 1.3.8 @@ -10180,16 +10320,16 @@ snapshots: transitivePeerDependencies: - supports-color - jest-cli@27.5.1: + jest-cli@27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)): dependencies: - '@jest/core': 27.5.1 + '@jest/core': 27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)) '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 import-local: 3.2.0 - jest-config: 27.5.1 + jest-config: 27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)) jest-util: 27.5.1 jest-validate: 27.5.1 prompts: 2.4.2 @@ -10201,7 +10341,7 @@ snapshots: - ts-node - utf-8-validate - jest-config@27.5.1: + jest-config@27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)): dependencies: '@babel/core': 7.27.4 '@jest/test-sequencer': 27.5.1 @@ -10227,6 +10367,8 @@ snapshots: pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 + optionalDependencies: + ts-node: 10.9.1(@types/node@16.18.126)(typescript@4.9.5) transitivePeerDependencies: - bufferutil - canvas @@ -10502,11 +10644,11 @@ snapshots: leven: 3.1.0 pretty-format: 27.5.1 - jest-watch-typeahead@1.1.0(jest@27.5.1): + jest-watch-typeahead@1.1.0(jest@27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5))): dependencies: ansi-escapes: 4.3.2 chalk: 4.1.2 - jest: 27.5.1 + jest: 27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)) jest-regex-util: 28.0.2 jest-watcher: 28.1.3 slash: 4.0.0 @@ -10552,11 +10694,11 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@27.5.1: + jest@27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)): dependencies: - '@jest/core': 27.5.1 + '@jest/core': 27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)) import-local: 3.2.0 - jest-cli: 27.5.1 + jest-cli: 27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)) transitivePeerDependencies: - bufferutil - canvas @@ -10764,6 +10906,9 @@ snapshots: dependencies: semver: 7.7.2 + make-error@1.3.6: + optional: true + makeerror@1.0.12: dependencies: tmpl: 1.0.5 @@ -10986,6 +11131,10 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openai@5.3.0(ws@8.18.2): + optionalDependencies: + ws: 8.18.2 + optionator@0.8.3: dependencies: deep-is: 0.1.4 @@ -11263,12 +11412,13 @@ snapshots: postcss: 8.5.4 postcss-value-parser: 4.2.0 - postcss-load-config@4.0.2(postcss@8.5.4): + postcss-load-config@4.0.2(postcss@8.5.4)(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)): dependencies: lilconfig: 3.1.3 yaml: 2.8.0 optionalDependencies: postcss: 8.5.4 + ts-node: 10.9.1(@types/node@16.18.126)(typescript@4.9.5) postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.4)(yaml@2.8.0): dependencies: @@ -11704,7 +11854,7 @@ snapshots: react-refresh@0.11.0: {} - react-scripts@5.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.4))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.4))(@types/babel__core@7.20.5)(esbuild@0.25.5)(eslint@8.57.1)(react@19.1.0)(type-fest@0.21.3)(typescript@4.9.5): + react-scripts@5.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.4))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.4))(@types/babel__core@7.20.5)(esbuild@0.25.5)(eslint@8.57.1)(react@19.1.0)(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5))(type-fest@0.21.3)(typescript@4.9.5): dependencies: '@babel/core': 7.27.4 '@pmmmwh/react-refresh-webpack-plugin': 0.5.16(react-refresh@0.11.0)(type-fest@0.21.3)(webpack-dev-server@4.15.2(webpack@5.99.9(esbuild@0.25.5)))(webpack@5.99.9(esbuild@0.25.5)) @@ -11722,15 +11872,15 @@ snapshots: dotenv: 10.0.0 dotenv-expand: 5.1.0 eslint: 8.57.1 - eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.4))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.4))(eslint@8.57.1)(jest@27.5.1)(typescript@4.9.5) + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.27.4))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.27.4))(eslint@8.57.1)(jest@27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)))(typescript@4.9.5) eslint-webpack-plugin: 3.2.0(eslint@8.57.1)(webpack@5.99.9(esbuild@0.25.5)) file-loader: 6.2.0(webpack@5.99.9(esbuild@0.25.5)) fs-extra: 10.1.0 html-webpack-plugin: 5.6.3(webpack@5.99.9(esbuild@0.25.5)) identity-obj-proxy: 3.0.0 - jest: 27.5.1 + jest: 27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)) jest-resolve: 27.5.1 - jest-watch-typeahead: 1.1.0(jest@27.5.1) + jest-watch-typeahead: 1.1.0(jest@27.5.1(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5))) mini-css-extract-plugin: 2.9.2(webpack@5.99.9(esbuild@0.25.5)) postcss: 8.5.4 postcss-flexbugs-fixes: 5.0.2(postcss@8.5.4) @@ -11748,7 +11898,7 @@ snapshots: semver: 7.7.2 source-map-loader: 3.0.2(webpack@5.99.9(esbuild@0.25.5)) style-loader: 3.3.4(webpack@5.99.9(esbuild@0.25.5)) - tailwindcss: 3.4.17 + tailwindcss: 3.4.17(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)) terser-webpack-plugin: 5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)) webpack: 5.99.9(esbuild@0.25.5) webpack-dev-server: 4.15.2(webpack@5.99.9(esbuild@0.25.5)) @@ -12455,7 +12605,7 @@ snapshots: symbol-tree@3.2.4: {} - tailwindcss@3.4.17: + tailwindcss@3.4.17(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -12474,7 +12624,7 @@ snapshots: postcss: 8.5.4 postcss-import: 15.1.0(postcss@8.5.4) postcss-js: 4.0.1(postcss@8.5.4) - postcss-load-config: 4.0.2(postcss@8.5.4) + postcss-load-config: 4.0.2(postcss@8.5.4)(ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5)) postcss-nested: 6.2.0(postcss@8.5.4) postcss-selector-parser: 6.1.2 resolve: 1.22.10 @@ -12574,6 +12724,25 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-node@10.9.1(@types/node@16.18.126)(typescript@4.9.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 16.18.126 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -12686,6 +12855,13 @@ snapshots: typescript@5.8.3: {} + typewriter-effect@2.22.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + prop-types: 15.8.1 + raf: 3.4.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + ufo@1.6.1: {} unbox-primitive@1.1.0: @@ -12752,6 +12928,9 @@ snapshots: uuid@8.3.2: {} + v8-compile-cache-lib@3.0.1: + optional: true + v8-to-istanbul@8.1.1: dependencies: '@types/istanbul-lib-coverage': 2.0.6 @@ -13140,4 +13319,7 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 + yn@3.1.1: + optional: true + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a5afcf7..a35699a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,5 @@ packages: - - backend + - api - frontend onlyBuiltDependencies: diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..6b26f75 --- /dev/null +++ b/vercel.json @@ -0,0 +1,4 @@ +{ + "version": 2, + "routes": [{ "src": "/api/(.*)", "dest": "/api/index.ts" }] +} |