In this blog post, we will continue developing the features that exist in the original game, but for some reason were left out of the common “Wordle in React” tutorials online.
I am also planning a last, but not least, part for this series — how to modify the game to play with words different than 5 letters long!
The agenda for today is:
- The share button
- A different view depends on the user’s game state
- The progress modal — adding streak!
- Bonus: A shaky row!
- Final touches
Are you ready? 😀
The Share Button
We are going back to the progress modal!
After they finish the game, our users want to show off their success and send their friends their game process. In the original game, when you click the share button, it copies to your clipboard a string of squares that are a visual representation of the guesses in the game (🟩⬛🟨). Then, you can paste that string anywhere you want: WhatsApp, Twitter, etc.
We will build this feature in 2 steps: First, create the string of squares along with the game name, game number, and the number of guesses it took. Then, we add the “copy to clipboard” functionality.
1. Create The Squares String
There are two places you can create that squares-string in, and they both make sense. The first one is in the useWordle.js
hook, right next to where we add a new guess to the guesses array: Adding a new guess— adding a new string of squares.
The second option is adding this code where the user requests the string: In the Share button’s “onClick” function. That way, we execute this code only when we need it. This approach is cleaner, but the problem with it is that we don’t currently have access to the guesses via the modal component, and passing them just for this feature seems like a major modification.
So, I went with the first approach. In the useWordle.js
hook, similar to guesses
state, I created squearesStrings
state with a very similar logic: Based on the previous squares-strings array, create a new array and add the new guess.
Translating the guess into squares string is pretty straightforward, so we’ll move on and focus on the juicy parts instead 😀
Now that we have an array of the squares representing the guesses, we’ll pass it to the modal and stringify it there. At this point, we will create the resultString
variable, which includes the rest of the information that is shared in the message: The Game name, the game number (which we need to pass as a parameter as well), and the number of turns it took.
This worked just fine, so what made me change to the second approach? The answer to that depends on future functionality, so we’ll revisit this one later.
2. “Copy to Clipboard” Functionality
This is a cool feature that it’s implementation is easier than I thought!
The w3schools tutorial is very clear, the only modification we need is to copy our resultString
variable and not a text box like they are showing in their demo. Our line of code will be: navigator.clipboard.writeText(resultString);
Our code will look like this:
And in index.css:
I usually don’t share CSS code, because styling is an individual taste. However, this time it contains functionality. For example:
v.tooltip .tooltiptext::after {content: ""; ...}
will initialize the text after the copy, so if the user clicks the button twice, they won’t get double the content.- The
visibility
property will ensure the tooltip appears when the share button is clicked and disappears when the mouse is not hovering above the button anymore.
A Different View Depends on The User’s Game State
In the previous blog post, we created the grey “Welcome” landing page. Have you ever tried going to the Wordle website after you already played that day in that browser? If you do so, you will see a “Hey, great job today!” page.
While working on this project, I visited the Wordle website a lot. I had to see what’s the scope of every feature and test how it responds to edge cases. It was by accident that I discovered there is a third landing page — the “you’re mid-game, come continue” page!
The different view is not only on the landing page. When the user clicks the button, they see their previous guesses on the grid and the colored keys on the keyboard.
A quick recap:
Our game is a single-page application.
“A single-page application is a website that interacts with the user by dynamically rewriting the current web page with new data from the web server, instead of the default method of a web browser loading entire new pages.” Wikipedia
In our case, we don’t use a server-side, but you get the idea. This means that depending on booleans, we show and hide different components: We start with showWordle=false
, which makes the user see a grey landing page with two buttons – Play and How to Play. When they click “Play”, we set the Wordle boolean (showWordle
) to true (line 9 below). This makes the Wordle component (the page with the grid and the keyboard) show up and the landing page hides (lines 6,15 below).
When the user clicks “How to Play”, they will see the instructions modal above the Wordle component. This means that you can see the grid page in the modal’s background and closing the modal will reveal the page fully. To achieve that, we need another boolean (line 3 below) to indicate the instructions modal should show as well:
Whether the wordle boolean or the instructions boolean are true (depending on which button the user clicked)— we show the wordle component (line 16 above).
Recap is over, now we can start!
The Post-Game Welcome Page
If we look at the welcome component, the only difference between the two grey pages is the title, description, and button(s).
The indication of which page to show depends on whether the user played today, which is a boolean! We’ll call our boolean gameWasPlayedToday
and for now — set it to false (line 3 below). We use this boolean to wrap our existing landing page code (line 8) and the post-game landing page code (lines 14–20).
The post-game welcome page has one button — see stats. Similar to showing the instructions modal, we need to create a boolean and use it for the logic:
- On line 2 we initialize
showProgress
to false. - On line 17 we set it to true on the button’s
onClick
function. - On lines 6,25 we add this boolean to the page’s conditions.
- On line 29 we pass the boolean to the Wordle component, which is the one showing/hiding the modal.
Now, all that is left here is to set gameWasPlayedToday
correctly (line 3). How do we know if the user played today? We have local storage for statistics and guess distribution— could that be useful? Yes and no.
The data we save in statistics do not currently indicate if the user played today. We can modify this object to save more information, but I think it’s already big enough, and prefer creating a new local storage object: localStorage.wordlestruckLastGamePlayed
.
Creating a new local storage object
There are five things we need to know about the last game played:
gameNum
will help us know if the last game played was today.turn
,isCorrect
will be used to color the correct guess distribution bar in green.guesses
,usedKeys
will be used to populate the grid and the keyboard.
Where does our code determine when a game is over? In the Wordle component’s useEffect
! That’s where we will set our new local storage object:
Now we can go back to the landing page component and set gameWasPlayedToday
dynamically: We start by setting lastGame
to our new localStorage variable (or undefined
if there isn’t any). Then we determine if the user played today by comparing the saved game index with today’s game index.
Note that there are two ways to populate gameWasPlayedToday
, and you’ll see both in my code. When it’s only one variable dependent on whether last game
is defined, I use the inline if, and add lastGame && ...
:
var gameWasPlayedToday = lastGame && lastGame.gameNum == index ? true : false;
If there are several variables, I prefer initializing them first and then modifying them in an if block:
var gameWasPlayedToday = false;
If (localStorage.wonderstruckLastGamePlayed){
let lastGame = JSON.parse(localStorage.wonderstruckLastGamePlayed)
gameWasPlayedToday = lastGame.gameNum == index ? true : false;
...
}
So, we used the stored game index, and we have 4 more variables to utilize: turn
, isCorrect
, guesses
, and usedKeys
. We’ll apply the turn
and isCorrect
variables to update the statistics component.
The post-game landing page has a “see stats” button that opens the progress modal (which is calling the statistics component).
Up until this point, the turn
and isCorrect
variables were passed through the component’s parameters, ‘in real time’ when the game was over. We will now pull that data from the local storage variable:
We’ll start with an if condition to check if there is stored data for a previous game played, and initialize the variables for a case there isn’t (this is the “second way to initialize variables based on lastGame
” that I mentioned earlier :D).
Did you notice something extra on line 12?
Even with the new landing page, we only reach the progress modal when the game is over. So, why did I add gameWasPlayedToday && ...
to the condition for coloring barColor
? This is not part of this feature, but part of a new one that I will give you the pleasure of adding yourselves: The header buttons.
The original Wordle has buttons in the main page’s header, that give you access to the instructions and the statistics modals at any time. This means the user can start a new game, and decide they want to check their statistics before the game is over.
In that case, we want all the bars to be grey (and not green), and in my game — I also hide the solution word. In addition, I chose to hide the share button, but this is a personal preference.
We already have the functionality to show those modals thanks to the various landing pages, so all that is left to do is create two buttons and setShowProgressModal(true)
/ setShowInstructionsModal(true)
on click!
Showing guesses and used keys
We made nice progress, but we still didn’t cover all the functionality required for the post-game landing page.
When the user clicks the See Stats button, it will open the progress modal, which we took care of. When the user closes that progress modal, they should see all their past guesses on the grid, but we don’t currently have that information. So… we’ll add it now!
We already have guesses
and usedKeys
saved in localStorage.wordlestruckLastGamePlayed
object, so all that is left to do is add a view option so that if the user already played today, we’ll populate the data with the stored object.
- On lines 3–10 below, we populate
gameWasPlayedToday
,gameData
. - On lines 23–31 is the good old Wordle main page — the grid and keypad components, wrapped with the condition of
!gameWasPlayedToday && ...
. - On lines 14–22, we also have the grid and keypad components but this time we populate the properties
guesses
,turn
, andusedKeys
with the data from the stored object. And of course, we wrap this block withgameWasPlayedToday && ...
Huston, we have a problem
Another issue that comes up now is that our new shiny share button will be accessible but without a squares-string! This is because we create the squares-string in the useWordle.js
hook, but we don’t interact with this part when opening the progress modal directly from the landing page.
I know, we are jumping back and forth, and it must be confusing! No need to scroll up, I’ll give you all the context you need: To let our users show off their game skills, we created squares string representing the game guesses (without exposing the word!).
We talked about two places we can build that string and chose to go with the useWordle.js
hook, right after we add a new guess to the guesses array. This is because we dismissed the second option, which is within the share button’s on-click function. We didn’t have access to the guesses via the progress modal component, and passing them just for this feature was too much hustle for the same result.
Well, guess what? We have access to the guesses array from wherever we want, thanks to the localStorage.wordlestruckLastGamePlayed
object! Let’s modify the code to create the share-string in the progress modal component.
A few things to note:
- The guesses array in the
useWordle.js
hook is initialized to be of size 6 (because there are max 6 guesses), and the guesses are added every turn. This means we can’t simply iterate over the guesses array (guesses.forEach((formattedGuess) => { ...
), we need to ensure we iterate only over the populated cells (if(formattedGuess) { ...
). - Now we can initialize
resultString
directly with the prefix, instead of adding it after the squaresString creation. - At this point, you can remove any code related to the previous version of
squaresString
we added inuseWordle.js
,Wordle.js
andProgressModal.js
.
A well-deserved meme break:
The Mid-Game Welcome Page
If the user loads the page after entering guesses but before finishing the game, they will see the mid-game welcome page.
This page has one button — Continue. When the user clicks it, they see the main page with the guesses they tried so far and the keys they used. To achieve that, we can’t save the game data only when the game ends — we need to save the relevant information after every round ( = after every guess the user entered)! We’ve seen usage of local storage before, but this one is different!
You can either trust me or try yourselves (I encourage both approaches 😉 ), but updating local storage directly won’t work. This is because the Wordle.js
component renders a lot more times than you might think —in fact, we trigger page rendering at every key press (due to the handlekey
listener). For that reason, if we were to try to continue the game, the typing would cause the data of previous guesses to initialize to an empty array.
The right approach is to add to useWordle.js
hook a new state variable: storageData
. We will set this variable after every guess entered, and then in Wordle.js
we will update local storage with it. That way, in those handlekey
renders, the data we set in the useWordle.js
hook stays as it was. It only updates on the submission of valid guesses, when the code reaches return {turn, currentGuess, guesses, ...}
.
Let’s start coding!
We will begin by initializing variables for each data needed in the hook: initialX
, when X
is game number, turn, etc. The value will be pulled from gameWasPlayedToday
or restarted, if there isn’t any data saved (lines 3–8 below). Then, we will create the new variable storageData
and initialize it with those variables (lines 10–17).
Please note that to continue the game at any point, we need to update our localStorage.wordlestruckLastGamePlayed
object to contain two more properties: history
and isCorrect
. This is because we need those for the useWordle hook functionality.
A quick reminder: The difference between history
and guesses
is that history
is a simple array of strings, while guesses
is an array of formatted guesses, where each character in each guess is colored.
All that is left to do is add the setter for storage data to addNewGuess()
function. We will set the properties just like we set their original counterparts (lines 11–49 below).
Lastly, we will return storageData
to the Wordle component (line 52).
Are you seeing what I am seeing?
This code is a bit painful to read because setStorageData
contains all the previous sets’ code: setIsCorrect, setGuesses, setHistory, setTurn
and SetUsedKeys
.
Code duplication is bad practice because now you have multiple places to update code and you might not remember all of them. How about modifying useWordle.js
to have only setStorageData
? We will do that later, once storageData
is up and running, and the application is using it exclusively. We still need to use the data we collected!
In Wordle.js
, just above the end of the useEffect
scope, we update the local storage with storageData
:
localStorage.wordlestruckLastGamePlayed = JSON.stringify(storageData);
Lastly, we need to put the correct boolean to show the mid-game landing page.
A quick reminder:
If the game hasn’t started yet — we want to show the “welcome” landing page. If the game started but is still in progress, we want to show the “welcome back” page, and if the user finished today’s game, we will show the “good job!” page:
We already set gameWasPlayedToday
variable when we worked on the post-game landing page:
gameWasPlayedToday=
lastGame && lastGame.gameNo === gameNo ? true : false;
This now needs to change. We modified the code to save state after every turn, therefore the game index in lastGame
indicates this game has started, not necessarily that it’s done.
If gameWasStartedToday
is the new gameWasPlayedToday
, how do we gather the value for gameWasPlayedToday
? No need to save additional variables in local storage, we can just calculate it from the existing stored data:
We know the game started today if the last game’s index is today’s game index. To check if the game was done — we use the same condition as in the Wordle.js
component — if isCorrect
is true or if the user played their 6th guess.
In case you wondered, I am saving the turn value in a local variable because I need it for the work-in-progress landing page — we want to show the user how many tries they already made.
Back to the Progress Modal!
In the previous blog post, we created the progress modal: It called the statistics component, which shows the user data of games played and % of wins, and below that their guess distribution. This is where we stopped because we needed today’s context to take the modal to the finish line!
Bug Fix — Update Statistics Only Once
With today’s changes, we have unlimited access to the Wordle component, and more specifically – access to the end-of-game condition: isCorrect===true || turn>5
:
We wrote this code in the previous blog post, go check it if you need a reminder!
This code has become problematic now because every time we go from the “you-already-played-today” landing page to the grid and keyboard page, the if statement (line 5 above) is correct, and we update the statistics (again) on lines 14–15.
Therefore, we need to make sure we update the statistics data only once per game. We can do that by wrapping it in another if
condition. That, together with the existing if condition (“if the game is finished”), creates a sweet spot that happens only once in the daily game’s life cycle: The state where local storage’s isCorrect
or turn>5
is changing value for the first time!
I created two variables: player_just_won
and player_just_lost
. Let me remind you we are updating the local storage after every turn. Now, I know the player has just won if isCorrect
is true but JSON.parse(localStorage.wordlestruckLastGamePlayed).isCorrect
is still false. Same for when the player just lost the game: turn>5
but also JSON.parse(localStorage.wordlestruckLastGamePlayed).turn===5
.
It’s also very important to move out the initializing code (lines 7–10 above) to be before the useEffect
hook. If you don’t move it, you will encounter a bug later, when you add the statistics button to the header. The bug occurs when the user enters the game for the first time and clicks the statistics button: It will be undefined! Logically, we want localStorage.statistics
to be defined when we reach the progress modal. So we’ll define it first thing in the Wordle component.
Our code will now look something like this:
Feature — Adding Streaks Data
To give the progress modal the final touch, we can collect the data for the current strike and the maximum strike.
We’ll start by adding those values to our statistics object, along with currentStreakLastIndex
— this is needed to know if the strike continues or not. This means the array of statistics will sit deeper in the object— localStorage.wordlestruckStatistics.stats
. We will initialize all the new properties with zero.
Updating the values can be tricky: It’s very important to update them in the correct order! Let’s start with the easier one: maxStreaks
is updated only if currentStreak
is bigger. we need to check currentStreak
‘s value only after we update it today.
currentStreak
will be increase only if today’s game continues the streak, which means if today’s game index is currentStreakLastIndex
+ 1. Otherwise, it will initialize to be 1. This means we should check currentStreakLastIndex
‘s value before we update it today.
Lastly, currentStreakLastIndex
‘s value should be updated to be today’s game index. Remember that we reach this code only when the game is over, so this game is the current streak’s last index.
Let’s see it in the code:
Important note! The original game is counting streaks of winning, not streaks of playing. I decided to count the streak of playing the game, regardless if the user managed to guess the correct answer or not. If you wish to count streaks for winning, make sure to check if isCorrect
is true before updating the new three variables (currentStreak
, maxStreak
and currentStreakLastIndex
).
If you already published your website and sent it to your friends (I’ve mentioned Netlify as a good platform to host your website for free, in the previous blog post), then you should update the name wordlestruckStatistics
to something else. Otherwise, you are referencing in your code the developed version of the object (wordlestruckStatistics.stats
for example), but your users have the previous version saved in their local storage, so running wordlestruckStatistics.stats
will break the application. When you rename that variable, the code will enter the initializing block (if(!wordlestruckStatistics){…}
), and you will save yourself a bug.
Bonus Feature — Shaky Rows!
There is one feature from the original game that I’ve decided to not recreate — the “not a real word” feature. When that happens, the row shakes, and a black tooltip appears, notifying you that this word doesn’t exist.
I am still wondering “How did they create a data set of all possible 5-letter words?”. Once you have this data set, it’s pretty easy to notify the user when they have invented a word.
In my game, as you will see in the next blog post, I have a small data set — less than 200. I can check if a guess is within this array, but I don’t think it fits that game, so I decided to not implement it.
With that said, I did want to shake my rows! This is the first turn I took from the original game — I am shaking the current guess row when the user is trying to enter a guess they already made today! In the original game, they let you waste a guess on a duplicate word — so watch out for that!
Well, how do we shake the row?
For this, we will need CSS animation. This is not the first time we have used animations in the project. We bounce tiles when the user types in characters, and flip tiles to reveal the characters’ colors! Let’s start by creating the shaking animation in the CSS:
All animations have the same structure: @keyframes <NAME>
and division to the percentage of progress. In our case, we want back-and-forth of left and right (lines 2–11 below). Then, we create a new class that will use it (lines 13–15):
Now we need to give the appropriate row div this class name. Which is the appropriate row? The shaking will be of the row that is currently in typing. So, we’ll modify Row.js
:
In the code above, we pass a property of shaky to the row component (line 1). The current row will shake (line 8 above) only if the shaky
bool is set to true
(line 2). Where do we set that value? At the same place we “find out” the user already tried this word — in useWordle hook!
The fun part is that this code was already in the base code, so no extra work was needed from us, just to add the setShaky()
function in the right places:
We set shaky
to true
in the block where we check if the history includes this word (line 8). We also need to set shaky
back to false
so it will stop shaking after the first character deletion (line 15).
Lastly, as line number 2 implies, we need to add the shaky variable to Wordle.js
:
const [shaky, setShaky] = useState(false)
Note that this time we pass the setting function, not the variable:
const useWordle = (…, setShaky)
Look at it shake! Lastly, we need to add a tooltip: We should notify the user why the guess didn’t enter. We already have a tooltip in the code so this will be easy for you to implement by yourselves.
With this done, we successfully finished our list of features!
Final Touches
Now that we have a cool game, there are a few steps to make it even cooler!
Refactoring the code to use only storageData:
Remember earlier, when we noticed a disgusting amount of duplicate code in useWordle.js
? We will fix it now! Start cautiously by going to Wordle.js
and adding variables instead of returning them from the useWordle hook and see if anything breaks.
Once that looks ok, go to useWordle.js
and remove those variables and their setters. You will need to use
storageData.<var>
instead of those variables in some places, but overall this is a simple change.
Phone screen capability:
Make sure the website view is neat from the phone as well. Check both Android and iPhone, because they don’t present the same way. A useful trick I learned in this project is to set font size or div width with this structure: min(9vw,40px);
. Pixels are fixed, but vw
is viewport width
, which works like a percentage. Keeping the pixel option is good because vw
gets too big on the laptop view, and this sizing structure keeps it proportional.
Fonts:
The whole goal of this project is to wink at the original Wordle. What better way to do so, than gather similar fonts as well? My search showed the original Wordle uses a special New York Times
font (that I assume is out of reach), so I used Abhaya Libre and AbrilFatface, and I am pleased with the result 🙂
More Refactoring:
At first glance, yes — why bother if the user doesn’t see the code? But, refactoring has two main advantages: First, optimizing the code and using better practices, and names. This will minimize things that could cause you to make oopsies. Second, it will sharpen your React skills. You’ll learn additional (better) ways to produce the same output.
The Only Feature I Couldn’t Figure Out:
In the original game, when the user already played today and clicks the See stats
button from the landing page, there is a small delay before showing the modal. This delay exposes the tiles beautifully flipping with the previous guesses. The code, as it is now, has flipping tiles animation, but I didn’t manage to delay the modal.
If you decide to work on this feature, please remember: The modal should be delayed only in two situations: When the user is winning, which is when we have the line setTimeout(() => setShowProgressModal(true), 2000)
, or when the user arrives from the landing page. There should be no delay when the user clicks the header button!
Wow, what a journey! In this blog post, we worked on the share button, set different views based on the user’s game state, and shook some rows!
In the next blog post, we will transfer our game from a boring 5-letter word to anything you want! Mine is Taylor Swift songs 😀
Exciting news to all the swifties out there — my Wordlestruck game will launch at the end of this blog post series!
See you soon!