Next lesson playing in 5 seconds

Cancel
  • Overview
  • Transcript

5.6 Create a Study Interface

This last feature is the most important one: a UI for studying cards. In this lesson, we’ll create the StudyModal, which will allow our users to study their cards. We’ll also implement a simple spaced repetition algorithm that will let our app chose which cards to study.

Related Links

5.6 Create a Study Interface

The last feature that we want to create in our application is actually the main feature, studying our decks of flash cards. Now, we're going to use a very, very simple version of a learning technique called spaced repetition. Many of the different flash card applications out there actually use spaced repetition, so we're gonna be in good company. Now, you can go read up on this learning technique yourself. But basically, to put it as simply as possible, the idea of spaced repetition is that the better you know a flash card the longer you can wait until studying it again. So if I do really well on a certain flash card today, I don't have to study it tomorrow. I can study it maybe in two or three days, and this way you can have a deck that has 1000s of flash cards but you don't have to study them all every day, right? Because you know a lot of them very well and so on a given day, you may only have to study a handful of cards which will only take you a handful of minutes. So we're going to make a very, very basic algorithm that kind of does this. So let's start by adding our route. Wherein our app.js file and I'm going to duplicate our previous route one more time and this one is going to be deck/:deckId/study. So this will allow us to study an individual deck. The component will be called the StudyModal. And so let's go ahead and import StudyModal from components/StudyModal. Now this will not use the CardModal presentational component that what we've already created. We're gonna have to create this from scratch but that's okay. So let's go ahead and create a new file src/components/ StudyModal.js. And why don't we just dive right into this we'll import some stuff as we go along, we'll figure out what we need. But let's just go ahead and start writing our StudyModal. Now this is gonna be a function, we know it will take some properties, we don't know what properties we're going to need just yet. So we'll go ahead and leave that blank for now. Now, the one thing we do know about the StudyModal is that we're going to need to display cards. But there's a fact about the StudyModal that you may not realize, and that is that we don't need to give the StudyModal a deck of cards to study. Because remember, we're interested in what the user is going to be seeing at a given time, right? The current state of the application. And the current state to them may be I'm studying a deck of cards. But the current state to us from the other side, if you will, is they're currently studying a single card. Right? They're not studying the whole deck at once. The U.I. is only ever going to show them one card to study at a time. So we don't have to build a modal that can manage multiple cards and go in between them. Really, we only need to manage a modal that can display a single card at a time so that makes our job much, much easier here. Now, the other possibility that we have to think about here is that they click that study button but they don't actually have any cards to study. So let's make that the default case first. So let's create a body variable here and this is just going to be sum jsx. Let's create a div. And this is just going to be our default situation. They have no cards to study. So I'm going to give this div a ClassName of 'no-cards' and inside this let's just put a paragraph and will say, You have no cards to study in this deck right now. Good job! So that's their message, if they have no cards to study. However, it's possible that they will have cards to study. In particular that they will have a card to study. So let's say, if we have a card to study, we'll change whatever the value of body is. Now, where do we get this card? Of course, it comes in as a property to our props object here and let's go ahead and destructure it out so we have a card. All right, so what is our body going to be like if we do have a card to study? Well let's create a new div here and I'm going to give this div a className of 'study-card'. And now within this div, we wanna have both the front and the back of the card but, of course, only one of those will be displaying at a given time. So let's create a div that will be the front and then we'll create another div here and this will be the back. So let's give our front div a className and in here we're gonna have a ternary expression, because we're not going to be showing the front div and the back div at the same time, right? So we're going to need some kind of state property to tell us whether we're supposed to be showing the front or the back. So we're going to call this property showBack. And this is going to be as part of our state. So we're gonna get it here as one of our properties. So let's go ahead and pull in showBack as well. Okay, so if showBack is true, then we don't want to show the front. So we'll give front a class of front and also a class of hide. If it is false, then we'll just give it the class of front. And we can do almost the exact same thing except reverse here for the back. We'll set the className, and if the className is showBack, then we will just give it the back class. Otherwise, we'll give the back class and the hide class. All right, so what do we do if we are displaying the front of the card? Well, let's create another div inside of this. And we'll have a paragraph in here and this will just have card.front displaying like that. And then underneath that div, let's add a button and onClick, we're going to have the onFlip function call and this will just have the text Flip. So this is the button that will actually flip the card to show us the back instead of the front. So onFlip is a function, of course, but it's going to have to dispatch a flip event. So that means we get it as one of our properties as well so we'll add onFlip to our props object destructuring. Okay, what about when we're showing the back? Well, let's start with something very similar to what we have in the front. In fact, I'm actually just gonna copy lines 11, 12 and 13 and paste them down here. Of course, the only difference is that we're going to be displaying the back of the card. And then after that let's add a paragraph here and this paragraph is going to have some text. We'll say, How did you do? And this is where the user gets to choose how well they did on the current card. Let's add another paragraph here and we're going to add a button. And this button will have an onClick function here. And we'll write this in just a second but it's gonna be an inline function. We'll take the event and we're gonna call something else. We'll come back to that in just a second. But the text for the first one is going to be Poorly, they didn't do very well at all. Let's duplicate this for two more buttons. The second one is just gonna be the text Okay and the third one will be Great. All right, so now what are we gonna do inside these onClicks? Well, in all the cases we're going to call the onStudy function. Which is a function that we call when the card is finished studying. And this function actually takes two parameters. The first one is the card.id. This of course is going to let the state know which card was being studied. The second parameter is going to be a number between 1 and 3, and this number between 1 and 3 is the score for our card. If the score of the card is 1, that means they did poorly and we want to restudy this card within one day. If the score of the card is 2, they did okay and we want to restudy this card within two days. Finally, if the score of the card is 3, then they did a great job and we will study the card in three days. So the score is going to be the second parameter to this onStudy function. Now, if they did okay, we'll just stay at the current score level. So we can just return card.score. Now if they did poorly, we want to return one less than their current card level. So we'll say card.score- 1. However, it's possible that card.score is already at 1, which is the lowest value. And so what we want to do is wrap this in a call to Math.max. And will return whichever one is higher, card.score- 1 or the number 1. So if card score is less than 1, then one will be returned. All right, now we'll kinda do the same thing for Great because we want to return card.score + 1, if they did a great job. However, if that is greater than 3, we just want 3 to be the highest possible value. So we can say math.min and will return whichever one is lower between card.score + 1 and 3. Now remember, we haven't actually returned any of this, we've just set this to be the body of our StudyModal. So now after our IF block here why don't we go ahead and actually return something. So let's go ahead and created div here, and we'll give it a className of modal and also study-modal. In here, of course, we will put in the body but above that let's put in a link. We'll give this link a className of btn and also of close, and we'll give it the text of simple X there. And when this is clicked the path is going to take them to /deck/ and let's interpolate deckId. So we need to get deckId from our parameters up here. So let's go back up to the top here and let's add deckId here. We also need onStudied which we didn't add yet. So let's add onStudied to this list. Now, we do have some importing to do which we haven't done yet. We already saw that we need to import { Link } from 'react-router and of course, because we have JSX in here we need to import React from react. Now, of course, at this point StudyModal is just a presentational component. So that means to turn this into a container component, we need to write our mapping functions. So let's start with mapStateToProps. We're going to need a bunch of things here. We're going to need the array of cards of course. We're also going to need to know whether we're showing the back or not. Now we haven't added a showBack reducer yet so we'll do that soon. But for now, let's just assume we have a property that's called showBack. Now from the router, we're going to need the deckId, so let's get the params.deckId, excellent, okay. So showBack and deckId can go through just as they are. We can see here in StudyModal, we need showBack and deckId. Now onFlip and onStudied, are methods, of course, so they'll go to the dispatcher. However, card is the other state property that we're going to need. So how are we going to get this? Well as you might guess we start with cards.filter and what we need to do is find one card that needs to be studied. So the way we can do this really is just find all the cards that need to be studied and then we can just get the first one from this list. The beauty of this is that, when they click study, we'll go ahead and just show the first card from the list of cards that needs to be studied. But then after they study that card it will be marked via onStudied, and so it no longer matches the criteria of cards that need to be studied. But because the state has changed, the StudyModal will be rendered and our filter here will be rerun which means we will find a new list of cards that need to be studied and the one we just studied will not be in that list. So we can just keep doing this process until we have no cards left the need to be studied in which case we'll show them the message you have no cards that need to be studied in this deck right now. And I think this is a very elegant solution to this problem. So let's go ahead and start filtering. We'll get a card, and of course the first thing we want to do is make sure this card is in the current deck. So we'll say, card.deckId should be equal to deckId. And then we need to put the rest of this in parentheses. That's important. So we get our boolean precedence, right? We'll say and and I'm going to write this onto a new line. And you may have realized that for our scoring system to work, we need a little bit more than a score. We also need to know when this card was last studied. And so cards will have a last studied on property. But of course they don't get that by default. So we want to study this card if it does not have a card.lastStudiedOn property or if it does have a lastStudiedOn property. Let's say, new Date- card.lastStudiedOn. Now because we're doing subtraction new date will convert to Unix epoch time. LastStudiedOn will be a timestamp in the same way, and so what we get here is the number of milliseconds between now and when this card was last studied. Now let's jump up to the top here just for a second and let's create a constant called MS_IN_DAY. And I happen to know that this number is 86400000, okay, so we have milliseconds in day. And now let's take the number of milliseconds between these two values and let's divide it by milliseconds in a day and that value there should be greater than or equal to card.score. If you think about this for a second it should make perfect sense. We have whenever our card was last studied and then we have today, and we're going to subtract those two values and then divide them by the number of milliseconds to get the number of days, since this card was last studied. So let's say it's been two days since this cards last studied, we want to study this card if that number is greater than or equal to card.score. So if card.score equals 1, that means it should be studied one day after it was last studied. Now, we're over one day since it was last studied. If we're at two days, and so, yes, we want to study that card. If we were at only one day, we would still want to study that card. However, if card.score was 3, meaning, we don't need to study this card for another three days. Then if the current number of days since it's last been studied was two, then 2 is not greater than or equal to 3 and so we will not study that card because it does not need to be studied today. So this is our very simple algorithm for how often we study a card. All right so that's the end of map state to props. Next let's do a map dispatched of props. Now, we're going to need some actions here so I'm going to go ahead and import those. We're going to import the update card action which is, of course, how we set a card is studied. We already know how to update a card's front and back but update card is not specific to those properties so we can use this to update the score in the LastStudiedOn property as well. We also are gonna need to create an action for deciding whether we're showing the back of the card or not. So let's just call that set showBack, and we're getting these from actions. Now, we have two methods that we need here. First one is onStudied, and the second one is onFlip. OnFlip will be easier so let's do that first. This will be a function, and we will just dispatch the setShowBack of it, and will pass true. This will take either true or nothing, or I guess, we could do false as the parameters, when we setShowBack to true. And that means, we do wanna show the back. All right. So now, what about onStudied? If we look at onStudied, where we're calling it or passing the card.id and some score. Okay, so let's expect that we've got cardId and a score and what we need to do is create a LastStudiedOn property. So what I'm going to do is let's get now which is going to be a new Date and then I want to do now.setHours. And we're going to set the hours the minutes the seconds in the milliseconds all to 0. So basically, what this means is if we're setting the hours and everything smaller to 0, then for this current date object it will refer to today at midnight. So the earliest possible point of today. The reason we're doing this is let's say, it's in the evening and I'm studying a card maybe it's 9 o'clock at night, if we did not set the hours like this and just use this new date as the LastStudiedOn date for this card, then we wouldn't be able to study that card again until after this time, say 9 o'clock tomorrow night, if our score was 1 or 9 o'clock in two days, if our score was 2, etc. However, by setting our hours to 0, this means we're not worried about the exact time during the day. If we wanna study the card any time tomorrow if our score's 1, then that will be just fine. So at lunch time tomorrow, let's say I have a break, I'm studying my cards, we will be able to study this card because we're setting the hours to 0. Okay, so now we want to go ahead and actually do our dispatch here and we're going to dispatch an update card event and we know that to update card we need to pass an object, which will be the update. Now it needs an id property with the cardId that we've got. We also need the score. I'll use the shorthand syntax there. And then we need LastStudiedOn and that will be and I'll just say, +now. And, of course, the unerary plus operator converts the data object to the Unix timestamp. And of course after we do that, let's dispatch another event and this event is going to be setShowBack and that will take no perimeters. And what this means is we're setting it back to showing the front. So setShowBack will setShowBack equal to false, and so when we study the card we're now showing the front of the next card. So now we have mapStateToProps and mnapDispatchToProps written. Let's come down to the bottom of our file here and let's do export default and we will call connect and this reminds me did we bring in connect. We didn't. Okay so let's import connect from react-redux. And now let's make our call connect(mapStateToProps and then mapDispatchToProps and we will pass the return function StudyModal and now we have our completed StudyModal. What we don't have is a setShowBack action or a reducer that manages that. So let's come to our actions here and let's create a new function called setShowBack and we'll take the value for back as our parameter and the type for this can be SHOW_BACK and the data will be whatever we pass through as that value for back. Okay, let's add our reducer here. So in the reducers file. Let's say export const showBack and of course as you would expect we take our state and our action. Let's create our switch statement here we have action.type and then we are listening for what do we call that action SHOW_BACK. Okay, so we have the SHOW_BACK action. So if we have SHOW_BACK, we'll just return action.data, or if there is no action.data, we will set this to false. So that way, if back is set to true it will be true. Otherwise of back is not true will get false, as the value the default case will be almost the very same thing. We'll return whatever the current state is or false. Now, if we come back to our application and let's click the study deck button. You can see that we have our first card showing up and so we can go ahead and study the front of this card, I'll click flip and look at that we can see the back of the card. We've got the back of the card showing up there. We've got how did you do. Poorly, Okay, or Great. Well, let's go ahead and say Poorly, for example. And right away we get the next card. This is excellent. So I can go ahead and study this card. I can flip it. I can say great and I could decide I'm done for now. And I go ahead and just close this and noticed that the road has taken us back to the deck route because we clicked the Close button. And it If I decide to go back and study the deck, we're right back where we left off. We don't get the first two items here because they've already been studied, and we're going to have to wait a few days before we can study those again. The other thing is that because study deck here has its own URL, I can go ahead and refresh the page directly at this URL and we can be right into study mode right away. So there we have it. We can actually study our deck now. We've pretty much built a complete application now. We've done almost everything we set out to do. The only thing is our data is being stored in local Storage. In a real application you would of course have to create a user account and sign in and your data would be stored on the server. And how would we manage that sending and receiving data from the server with our reaction redux app. Well we're going to look at that in the next lesson.

Back to the top