diff --git a/README.md b/README.md
index b415aa2..c629ed1 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,9 @@ This project is designed for teaching and practicing the fundamentals of:
- State flow between parent/child components
## π οΈ Getting Started
+
1. Clone the repository
+
```sh
git clone https://github.com/your-username/tic-tac-toe-react.git
@@ -17,19 +19,25 @@ cd tic-tac-toe-react
```
2. Install dependencies
+
```sh
npm install
```
+
3. Start the development server
+
```sh
npm run dev
```
+
Your app should be running at:
+
```sh
http://localhost:5173
```
## π― Learning Goals
+
- useState for managing UI state
- Data flow between parent and child components
- Component reusability (Board/Square)
@@ -40,42 +48,54 @@ http://localhost:5173
- Managing more complex state
## π Tasks to Implement (Step-by-Step)
+
### Task 1 β Make the Squares Clickable
**Goal:** When clicking a square, place "X" or "O" depending on whose turn it is.
**Hints:**
+
- Use the existing isXNext state
- Update the squares array with the new value
- Toggle the turn after each move
### Task 2 β Prevent Overwriting Moves
+
**Goal:** When a player gets 3 in a row, display a winner message.
**Hints:**
+
- Check `squares[index] !== null` before updating
### Task 3 β Add Winner Detection
+
**Goal:** When a player gets 3 in a row, display a winner message.
**Hints:**
+
- Create a `calculateWinner()` helper
- Use all 8 winning combinations
- Add a winner: `string | null` state in Game
### Task 4 β Stop Moves After Win
+
**Goal:** Disable the board after someone wins.
**Hints:**
+
- If there's a winner β ignore clicks
- Or disable the buttons with disabled attribute
### Task 5 β Add a βPlay Againβ Button
+
**Goal:** Disable the board after someone wins.
**Hints:**
+
- Clear squares back to `Array(9).fill(null)`
- Reset `isXNext` β true
### Task 6 β Add a Turn Countdown Timer (useEffect)
+
**Goal:** Each player gets e.g. 10 seconds to play. If time runs out β automatically switch turn.
**Hints:**
+
- Create `timeLeft` state
- Use `useEffect` with `setInterval`
- Reset timer on every turn change
-- Cleanup the interval on unmount or turn switch
\ No newline at end of file
+- Cleanup the interval on unmount or turn switch
diff --git a/package-lock.json b/package-lock.json
index 2fec462..f812950 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,8 @@
"version": "0.0.0",
"dependencies": {
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-toastify": "^11.0.5"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -57,6 +58,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1389,6 +1391,7 @@
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1399,6 +1402,7 @@
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1458,6 +1462,7 @@
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
@@ -1709,6 +1714,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1814,6 +1820,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -1876,6 +1883,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2035,6 +2051,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -2721,6 +2738,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -2782,6 +2800,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2791,6 +2810,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -2808,6 +2828,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-toastify": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
+ "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
+ "license": "MIT",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19",
+ "react-dom": "^18 || ^19"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2984,6 +3017,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -3070,6 +3104,7 @@
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -3191,6 +3226,7 @@
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"dev": true,
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index 51210e2..37aa4a0 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
},
"dependencies": {
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-toastify": "^11.0.5"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
diff --git a/src/App.css b/src/App.css
index b9d355d..9954c3d 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,5 +1,5 @@
#root {
- max-width: 1280px;
+ max-width: var(--app-max-width);
margin: 0 auto;
padding: 2rem;
text-align: center;
diff --git a/src/App.tsx b/src/App.tsx
index 171e1ef..851a75f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,8 +1,14 @@
import "./App.css";
import Game from "./components/Game";
-
+import { ToastContainer, toast } from "react-toastify";
+import "react-toastify/dist/ReactToastify.css";
function App() {
- return
Next Player: {isXNext ? "X" : "O"}
+ return ( +
+ {STRINGS.NEXT_PLAYER_LABEL}
+
Turn Timer: {turnTimer}
+ +