diff --git a/Notes.md b/Notes.md new file mode 100644 index 0000000..2db81b0 --- /dev/null +++ b/Notes.md @@ -0,0 +1,35 @@ +## What was good? + +### HTML + +- Clean structure +- Scripts and CSS link tags were imported correctly + +### CSS + +- Good use of descendant selectors +- Good class naming +- CSS reset, font imports + +### JS + +- State was consolidated +- Elements were pre-selected +- Good use of SessionStorage + +## What could improve? + +### HTML + +- Boilerplate +- Location of script tag + +### CSS + +- Reusability +- General styling + +### JS + +- Overall design pattern +- Global variables diff --git a/README.md b/README.md index 98d11e7..cb88b47 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,10 @@ There's a YouTube video I made about this repository. [You should watch it.]() ## Quickstart -There are four examples in this repository that show how the `/original` project could be refactored using several different libraries and patterns. I suggest reading through them in the following order. +There are two examples in this repository that show how the `/original` project could be refactored using different libraries and patterns. I suggest reading through them in the following order. -1. Vanilla Refactor - this is the _closest_ representation of the original project and I highly recommend starting here since the remaining examples build off of the patterns here. -2. Vanilla TypeScript Refactor - this is the Vanilla Refactor reproduced using TypeScript with an additional compile step (required to compile the TypeScript to JavaScript that can run in the browser) -3. Alpine.js Refactor - this shows how we can use a lightweight framework like Alpine.js to reduce the boilerplate needed -4. React Refactor - this shows a React implementation of the project, which is a much more _declarative_ approach than the vanilla implementations, which are mostly _imperative_. See my post on [declarative vs. imperative programming](https://www.zachgollwitzer.com/posts/imperative-programming). +1. Vanilla Refactor - this is the _closest_ representation of the original project and I highly recommend starting here since the remaining examples build off of the patterns here. You can also view the `typescript` Git branch to see how this would look written in TS rather than JS. +2. React Refactor - this shows a React implementation of the project, which is a much more _declarative_ approach than the vanilla implementations, which are mostly _imperative_. See my post on [declarative vs. imperative programming](https://www.zachgollwitzer.com/posts/imperative-programming). This also has a TypeScript implementation on the `typescript` branch. ## Attribution diff --git a/Vanilla Refactor.md b/Vanilla Refactor.md new file mode 100644 index 0000000..1db8b96 --- /dev/null +++ b/Vanilla Refactor.md @@ -0,0 +1,17 @@ +## Vanilla HTML / CSS / JS Tic Tac Toe Game + +### Concepts covered + +- MVC architectural pattern +- What is "state"? +- Event delegation +- Mobile responsiveness +- CSS animations +- CSS grid +- Much more!! + +### Prerequisites + +- Beginner to intermediate level video +- Must know HTML, CSS, JS (basics) +- (recommended) have watched prior video introducing the original project diff --git a/Video.md b/Video.md new file mode 100644 index 0000000..7745504 --- /dev/null +++ b/Video.md @@ -0,0 +1,16 @@ +## Refactoring YOUR code + +### Why? + +Because it's fun and hopefully educational! + +### Associated videos + +1. **THIS video** - a walkthrough of the original solution and areas for improvement +2. **Vanilla JS refactor** - I'll refactor the project with Vanilla JS (BONUS: will also do it in TypeScript) +3. **React refactor** - I'll refactor the project with React (BONUS: will also do it in TypeScript) + +### Prerequisites + +- Beginner level video +- Must know HTML, CSS, JS (basics) diff --git a/live-vanilla-build/css/index.css b/live-vanilla-build/css/index.css new file mode 100644 index 0000000..9b1f6d0 --- /dev/null +++ b/live-vanilla-build/css/index.css @@ -0,0 +1,252 @@ +@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600&display=swap"); + +:root { + --dark-gray: #1a2a32; + --gray: #2e4756; + --turquoise: #3cc4bf; + --yellow: #f2b147; + --light-gray: #d3d3d3; +} + +* { + padding: 0; + margin: 0; + box-sizing: border-box; + list-style: none; + font-family: "Montserrat", sans-serif; + border: none; +} + +html, +body { + height: 100%; + background-color: var(--dark-gray); +} + +body { + padding: 90px 20px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +/* Shared utility classes */ + +button:hover { + cursor: pointer; + opacity: 90%; +} + +.hidden { + display: none !important; +} + +.yellow { + color: var(--yellow); +} + +.turquoise { + color: var(--turquoise); +} + +.shadow { + box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px, + rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset; +} + +.border { + border: 1px solid rgba(211, 211, 211, 0.4) !important; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, 80px); + grid-template-rows: 50px repeat(3, 80px) 60px; + gap: 5px; +} + +@media (min-width: 768px) { + .grid { + width: 490px; + grid-template-columns: repeat(3, 150px); + grid-template-rows: 50px repeat(3, 150px) 60px; + gap: 20px; + } +} + +.turn { + color: var(--yellow); + + grid-column-start: 1; + grid-column-end: 3; + align-self: center; + + display: flex; + align-items: center; + gap: 20px; +} + +@keyframes turn-text-animation { + 0% { + opacity: 0; + transform: translateX(-20px); + } + 100% { + opacity: 100%; + transform: translateX(0); + } +} + +.turn p { + font-size: 14px; + animation: 0.6s ease-in-out turn-text-animation; +} + +@keyframes turn-icon-animation { + 0% { + transform: scale(1); + } + 25% { + transform: scale(1.4); + } + 100% { + transform: scale(1); + } +} + +.turn i { + font-size: 1.8rem; + margin-left: 10px; + animation: 0.6s ease-in-out turn-icon-animation; +} + +/* Menu styles */ +.menu { + position: relative; +} + +.menu-btn { + width: 100%; + height: 100%; + display: flex; + justify-content: space-around; + align-items: center; + border-radius: 10px; + color: white; + background-color: rgba(211, 211, 211, 0.05); + border: 1px solid transparent; +} + +.menu-btn:focus, +.menu-btn:hover { + background-color: rgba(211, 211, 211, 0.07); +} + +.items { + position: absolute; + z-index: 10; + top: 60px; + right: 0; + background-color: #203139; + border-radius: 2px; + padding: 10px; +} + +.items button { + background-color: transparent; + padding: 8px; + color: white; +} + +.items button:hover { + text-decoration: underline; + cursor: pointer; +} + +.square { + background-color: var(--gray); + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + font-size: 3rem; +} + +.square:hover { + cursor: pointer; + opacity: 90%; +} + +.score { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 10px; +} + +.score p { + font-size: 14px; + font-weight: 600; +} + +.score span { + font-size: 12px; + margin-top: 2px; +} + +.actions { + background-color: purple; +} + +/* Footer styles */ + +footer { + color: white; + margin-top: 50px; +} + +footer p { + margin-top: 10px; + text-align: center; +} + +footer a { + color: var(--yellow); +} + +/* Modal styles - opens when game finishes */ + +.modal { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); +} + +.modal-contents { + /* transform: translateY(-80px); */ + height: 150px; + width: 100%; + max-width: 300px; + background-color: #2a4544; + border-radius: 20px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 20px; + color: white; + margin: 10px; +} + +.modal-contents button { + padding: 10px; + background-color: var(--turquoise); + color: #2a4544; + border-radius: 3px; +} diff --git a/live-vanilla-build/index.html b/live-vanilla-build/index.html new file mode 100644 index 0000000..c5768df --- /dev/null +++ b/live-vanilla-build/index.html @@ -0,0 +1,88 @@ + + + + + + + + + Vanilla JS T3 + + +
+
+ +
+ +

Player 1, you're up!

+
+ + + + + +
+
+
+
+
+
+
+
+
+ + +
+

Player 1

+ 0 Wins +
+
+

Ties

+ 0 +
+
+

Player 2

+ 0 Wins +
+
+
+ + + + + + + + + diff --git a/live-vanilla-build/js/app.js b/live-vanilla-build/js/app.js new file mode 100644 index 0000000..7b31b6d --- /dev/null +++ b/live-vanilla-build/js/app.js @@ -0,0 +1,148 @@ +const App = { + // All of our selected HTML elements + $: { + menu: document.querySelector('[data-id="menu"]'), + menuItems: document.querySelector('[data-id="menu-items"]'), + resetBtn: document.querySelector('[data-id="reset-btn"]'), + newRoundBtn: document.querySelector('[data-id="new-round-btn"]'), + squares: document.querySelectorAll('[data-id="square"]'), + modal: document.querySelector('[data-id="modal"]'), + modalText: document.querySelector('[data-id="modal-text"]'), + modalBtn: document.querySelector('[data-id="modal-btn"]'), + turn: document.querySelector('[data-id="turn"]'), + }, + + state: { + moves: [], + }, + + getGameStatus(moves) { + const p1Moves = moves + .filter((move) => move.playerId === 1) + .map((move) => +move.squareId); + const p2Moves = moves + .filter((move) => move.playerId === 2) + .map((move) => +move.squareId); + + const winningPatterns = [ + [1, 2, 3], + [1, 5, 9], + [1, 4, 7], + [2, 5, 8], + [3, 5, 7], + [3, 6, 9], + [4, 5, 6], + [7, 8, 9], + ]; + + let winner = null; + + winningPatterns.forEach((pattern) => { + const p1Wins = pattern.every((v) => p1Moves.includes(v)); + const p2Wins = pattern.every((v) => p2Moves.includes(v)); + + if (p1Wins) winner = 1; + if (p2Wins) winner = 2; + }); + + return { + status: moves.length === 9 || winner != null ? "complete" : "in-progress", // in-progress | complete + winner, // 1 | 2 | null + }; + }, + + init() { + App.registerEventListeners(); + }, + + registerEventListeners() { + // DONE + App.$.menu.addEventListener("click", (event) => { + App.$.menuItems.classList.toggle("hidden"); + }); + + // TODO + App.$.resetBtn.addEventListener("click", (event) => { + console.log("Reset the game"); + }); + + // TODO + App.$.newRoundBtn.addEventListener("click", (event) => { + console.log("Add a new round"); + }); + + App.$.modalBtn.addEventListener("click", (event) => { + App.state.moves = []; + App.$.squares.forEach((square) => square.replaceChildren()); + App.$.modal.classList.add("hidden"); + }); + + // TODO + App.$.squares.forEach((square) => { + square.addEventListener("click", (event) => { + // Check if there is already a play, if so, return early + const hasMove = (squareId) => { + const existingMove = App.state.moves.find( + (move) => move.squareId === squareId + ); + return existingMove !== undefined; + }; + + if (hasMove(+square.id)) { + return; + } + + // Determine which player icon to add to the square + const lastMove = App.state.moves.at(-1); + const getOppositePlayer = (playerId) => (playerId === 1 ? 2 : 1); + const currentPlayer = + App.state.moves.length === 0 + ? 1 + : getOppositePlayer(lastMove.playerId); + const nextPlayer = getOppositePlayer(currentPlayer); + + const squareIcon = document.createElement("i"); + const turnIcon = document.createElement("i"); + const turnLabel = document.createElement("p"); + turnLabel.innerText = `Player ${nextPlayer}, you are up!`; + + if (currentPlayer === 1) { + squareIcon.classList.add("fa-solid", "fa-x", "yellow"); + turnIcon.classList.add("fa-solid", "fa-o", "turquoise"); + turnLabel.classList = "turquoise"; + } else { + squareIcon.classList.add("fa-solid", "fa-o", "turquoise"); + turnIcon.classList.add("fa-solid", "fa-x", "yellow"); + turnLabel.classList = "yellow"; + } + + App.$.turn.replaceChildren(turnIcon, turnLabel); + + App.state.moves.push({ + squareId: +square.id, + playerId: currentPlayer, + }); + + square.replaceChildren(squareIcon); + + // Check if there is a winner or tie game + const game = App.getGameStatus(App.state.moves); + + if (game.status === "complete") { + App.$.modal.classList.remove("hidden"); + + let message = ""; + if (game.winner) { + message = `Player ${game.winner} wins!`; + } else { + message = "Tie game!"; + } + + App.$.modalText.textContent = message; + } + }); + }); + }, +}; + +window.addEventListener("load", App.init); diff --git a/vanilla-refactor/css/index.css b/vanilla-refactor/css/index.css index 108e100..929e615 100644 --- a/vanilla-refactor/css/index.css +++ b/vanilla-refactor/css/index.css @@ -44,9 +44,6 @@ button:hover { } @media (min-width: 768px) { - /** - * - */ .grid { width: 490px; grid-template-columns: repeat(3, 150px); diff --git a/vanilla-refactor/index.html b/vanilla-refactor/index.html index bffb421..8baf5e4 100644 --- a/vanilla-refactor/index.html +++ b/vanilla-refactor/index.html @@ -31,7 +31,6 @@ -