├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── app ├── js │ └── script.js └── scss │ ├── components │ ├── _index.scss │ ├── component1.scss │ └── guess.scss │ ├── globals │ ├── _index.scss │ ├── boilerplate.scss │ ├── colors.scss │ ├── fonts.scss │ ├── layout.scss │ └── typography.scss │ ├── style.scss │ └── util │ ├── _index.scss │ ├── animations.scss │ ├── breakpoints.scss │ └── functions.scss ├── dist ├── script.js ├── script.js.map ├── style.css └── style.css.map ├── gulpfile.js ├── index.html ├── notes.md ├── package-lock.json └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 thecodercoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wordle Clone with HTML, Sass, and JavaScript 2 | 3 | The code in this repo is from my YouTube tutorial, [building a Wordle clone from scratch](https://youtu.be/PNGgQzw6PQg). 4 | -------------------------------------------------------------------------------- /app/js/script.js: -------------------------------------------------------------------------------- 1 | //console.log('keypress'); 2 | const lettersPattern = /[a-z]/; // /^[A-Za-z][A-Za-z0-9]*$/; 3 | let currentGuessCount = 1; 4 | let currentGuess = document.querySelector('#guess' + currentGuessCount); 5 | let words = ['apple', 'baker', 'store', 'horse', 'speak', 'clone', 'bread']; 6 | let solutionWord = ''; 7 | 8 | const chooseWord = () => { 9 | // choose random item from words array 10 | let randomItem = Math.floor(Math.random() * (words.length - 1)) + 1; 11 | solutionWord = words[randomItem]; 12 | }; 13 | 14 | chooseWord(); 15 | //console.log('solution word = ' + solutionWord); 16 | 17 | // detect keypress (letter, backspace, enter, other) 18 | document.addEventListener('keydown', (e) => { 19 | //console.log('keypress: ' + e.key); 20 | let keypress = e.key; 21 | if (currentGuessCount < 7) { 22 | if ( 23 | keypress.length == 1 && 24 | lettersPattern.test(e.key) && 25 | currentGuess.dataset.letters.length < 5 26 | ) { 27 | //console.log('is letter'); 28 | updateLetters(keypress); 29 | } else if (e.key == 'Backspace' && currentGuess.dataset.letters != '') { 30 | //console.log('is backspace'); 31 | deleteFromLetters(); 32 | } else if (e.key == 'Enter' && currentGuess.dataset.letters.length == 5) { 33 | submitGuess(); 34 | } 35 | } 36 | }); 37 | 38 | const submitGuess = () => { 39 | //console.log('submit guess'); 40 | for (let i = 0; i < 5; i++) { 41 | setTimeout(() => { 42 | revealTile(i, checkLetter(i)); 43 | }, i * 200); 44 | } 45 | }; 46 | 47 | const checkIfGuessComplete = (i) => { 48 | if (i == 4) { 49 | checkWin(); 50 | } 51 | }; 52 | 53 | const jumpTiles = () => { 54 | //console.log('jumpTiles'); 55 | //console.log(currentGuessCount); 56 | for (let i = 0; i < 5; i++) { 57 | setTimeout(() => { 58 | let currentTile = document.querySelector( 59 | '#guess' + currentGuessCount + 'Tile' + (i + 1) 60 | ); 61 | currentTile.classList.add('jump'); 62 | }, i * 200); 63 | } 64 | }; 65 | 66 | const checkWin = () => { 67 | //console.log('check win'); 68 | if (solutionWord == currentGuess.dataset.letters) { 69 | // Win 70 | //console.log('game is won!'); 71 | setTimeout(() => { 72 | jumpTiles(); 73 | }, 500); 74 | } else { 75 | // Not won 76 | currentGuessCount = currentGuessCount + 1; 77 | currentGuess = document.querySelector('#guess' + currentGuessCount); 78 | //console.log('not a win, increment guess count to ' + currentGuessCount); 79 | if (currentGuessCount == 7) { 80 | setTimeout(() => { 81 | showSolution(); 82 | }, 500); 83 | } 84 | } 85 | }; 86 | 87 | const showSolution = () => { 88 | alert('Better luck next time. The solution was: ' + solutionWord); 89 | }; 90 | 91 | // Update "letters" 92 | const updateLetters = (letter) => { 93 | let oldLetters = currentGuess.dataset.letters; 94 | let newLetters = oldLetters + letter; 95 | let currentTile = newLetters.length; 96 | currentGuess.dataset.letters = newLetters; 97 | //console.log('currentTile = ' + currentTile); 98 | updateTiles(currentTile, letter); 99 | }; 100 | 101 | // Update tile markup 102 | const updateTiles = (tileNumber, letter) => { 103 | //console.log('updateTiles(' + tileNumber, letter + ')'); 104 | let currentTile = document.querySelector( 105 | '#guess' + currentGuessCount + 'Tile' + tileNumber 106 | ); 107 | currentTile.innerText = letter; 108 | currentTile.classList.add('has-letter'); 109 | }; 110 | 111 | // Backspace -- Delete last letter 112 | const deleteFromLetters = () => { 113 | // remove last letter from data-letters 114 | let oldLetters = currentGuess.dataset.letters; 115 | //console.log('oldLetters = ' + oldLetters); 116 | let newLetters = oldLetters.slice(0, -1); 117 | //console.log('newLetters = ' + newLetters); 118 | currentGuess.dataset.letters = newLetters; 119 | deleteFromTiles(oldLetters.length); 120 | }; 121 | 122 | // Backspace -- Delete last tile markup 123 | const deleteFromTiles = (tileNumber) => { 124 | // remove markup from last tile 125 | //console.log('deleteFromTiles = ' + tileNumber); 126 | let currentTile = document.querySelector( 127 | '#guess' + currentGuessCount + 'Tile' + tileNumber 128 | ); 129 | currentTile.innerText = ''; 130 | currentTile.classList.remove('has-letter'); 131 | }; 132 | 133 | // Check letter to solution 134 | // parameter = letter position in word 135 | const checkLetter = (position) => { 136 | //console.log('checkLetter'); 137 | let guessedLetter = currentGuess.dataset.letters.charAt(position); 138 | let solutionLetter = solutionWord.charAt(position); 139 | //console.log(guessedLetter, solutionLetter); 140 | 141 | // If letters match, return "correct" 142 | if (guessedLetter == solutionLetter) { 143 | return 'correct'; 144 | } 145 | // If not a match, if letter exists in solution word, return "present" 146 | else { 147 | return checkLetterExists(guessedLetter) ? 'present' : 'absent'; 148 | } 149 | 150 | // If not a match, if letter doesn't exist in solution, return "absent" 151 | }; 152 | 153 | const checkLetterExists = (letter) => { 154 | return solutionWord.includes(letter); 155 | }; 156 | 157 | const revealTile = (i, state) => { 158 | //console.log('revealTile = ' + i, state); 159 | let tileNum = i + 1; 160 | flipTile(tileNum, state); 161 | checkIfGuessComplete(i); 162 | }; 163 | 164 | const flipTile = (tileNum, state) => { 165 | let tile = document.querySelector( 166 | '#guess' + currentGuessCount + 'Tile' + tileNum 167 | ); 168 | tile.classList.add('flip-in'); 169 | setTimeout(() => { 170 | tile.classList.add(state); 171 | }, 250); 172 | setTimeout(() => { 173 | tile.classList.remove('flip-in'); 174 | tile.classList.add('flip-out'); 175 | }, 250); 176 | setTimeout(() => { 177 | tile.classList.remove('flip-out'); 178 | }, 1500); 179 | }; 180 | /* 181 | - if keypress is a letter 182 | - update "letters" attribute 183 | - update tile markup based on "letters" value 184 | - if keypress is backspace 185 | - delete last letter in "letters" 186 | - update tile markup based on "letters" 187 | */ 188 | -------------------------------------------------------------------------------- /app/scss/components/_index.scss: -------------------------------------------------------------------------------- 1 | // Example 2 | @forward 'guess'; 3 | -------------------------------------------------------------------------------- /app/scss/components/component1.scss: -------------------------------------------------------------------------------- 1 | @use '../util' as *; 2 | -------------------------------------------------------------------------------- /app/scss/components/guess.scss: -------------------------------------------------------------------------------- 1 | @use '../util' as *; 2 | 3 | .guess { 4 | display: grid; 5 | grid-template-columns: repeat(5, 1fr); 6 | gap: rem(3); 7 | width: rem(300); 8 | margin-bottom: rem(10); 9 | 10 | &__tile { 11 | border: rem(2) solid hsl(0, 0%, 26%); 12 | width: rem(52); 13 | height: rem(52); 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | font-size: rem(32); 18 | font-weight: 600; 19 | } 20 | 21 | .has-letter { 22 | border-color: hsl(0, 0%, 40%); 23 | } 24 | 25 | .correct { 26 | background-color: hsl(120, 100%, 28%); 27 | border-color: hsl(120, 100%, 28%); 28 | } 29 | 30 | .present { 31 | background-color: hsl(49, 69%, 47%); 32 | border-color: hsl(49, 69%, 47%); 33 | } 34 | 35 | .absent { 36 | background-color: hsl(0, 0%, 35%); 37 | border-color: hsl(0, 0%, 35%); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/scss/globals/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'fonts'; 2 | @forward 'colors'; 3 | @forward 'boilerplate'; 4 | @forward 'typography'; 5 | @forward 'layout'; 6 | -------------------------------------------------------------------------------- /app/scss/globals/boilerplate.scss: -------------------------------------------------------------------------------- 1 | @use '../util' as *; 2 | 3 | html { 4 | font-size: 100%; 5 | font-family: var(--font-body); 6 | box-sizing: border-box; 7 | } 8 | 9 | *, 10 | *::before, 11 | *::after { 12 | box-sizing: inherit; 13 | } 14 | 15 | body { 16 | margin: 0; 17 | padding: 1rem 2rem; 18 | background-color: hsl(240, 3%, 7%); 19 | color: hsl(0, 0%, 100%); 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | text-transform: uppercase; 24 | font-weight: 600; 25 | } 26 | -------------------------------------------------------------------------------- /app/scss/globals/colors.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --white: hsl(0, 0%, 100%); 3 | --black: hsl(0, 0%, 0%); 4 | } 5 | -------------------------------------------------------------------------------- /app/scss/globals/fonts.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-body: 'Open Sans', Arial, sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /app/scss/globals/layout.scss: -------------------------------------------------------------------------------- 1 | @use '../util' as *; 2 | 3 | .container { 4 | padding: 0 rem(24); 5 | max-width: rem(1110); 6 | margin: 0 auto; 7 | 8 | @include breakpoint(large) { 9 | padding: 0; 10 | transform: translate(0, 0); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/scss/globals/typography.scss: -------------------------------------------------------------------------------- 1 | @use '../util' as *; 2 | 3 | h1, 4 | h2, 5 | h3 { 6 | margin-top: 0; 7 | line-height: 1.1; 8 | } 9 | 10 | h1 { 11 | font-size: rem(36); 12 | letter-spacing: rem(2); 13 | } 14 | 15 | a, 16 | a:visited, 17 | a:active { 18 | text-decoration: none; 19 | } 20 | -------------------------------------------------------------------------------- /app/scss/style.scss: -------------------------------------------------------------------------------- 1 | @use 'globals'; 2 | @use 'components'; 3 | -------------------------------------------------------------------------------- /app/scss/util/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'breakpoints'; 2 | @forward 'functions'; 3 | @forward 'animations'; 4 | -------------------------------------------------------------------------------- /app/scss/util/animations.scss: -------------------------------------------------------------------------------- 1 | @use 'functions' as *; 2 | 3 | @keyframes flip-in { 4 | from { 5 | transform: rotateX(0deg); 6 | } 7 | to { 8 | transform: rotateX(-90deg); 9 | } 10 | } 11 | 12 | @keyframes flip-out { 13 | from { 14 | transform: rotateX(-90deg); 15 | } 16 | to { 17 | transform: rotateX(0deg); 18 | } 19 | } 20 | 21 | .flip-in { 22 | animation: flip-in 250ms; 23 | animation-fill-mode: forwards; 24 | } 25 | 26 | .flip-out { 27 | animation: flip-out 250ms; 28 | animation-fill-mode: forwards; 29 | } 30 | 31 | @keyframes jump { 32 | 0% { 33 | transform: translateY(0); 34 | } 35 | 50% { 36 | transform: translateY(rem(-10)); 37 | } 38 | 100% { 39 | transform: translateY(0); 40 | } 41 | } 42 | 43 | .jump { 44 | animation: jump 250ms; 45 | animation-fill-mode: forwards; 46 | } 47 | -------------------------------------------------------------------------------- /app/scss/util/breakpoints.scss: -------------------------------------------------------------------------------- 1 | // 640px, 1150px, 1400px 2 | $breakpoints-up: ( 3 | 'medium': '40em', 4 | 'large': '71.875em', 5 | 'xlarge': '87.5em', 6 | ); 7 | 8 | // 639px, 1149px, 1399px 9 | $breakpoints-down: ( 10 | 'small': '39.9375em', 11 | 'medium': '71.8125em', 12 | 'large': '87.4375em', 13 | ); 14 | 15 | @mixin breakpoint($size) { 16 | @media (min-width: map-get($breakpoints-up, $size)) { 17 | @content; 18 | } 19 | } 20 | 21 | @mixin breakpoint-down($size) { 22 | @media (max-width: map-get($breakpoints-down, $size)) { 23 | @content; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/scss/util/functions.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | // Source: https://css-tricks.com/snippets/sass/px-to-em-functions/ 4 | @function rem($pixels, $context: 16) { 5 | @return (math.div($pixels, $context)) * 1rem; 6 | } 7 | -------------------------------------------------------------------------------- /dist/script.js: -------------------------------------------------------------------------------- 1 | "use strict";var lettersPattern=/[a-z]/,currentGuessCount=1,currentGuess=document.querySelector("#guess"+currentGuessCount),words=["apple","baker","store","horse","speak","clone","bread"],solutionWord="",chooseWord=function(){var e=Math.floor(Math.random()*(words.length-1))+1;solutionWord=words[e]};chooseWord(),document.addEventListener("keydown",(function(e){var t=e.key;currentGuessCount<7&&(1==t.length&&lettersPattern.test(e.key)&¤tGuess.dataset.letters.length<5?updateLetters(t):"Backspace"==e.key&&""!=currentGuess.dataset.letters?deleteFromLetters():"Enter"==e.key&&5==currentGuess.dataset.letters.length&&submitGuess())}));var submitGuess=function(){for(var e=function(e){setTimeout((function(){revealTile(e,checkLetter(e))}),200*e)},t=0;t<5;t++)e(t)},checkIfGuessComplete=function(e){4==e&&checkWin()},jumpTiles=function(){for(var e=function(e){setTimeout((function(){document.querySelector("#guess"+currentGuessCount+"Tile"+(e+1)).classList.add("jump")}),200*e)},t=0;t<5;t++)e(t)},checkWin=function(){solutionWord==currentGuess.dataset.letters?setTimeout((function(){jumpTiles()}),500):(currentGuessCount+=1,currentGuess=document.querySelector("#guess"+currentGuessCount),7==currentGuessCount&&setTimeout((function(){showSolution()}),500))},showSolution=function(){alert("Better luck next time. The solution was: "+solutionWord)},updateLetters=function(e){var t=currentGuess.dataset.letters+e,s=t.length;currentGuess.dataset.letters=t,updateTiles(s,e)},updateTiles=function(e,t){var s=document.querySelector("#guess"+currentGuessCount+"Tile"+e);s.innerText=t,s.classList.add("has-letter")},deleteFromLetters=function(){var e=currentGuess.dataset.letters,t=e.slice(0,-1);currentGuess.dataset.letters=t,deleteFromTiles(e.length)},deleteFromTiles=function(e){var t=document.querySelector("#guess"+currentGuessCount+"Tile"+e);t.innerText="",t.classList.remove("has-letter")},checkLetter=function(e){var t=currentGuess.dataset.letters.charAt(e);return t==solutionWord.charAt(e)?"correct":checkLetterExists(t)?"present":"absent"},checkLetterExists=function(e){return solutionWord.includes(e)},revealTile=function(e,t){flipTile(e+1,t),checkIfGuessComplete(e)},flipTile=function(e,t){var s=document.querySelector("#guess"+currentGuessCount+"Tile"+e);s.classList.add("flip-in"),setTimeout((function(){s.classList.add(t)}),250),setTimeout((function(){s.classList.remove("flip-in"),s.classList.add("flip-out")}),250),setTimeout((function(){s.classList.remove("flip-out")}),1500)}; 2 | //# sourceMappingURL=script.js.map -------------------------------------------------------------------------------- /dist/script.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["script.js"],"names":["lettersPattern","currentGuessCount","currentGuess","document","querySelector","words","solutionWord","chooseWord","randomItem","Math","floor","random","length","addEventListener","e","keypress","key","test","dataset","letters","updateLetters","deleteFromLetters","submitGuess","_loop","i","setTimeout","revealTile","checkLetter","checkIfGuessComplete","checkWin","jumpTiles","_loop2","classList","add","showSolution","alert","letter","newLetters","currentTile","updateTiles","tileNumber","innerText","oldLetters","slice","deleteFromTiles","remove","position","guessedLetter","charAt","checkLetterExists","includes","state","flipTile","tileNum","tile"],"mappings":"AAAA,aACA,IAAMA,eAAiB,QACnBC,kBAAoB,EACpBC,aAAeC,SAASC,cAAc,SAAWH,mBACjDI,MAAQ,CAAC,QAAS,QAAS,QAAS,QAAS,QAAS,QAAS,SAC/DC,aAAe,GAEbC,WAAa,WAEjB,IAAIC,EAAaC,KAAKC,MAAMD,KAAKE,UAAYN,MAAMO,OAAS,IAAM,EAClEN,aAAeD,MAAMG,IAGvBD,aAIAJ,SAASU,iBAAiB,WAAW,SAACC,GAEpC,IAAIC,EAAWD,EAAEE,IACbf,kBAAoB,IAED,GAAnBc,EAASH,QACTZ,eAAeiB,KAAKH,EAAEE,MACtBd,aAAagB,QAAQC,QAAQP,OAAS,EAGtCQ,cAAcL,GACI,aAATD,EAAEE,KAAsD,IAAhCd,aAAagB,QAAQC,QAEtDE,oBACkB,SAATP,EAAEE,KAAyD,GAAvCd,aAAagB,QAAQC,QAAQP,QAC1DU,kBAKN,IAAMA,YAAc,WAElB,IAFwB,IAAAC,EAAA,SAEfC,GACPC,YAAW,WACTC,WAAWF,EAAGG,YAAYH,MACrB,IAAJA,IAHIA,EAAI,EAAGA,EAAI,EAAGA,IAAKD,EAAnBC,IAOLI,qBAAuB,SAACJ,GACnB,GAALA,GACFK,YAIEC,UAAY,WAGhB,IAHsB,IAAAC,EAAA,SAGbP,GACPC,YAAW,WACStB,SAASC,cACzB,SAAWH,kBAAoB,QAAUuB,EAAI,IAEnCQ,UAAUC,IAAI,UACrB,IAAJT,IANIA,EAAI,EAAGA,EAAI,EAAGA,IAAKO,EAAnBP,IAULK,SAAW,WAEXvB,cAAgBJ,aAAagB,QAAQC,QAGvCM,YAAW,WACTK,cACC,MAGH7B,mBAAwC,EACxCC,aAAeC,SAASC,cAAc,SAAWH,mBAExB,GAArBA,mBACFwB,YAAW,WACTS,iBACC,OAKHA,aAAe,WACnBC,MAAM,4CAA8C7B,eAIhDc,cAAgB,SAACgB,GACrB,IACIC,EADanC,aAAagB,QAAQC,QACRiB,EAC1BE,EAAcD,EAAWzB,OAC7BV,aAAagB,QAAQC,QAAUkB,EAE/BE,YAAYD,EAAaF,IAIrBG,YAAc,SAACC,EAAYJ,GAE/B,IAAIE,EAAcnC,SAASC,cACzB,SAAWH,kBAAoB,OAASuC,GAE1CF,EAAYG,UAAYL,EACxBE,EAAYN,UAAUC,IAAI,eAItBZ,kBAAoB,WAExB,IAAIqB,EAAaxC,aAAagB,QAAQC,QAElCkB,EAAaK,EAAWC,MAAM,GAAI,GAEtCzC,aAAagB,QAAQC,QAAUkB,EAC/BO,gBAAgBF,EAAW9B,SAIvBgC,gBAAkB,SAACJ,GAGvB,IAAIF,EAAcnC,SAASC,cACzB,SAAWH,kBAAoB,OAASuC,GAE1CF,EAAYG,UAAY,GACxBH,EAAYN,UAAUa,OAAO,eAKzBlB,YAAc,SAACmB,GAEnB,IAAIC,EAAgB7C,aAAagB,QAAQC,QAAQ6B,OAAOF,GAKxD,OAAIC,GAJiBzC,aAAa0C,OAAOF,GAKhC,UAIAG,kBAAkBF,GAAiB,UAAY,UAMpDE,kBAAoB,SAACb,GACzB,OAAO9B,aAAa4C,SAASd,IAGzBV,WAAa,SAACF,EAAG2B,GAGrBC,SADc5B,EAAI,EACA2B,GAClBvB,qBAAqBJ,IAGjB4B,SAAW,SAACC,EAASF,GACzB,IAAIG,EAAOnD,SAASC,cAClB,SAAWH,kBAAoB,OAASoD,GAE1CC,EAAKtB,UAAUC,IAAI,WACnBR,YAAW,WACT6B,EAAKtB,UAAUC,IAAIkB,KAClB,KACH1B,YAAW,WACT6B,EAAKtB,UAAUa,OAAO,WACtBS,EAAKtB,UAAUC,IAAI,cAClB,KACHR,YAAW,WACT6B,EAAKtB,UAAUa,OAAO,cACrB","file":"script.js","sourcesContent":["//console.log('keypress');\r\nconst lettersPattern = /[a-z]/; // /^[A-Za-z][A-Za-z0-9]*$/;\r\nlet currentGuessCount = 1;\r\nlet currentGuess = document.querySelector('#guess' + currentGuessCount);\r\nlet words = ['apple', 'baker', 'store', 'horse', 'speak', 'clone', 'bread'];\r\nlet solutionWord = '';\r\n\r\nconst chooseWord = () => {\r\n // choose random item from words array\r\n let randomItem = Math.floor(Math.random() * (words.length - 1)) + 1;\r\n solutionWord = words[randomItem];\r\n};\r\n\r\nchooseWord();\r\n//console.log('solution word = ' + solutionWord);\r\n\r\n// detect keypress (letter, backspace, enter, other)\r\ndocument.addEventListener('keydown', (e) => {\r\n //console.log('keypress: ' + e.key);\r\n let keypress = e.key;\r\n if (currentGuessCount < 7) {\r\n if (\r\n keypress.length == 1 &&\r\n lettersPattern.test(e.key) &&\r\n currentGuess.dataset.letters.length < 5\r\n ) {\r\n //console.log('is letter');\r\n updateLetters(keypress);\r\n } else if (e.key == 'Backspace' && currentGuess.dataset.letters != '') {\r\n //console.log('is backspace');\r\n deleteFromLetters();\r\n } else if (e.key == 'Enter' && currentGuess.dataset.letters.length == 5) {\r\n submitGuess();\r\n }\r\n }\r\n});\r\n\r\nconst submitGuess = () => {\r\n //console.log('submit guess');\r\n for (let i = 0; i < 5; i++) {\r\n setTimeout(() => {\r\n revealTile(i, checkLetter(i));\r\n }, i * 200);\r\n }\r\n};\r\n\r\nconst checkIfGuessComplete = (i) => {\r\n if (i == 4) {\r\n checkWin();\r\n }\r\n};\r\n\r\nconst jumpTiles = () => {\r\n //console.log('jumpTiles');\r\n //console.log(currentGuessCount);\r\n for (let i = 0; i < 5; i++) {\r\n setTimeout(() => {\r\n let currentTile = document.querySelector(\r\n '#guess' + currentGuessCount + 'Tile' + (i + 1)\r\n );\r\n currentTile.classList.add('jump');\r\n }, i * 200);\r\n }\r\n};\r\n\r\nconst checkWin = () => {\r\n //console.log('check win');\r\n if (solutionWord == currentGuess.dataset.letters) {\r\n // Win\r\n //console.log('game is won!');\r\n setTimeout(() => {\r\n jumpTiles();\r\n }, 500);\r\n } else {\r\n // Not won\r\n currentGuessCount = currentGuessCount + 1;\r\n currentGuess = document.querySelector('#guess' + currentGuessCount);\r\n //console.log('not a win, increment guess count to ' + currentGuessCount);\r\n if (currentGuessCount == 7) {\r\n setTimeout(() => {\r\n showSolution();\r\n }, 500);\r\n }\r\n }\r\n};\r\n\r\nconst showSolution = () => {\r\n alert('Better luck next time. The solution was: ' + solutionWord);\r\n};\r\n\r\n// Update \"letters\"\r\nconst updateLetters = (letter) => {\r\n let oldLetters = currentGuess.dataset.letters;\r\n let newLetters = oldLetters + letter;\r\n let currentTile = newLetters.length;\r\n currentGuess.dataset.letters = newLetters;\r\n //console.log('currentTile = ' + currentTile);\r\n updateTiles(currentTile, letter);\r\n};\r\n\r\n// Update tile markup\r\nconst updateTiles = (tileNumber, letter) => {\r\n //console.log('updateTiles(' + tileNumber, letter + ')');\r\n let currentTile = document.querySelector(\r\n '#guess' + currentGuessCount + 'Tile' + tileNumber\r\n );\r\n currentTile.innerText = letter;\r\n currentTile.classList.add('has-letter');\r\n};\r\n\r\n// Backspace -- Delete last letter\r\nconst deleteFromLetters = () => {\r\n // remove last letter from data-letters\r\n let oldLetters = currentGuess.dataset.letters;\r\n //console.log('oldLetters = ' + oldLetters);\r\n let newLetters = oldLetters.slice(0, -1);\r\n //console.log('newLetters = ' + newLetters);\r\n currentGuess.dataset.letters = newLetters;\r\n deleteFromTiles(oldLetters.length);\r\n};\r\n\r\n// Backspace -- Delete last tile markup\r\nconst deleteFromTiles = (tileNumber) => {\r\n // remove markup from last tile\r\n //console.log('deleteFromTiles = ' + tileNumber);\r\n let currentTile = document.querySelector(\r\n '#guess' + currentGuessCount + 'Tile' + tileNumber\r\n );\r\n currentTile.innerText = '';\r\n currentTile.classList.remove('has-letter');\r\n};\r\n\r\n// Check letter to solution\r\n// parameter = letter position in word\r\nconst checkLetter = (position) => {\r\n //console.log('checkLetter');\r\n let guessedLetter = currentGuess.dataset.letters.charAt(position);\r\n let solutionLetter = solutionWord.charAt(position);\r\n //console.log(guessedLetter, solutionLetter);\r\n\r\n // If letters match, return \"correct\"\r\n if (guessedLetter == solutionLetter) {\r\n return 'correct';\r\n }\r\n // If not a match, if letter exists in solution word, return \"present\"\r\n else {\r\n return checkLetterExists(guessedLetter) ? 'present' : 'absent';\r\n }\r\n\r\n // If not a match, if letter doesn't exist in solution, return \"absent\"\r\n};\r\n\r\nconst checkLetterExists = (letter) => {\r\n return solutionWord.includes(letter);\r\n};\r\n\r\nconst revealTile = (i, state) => {\r\n //console.log('revealTile = ' + i, state);\r\n let tileNum = i + 1;\r\n flipTile(tileNum, state);\r\n checkIfGuessComplete(i);\r\n};\r\n\r\nconst flipTile = (tileNum, state) => {\r\n let tile = document.querySelector(\r\n '#guess' + currentGuessCount + 'Tile' + tileNum\r\n );\r\n tile.classList.add('flip-in');\r\n setTimeout(() => {\r\n tile.classList.add(state);\r\n }, 250);\r\n setTimeout(() => {\r\n tile.classList.remove('flip-in');\r\n tile.classList.add('flip-out');\r\n }, 250);\r\n setTimeout(() => {\r\n tile.classList.remove('flip-out');\r\n }, 1500);\r\n};\r\n/*\r\n- if keypress is a letter\r\n - update \"letters\" attribute\r\n - update tile markup based on \"letters\" value\r\n- if keypress is backspace\r\n - delete last letter in \"letters\"\r\n - update tile markup based on \"letters\"\r\n*/\r\n"]} -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | :root{--font-body:"Open Sans",Arial,sans-serif;--white:#fff;--black:#000}@-webkit-keyframes flip-in{0%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg)}to{-webkit-transform:rotateX(-90deg);transform:rotateX(-90deg)}}@keyframes flip-in{0%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg)}to{-webkit-transform:rotateX(-90deg);transform:rotateX(-90deg)}}@-webkit-keyframes flip-out{0%{-webkit-transform:rotateX(-90deg);transform:rotateX(-90deg)}to{-webkit-transform:rotateX(0deg);transform:rotateX(0deg)}}@keyframes flip-out{0%{-webkit-transform:rotateX(-90deg);transform:rotateX(-90deg)}to{-webkit-transform:rotateX(0deg);transform:rotateX(0deg)}}.flip-in{-webkit-animation:flip-in .25s;animation:flip-in .25s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.flip-out{-webkit-animation:flip-out .25s;animation:flip-out .25s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}@-webkit-keyframes jump{0%{-webkit-transform:translateY(0);transform:translateY(0)}50%{-webkit-transform:translateY(-.625rem);transform:translateY(-.625rem)}to{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes jump{0%{-webkit-transform:translateY(0);transform:translateY(0)}50%{-webkit-transform:translateY(-.625rem);transform:translateY(-.625rem)}to{-webkit-transform:translateY(0);transform:translateY(0)}}.jump{-webkit-animation:jump .25s;animation:jump .25s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}html{box-sizing:border-box;font-family:var(--font-body);font-size:100%}*,:after,:before{box-sizing:inherit}body{align-items:center;background-color:#111112;color:#fff;display:flex;flex-direction:column;font-weight:600;margin:0;padding:1rem 2rem;text-transform:uppercase}h1,h2,h3{line-height:1.1;margin-top:0}h1{font-size:2.25rem;letter-spacing:.125rem}a,a:active,a:visited{text-decoration:none}.container{margin:0 auto;max-width:69.375rem;padding:0 1.5rem}@media (min-width:71.875em){.container{padding:0;-webkit-transform:translate(0);transform:translate(0)}}.guess{display:grid;gap:.1875rem;grid-template-columns:repeat(5,1fr);margin-bottom:.625rem;width:18.75rem}.guess__tile{align-items:center;border:.125rem solid #424242;display:flex;font-size:2rem;font-weight:600;height:3.25rem;justify-content:center;width:3.25rem}.guess .has-letter{border-color:#666}.guess .correct{background-color:#008f00;border-color:#008f00}.guess .present{background-color:#cbac25;border-color:#cbac25}.guess .absent{background-color:#595959;border-color:#595959} 2 | /*# sourceMappingURL=style.css.map */ -------------------------------------------------------------------------------- /dist/style.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["globals/fonts.scss","globals/colors.scss","style.css","util/animations.scss","globals/boilerplate.scss","globals/typography.scss","globals/layout.scss","util/breakpoints.scss","components/guess.scss"],"names":[],"mappings":"AAAA,MACE,wCAAA,CCAD,YAAA,CACA,YCAD,CCAA,2BACE,GACE,+BAAA,CAAA,uBDQF,CCNA,GACE,iCAAA,CAAA,yBDQF,CACF,CCdA,mBACE,GACE,+BAAA,CAAA,uBDQF,CCNA,GACE,iCAAA,CAAA,yBDQF,CACF,CCLA,4BACE,GACE,iCAAA,CAAA,yBDOF,CCLA,GACE,+BAAA,CAAA,uBDOF,CACF,CCbA,oBACE,GACE,iCAAA,CAAA,yBDOF,CCLA,GACE,+BAAA,CAAA,uBDOF,CACF,CCJA,SACE,8BAAA,CAAA,sBAAA,CACA,oCAAA,CAAA,4BDMF,CCHA,UACE,+BAAA,CAAA,uBAAA,CACA,oCAAA,CAAA,4BDMF,CCHA,wBACE,GACE,+BAAA,CAAA,uBDMF,CCJA,IACE,sCAAA,CAAA,8BDMF,CCJA,GACE,+BAAA,CAAA,uBDMF,CACF,CCfA,gBACE,GACE,+BAAA,CAAA,uBDMF,CCJA,IACE,sCAAA,CAAA,8BDMF,CCJA,GACE,+BAAA,CAAA,uBDMF,CACF,CCHA,MACE,2BAAA,CAAA,mBAAA,CACA,oCAAA,CAAA,4BDKF,CE/CA,KAGE,qBAAA,CADA,4BAAA,CADA,cFoDF,CE/CA,iBAGE,kBFkDF,CE/CA,KAOE,kBAAA,CAJA,wBAAA,CACA,UAAA,CACA,YAAA,CACA,qBAAA,CAGA,eAAA,CARA,QAAA,CACA,iBAAA,CAMA,wBFmDF,CGvEA,SAIE,eAAA,CADA,YH2EF,CGvEA,GACE,iBAAA,CACA,sBH0EF,CGvEA,qBAGE,oBH0EF,CIzFA,WAGC,aAAA,CADA,mBAAA,CADA,gBJ8FD,CKlFC,4BDbD,WAME,SAAA,CACA,8BAAA,CAAA,sBJ6FA,CACF,CMrGA,OACE,YAAA,CAEA,YAAA,CADA,mCAAA,CAGA,qBAAA,CADA,cNyGF,CMtGE,aAME,kBAAA,CALA,4BAAA,CAGA,YAAA,CAGA,cAAA,CACA,eAAA,CALA,cAAA,CAEA,sBAAA,CAHA,aN8GJ,CMrGE,mBACE,iBNuGJ,CMpGE,gBACE,wBAAA,CACA,oBNsGJ,CMnGE,gBACE,wBAAA,CACA,oBNqGJ,CMlGE,eACE,wBAAA,CACA,oBNoGJ","file":"style.css","sourcesContent":[":root {\r\n --font-body: 'Open Sans', Arial, sans-serif;\r\n}\r\n",":root {\r\n\t--white: hsl(0, 0%, 100%);\r\n\t--black: hsl(0, 0%, 0%);\r\n}\r\n",":root {\n --font-body: \"Open Sans\", Arial, sans-serif;\n}\n\n:root {\n --white: hsl(0, 0%, 100%);\n --black: hsl(0, 0%, 0%);\n}\n\n@keyframes flip-in {\n from {\n transform: rotateX(0deg);\n }\n to {\n transform: rotateX(-90deg);\n }\n}\n@keyframes flip-out {\n from {\n transform: rotateX(-90deg);\n }\n to {\n transform: rotateX(0deg);\n }\n}\n.flip-in {\n animation: flip-in 250ms;\n animation-fill-mode: forwards;\n}\n\n.flip-out {\n animation: flip-out 250ms;\n animation-fill-mode: forwards;\n}\n\n@keyframes jump {\n 0% {\n transform: translateY(0);\n }\n 50% {\n transform: translateY(-0.625rem);\n }\n 100% {\n transform: translateY(0);\n }\n}\n.jump {\n animation: jump 250ms;\n animation-fill-mode: forwards;\n}\n\nhtml {\n font-size: 100%;\n font-family: var(--font-body);\n box-sizing: border-box;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\nbody {\n margin: 0;\n padding: 1rem 2rem;\n background-color: #111112;\n color: white;\n display: flex;\n flex-direction: column;\n align-items: center;\n text-transform: uppercase;\n font-weight: 600;\n}\n\nh1,\nh2,\nh3 {\n margin-top: 0;\n line-height: 1.1;\n}\n\nh1 {\n font-size: 2.25rem;\n letter-spacing: 0.125rem;\n}\n\na,\na:visited,\na:active {\n text-decoration: none;\n}\n\n.container {\n padding: 0 1.5rem;\n max-width: 69.375rem;\n margin: 0 auto;\n}\n@media (min-width: 71.875em) {\n .container {\n padding: 0;\n transform: translate(0, 0);\n }\n}\n\n.guess {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n gap: 0.1875rem;\n width: 18.75rem;\n margin-bottom: 0.625rem;\n}\n.guess__tile {\n border: 0.125rem solid #424242;\n width: 3.25rem;\n height: 3.25rem;\n display: flex;\n justify-content: center;\n align-items: center;\n font-size: 2rem;\n font-weight: 600;\n}\n.guess .has-letter {\n border-color: #666666;\n}\n.guess .correct {\n background-color: #008f00;\n border-color: #008f00;\n}\n.guess .present {\n background-color: #cbac25;\n border-color: #cbac25;\n}\n.guess .absent {\n background-color: #595959;\n border-color: #595959;\n}","@use 'functions' as *;\r\n\r\n@keyframes flip-in {\r\n from {\r\n transform: rotateX(0deg);\r\n }\r\n to {\r\n transform: rotateX(-90deg);\r\n }\r\n}\r\n\r\n@keyframes flip-out {\r\n from {\r\n transform: rotateX(-90deg);\r\n }\r\n to {\r\n transform: rotateX(0deg);\r\n }\r\n}\r\n\r\n.flip-in {\r\n animation: flip-in 250ms;\r\n animation-fill-mode: forwards;\r\n}\r\n\r\n.flip-out {\r\n animation: flip-out 250ms;\r\n animation-fill-mode: forwards;\r\n}\r\n\r\n@keyframes jump {\r\n 0% {\r\n transform: translateY(0);\r\n }\r\n 50% {\r\n transform: translateY(rem(-10));\r\n }\r\n 100% {\r\n transform: translateY(0);\r\n }\r\n}\r\n\r\n.jump {\r\n animation: jump 250ms;\r\n animation-fill-mode: forwards;\r\n}\r\n","@use '../util' as *;\r\n\r\nhtml {\r\n font-size: 100%;\r\n font-family: var(--font-body);\r\n box-sizing: border-box;\r\n}\r\n\r\n*,\r\n*::before,\r\n*::after {\r\n box-sizing: inherit;\r\n}\r\n\r\nbody {\r\n margin: 0;\r\n padding: 1rem 2rem;\r\n background-color: hsl(240, 3%, 7%);\r\n color: hsl(0, 0%, 100%);\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n text-transform: uppercase;\r\n font-weight: 600;\r\n}\r\n","@use '../util' as *;\r\n\r\nh1,\r\nh2,\r\nh3 {\r\n margin-top: 0;\r\n line-height: 1.1;\r\n}\r\n\r\nh1 {\r\n font-size: rem(36);\r\n letter-spacing: rem(2);\r\n}\r\n\r\na,\r\na:visited,\r\na:active {\r\n text-decoration: none;\r\n}\r\n","@use '../util' as *;\r\n\r\n.container {\r\n\tpadding: 0 rem(24);\r\n\tmax-width: rem(1110);\r\n\tmargin: 0 auto;\r\n\r\n\t@include breakpoint(large) {\r\n\t\tpadding: 0;\r\n\t\ttransform: translate(0, 0);\r\n\t}\r\n}\r\n","// 640px, 1150px, 1400px\r\n$breakpoints-up: (\r\n\t'medium': '40em',\r\n\t'large': '71.875em',\r\n\t'xlarge': '87.5em',\r\n);\r\n\r\n// 639px, 1149px, 1399px\r\n$breakpoints-down: (\r\n\t'small': '39.9375em',\r\n\t'medium': '71.8125em',\r\n\t'large': '87.4375em',\r\n);\r\n\r\n@mixin breakpoint($size) {\r\n\t@media (min-width: map-get($breakpoints-up, $size)) {\r\n\t\t@content;\r\n\t}\r\n}\r\n\r\n@mixin breakpoint-down($size) {\r\n\t@media (max-width: map-get($breakpoints-down, $size)) {\r\n\t\t@content;\r\n\t}\r\n}\r\n","@use '../util' as *;\r\n\r\n.guess {\r\n display: grid;\r\n grid-template-columns: repeat(5, 1fr);\r\n gap: rem(3);\r\n width: rem(300);\r\n margin-bottom: rem(10);\r\n\r\n &__tile {\r\n border: rem(2) solid hsl(0, 0%, 26%);\r\n width: rem(52);\r\n height: rem(52);\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n font-size: rem(32);\r\n font-weight: 600;\r\n }\r\n\r\n .has-letter {\r\n border-color: hsl(0, 0%, 40%);\r\n }\r\n\r\n .correct {\r\n background-color: hsl(120, 100%, 28%);\r\n border-color: hsl(120, 100%, 28%);\r\n }\r\n\r\n .present {\r\n background-color: hsl(49, 69%, 47%);\r\n border-color: hsl(49, 69%, 47%);\r\n }\r\n\r\n .absent {\r\n background-color: hsl(0, 0%, 35%);\r\n border-color: hsl(0, 0%, 35%);\r\n }\r\n}\r\n"]} -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Initialize modules 2 | const { src, dest, watch, series } = require('gulp'); 3 | const sass = require('gulp-sass')(require('sass')); 4 | const postcss = require('gulp-postcss'); 5 | const autoprefixer = require('autoprefixer'); 6 | const cssnano = require('cssnano'); 7 | const babel = require('gulp-babel'); 8 | const terser = require('gulp-terser'); 9 | const browsersync = require('browser-sync').create(); 10 | 11 | // Sass Task 12 | function scssTask() { 13 | return src('app/scss/style.scss', { sourcemaps: true }) 14 | .pipe(sass()) 15 | .pipe(postcss([autoprefixer(), cssnano()])) 16 | .pipe(dest('dist', { sourcemaps: '.' })); 17 | } 18 | 19 | // JavaScript Task 20 | function jsTask() { 21 | return src('app/js/script.js', { sourcemaps: true }) 22 | .pipe(babel({ presets: ['@babel/preset-env'] })) 23 | .pipe(terser()) 24 | .pipe(dest('dist', { sourcemaps: '.' })); 25 | } 26 | 27 | // Browsersync 28 | function browserSyncServe(cb) { 29 | browsersync.init({ 30 | server: { 31 | baseDir: '.', 32 | }, 33 | notify: { 34 | styles: { 35 | top: 'auto', 36 | bottom: '0', 37 | }, 38 | }, 39 | }); 40 | cb(); 41 | } 42 | function browserSyncReload(cb) { 43 | browsersync.reload(); 44 | cb(); 45 | } 46 | 47 | // Watch Task 48 | function watchTask() { 49 | watch('*.html', browserSyncReload); 50 | watch( 51 | ['app/scss/**/*.scss', 'app/**/*.js'], 52 | series(scssTask, jsTask, browserSyncReload) 53 | ); 54 | } 55 | 56 | // Default Gulp Task 57 | exports.default = series(scssTask, jsTask, browserSyncServe, watchTask); 58 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |