summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--api/.env1
-rw-r--r--api/ai.ts93
-rw-r--r--api/index.ts52
-rw-r--r--api/package.json (renamed from backend/package.json)5
-rw-r--r--api/tsconfig.json10
-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.ts23
-rw-r--r--backend/tsconfig.json8
-rw-r--r--frontend/package.json1
-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
-rw-r--r--package.json2
-rw-r--r--pnpm-lock.yaml234
-rw-r--r--pnpm-workspace.yaml2
-rw-r--r--vercel.json4
23 files changed, 570 insertions, 114 deletions
diff --git a/.gitignore b/.gitignore
index c907762..42e5ded 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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" }]
+}