So there are two things we have to add. The first one is audio feedback to our correct or wrong answers and the second one is to resolve the redirect we are getting once we select the correct last answer because we want to see a finished screen. Let's go ahead and go inside of our terminal here and let's run npm run database seed. This will reset the database so that we can try out our challenges again. So let me just refresh this.
There we go. I can now select my course and I can go into the first lesson. So I want to get feedback if I select this option, for example. So here's where I'm going to gather my files from. So go inside of my public folder, you can find the link in the description for my GitHub repository, or you can use any audio files you want.
So I used freesound.org to find CC0 licensed audio files, which means you can use this without copyright and without attribution if you are wondering. So you need to find the correct audio file. So let's go ahead and find this one. So I'm going to download that. Then let's go ahead and let's find incorrect.
Select that one and download it as well. And you also need to find finish.mp3. So those three audio files are needed for our quiz. And then what you would do is you would drag and drop finish, incorrect and correct inside of your public folder right here. So let's go ahead and use them to play the sounds.
So we already know how to do that because we did it in our card component in the lesson. We used the useAudio from React use to load the audio file. So let's go ahead and let's load that now. I'm gonna go back inside of my quiz component here and let's go ahead and do the following. Let's go ahead and destructure an array from useAudio and let's add the source to be slash correct.vav.
Let's import useAudio from react-use. So let me move that right here. And now that we have added that, let me just find it. Let's go ahead and destructure our correct audio. So that's gonna be the HTML thing.
Then we are going to not use the second argument. So I'm gonna call that underscore C for correct. And Then I'm gonna have correct controls, which I will use to trigger play or stop. Then I will copy and paste this. I'm going to rename the correct audio and correct controls to incorrect audio and incorrect controls.
I will rename the underscore C to underscore I, representing that we are skipping over the second parameter in this destructured array for the incorrect controls and name the source file incorrect. So just make sure that the correct and incorrect match exactly what you have in your public folder. There we go. Correct. And in here I have incorrect with the same extension.
And now I need to find a place where I'm going to use this correct audio and incorrect audio. So go ahead and find the return method. And in here very simply just go ahead and render the incorrect audio and correct audio like that. So you need to render them somewhere and now that you've rendered them you can use them. You can use the correct controls and incorrect controls.
So find the onContinue method here and this is the first if clause if we selected a correct option. So in here where you set the status to correct after you've confirmed that there are no hard errors, go ahead and do correct.correctControls.play. And do the same thing in the else clause. After the if one, where you set the status to wrong, you're going to use incorrect controls.play. So now let's try this out.
I'm going to refresh this and let me select an incorrect option here. I will click on check and there we go. If you heard it in the background of my microphone, there was an incorrect sound. Now let's go ahead and click retry and let me click again for the success one. There we go.
So Both of these are working for me. So what we have to build the next is the end screen. Right now, if we try it out, so if I go ahead and I select all the answers I need and I go all the way to the last one, what happens is I get immediately redirected. So the reason I get redirected is because we revalidate the learn page. No, we revalidate the lesson page, right?
So in here with my actions challenge progress we revalidate the learn page. So what does that mean? Well, let's go inside of our learn page. So inside of app... My apologies, I'm not talking about...
This is not problematic. The lesson page is problematic. So when we revalidate the lesson and we just finished the last challenge what happens? Inside of the lesson here, inside of our page we do something called user progress and get lesson get lesson is the problematic one so this gets revalidated meaning these two methods are called again. So then we go inside of this getLesson and what we call is getCourseProgress.
And inside of this getCourseProgress we attempt to fetch the lesson that the user is currently on. Well what happens is that the user after they finish the last challenge is no longer in that lesson that they are currently seeing. So the revalidated output of that new lesson page currently holds a completely new lesson because of this query. So we find the first uncompleted lesson. And the lesson that the user just completed the last challenge in is no longer the first uncompleted lesson.
So our getLearn() method here, which uses the getCourseProgress() finds the next uncompleted lesson. And that is technically fine because in our page here, we are very careful. So we only pass the initial data. So our user interface doesn't change. The user still sees the lesson they last laughed on.
But here's where we arrive to a problem. Inside of our seed script right here, if you take a look at it, inside of scripts.seed.ts, We have all of these lessons, right? But the only lesson which is actually production-ready, let's call it like that, is the first lesson. The second lesson right here has an ID of 2. And if I just close this for a second and if I look at my challenges there is not a single challenge which has the lesson ID of two.
That subsequently means that there are also no challenge options which relate to the lesson of two. So then when we go inside of our queries here what happens is that my getCourseProgress method cannot find the first uncompleted lesson. So both activeLesson and activeLessonId are undefined or null and then inside of this get lesson method this becomes undefined, right? Actually it becomes null and we also didn't pass any ID in here. So that is undefined as well.
So lesson ID is undefined. And then this returns null in the revalidation of our lesson path. And then this entire page gets revalidated, we hit this page right here and we redirect to slash learn. So that's why that happens. So now we have to find a way to resolve that.
So technically this is only an issue in development because in production you would never have this kind of seed script where you have a lesson... Where are our lessons? Here they are... Where you have a lesson which doesn't have its subsequent challenges. Right?
Later when we create an admin dashboard that's not gonna happen because we're gonna have a flow of how we create things. But our seed script right now kind of created these lessons to be purely... What's the name for it? Presentational. That's why we created all of these additional lessons, but none of them actually have any challenges.
So that's what's going on inside of here. So what we can do right now is we can try and do the following. So let's what happens if I go ahead and not do this. So now we have a lot of issues like this. So how about we create How about we just handle that, right?
What if I do lesson.challenges? So this is gonna be kind of an ugly solution here So I'm going to wrap lessons.challenges and add an empty array here that resolved that and then I'll do the exact same thing here So I'm going to make sure that these less than dot challenges are always unarrayed. And then in here I will add this and this. And that's one way of doing it. But then we also have to change the types.
So let's not do it this way. Let me bring this back. I'm not gonna do that instead. So I'm kind of experimenting now because this is a very specific problem that we have here for development. So maybe the easiest thing to do right now would be to go back inside of our seed script and simply add the challenges for the second lesson right here.
So how about we do that? So we don't have to delete anything in the lessons. We just have to add just one more challenge inside of our new lesson. So this is what we can do. Let's just copy this exactly as it is.
Keep the challenge options as they are, right? So all of these challenge options are for each of the three challenges inside of our challenge script. So let's go all the way to the end, here where we have the console log sitting finished. And in here, let's create some new challenges. So I know that there are three challenges, which means that this ID is going to be 4, then this ID is going to be 5, and this ID is going to be 6.
So then I can change this lesson ID for each of these to not be 1, but to be 2 instead. And I think this should already be enough. So just the fact that we have some new challenges being inserted for the lesson ID 2, which is no longer nouns but verbs now. If you revisit your lessons in here, there we go, you can see that the title 2 is verbs. And the unit can stay the same right that doesn't matter.
So let's go ahead and quickly try that out. So I think that should be enough because if I look at my queries in the get course progress here, yes all we care about is the challenges. So as long as we have the challenges and they don't have the challenge progress, it should be able to find the first uncompleted lesson. So let's go ahead and try it out here. So if I go ahead and run npm run database seed again.
Let's see if that is going to work. There we go. So that seems to be fine. If I refresh now, I'm going to choose my language again. So Spanish.
And let me go ahead and just finish this entire course here. So I will select the correct meaning and let me go ahead and wrap this up. And there we go. Now, when I reach the last one, what happened is that right now my new active lesson has completely changed. I have finished this lesson.
My new active lesson is the second one which we just modified the seed script for. But visually the user is still seeing this one and what will happen once I click on next is an error. Why does an error happen? Well, this is why. So if you go inside of your quiz component, so inside of app quiz.tsx right here, In here we have an onNext method which will simply update the current index.
So technically when it reaches the last one it will update the index to a number higher than the length of our challenges. So challenge will be active index something like 4, right? But we only have 3 challenges. So that means that the challenge becomes undefined. So here's what we have to handle.
Also, if you click save here, you're going to get redirected to this weird page. That's because this actually revalidated, hot reload, revalidated our app and loaded the next challenge, but completely ignore that for now. So this is just development stuff. All right, this is what we have to do now. Above this const title here you have to go ahead and run the following.
You have to run if there is no challenge. If there is no challenge, you will return a div here, which will very simply say, hello, actually finished the challenge. There we go. So let's go ahead and try this again. If I go inside of my terminal and if I run npm run database seed again now, let's quickly see if that is going to work.
So I'm going to go ahead, go back inside of my localhost 3000, continue learning here, Spanish, Start and let me go ahead and try this again. So I'm going to completely finish this challenge. El Robot and let's click next. And there we go. No longer do we have an error.
Instead, we have a text which says, finished the challenge. Perfect. So that is what we are going to be working on now we are going to style this div so it actually displays that we have finished a challenge so in case you cannot see this just do npm run database seed again and make sure that you don't do any subsequent refreshes and you should be able to see this page right here. So this lesson page, this finished challenge page should only display, it should only display temporarily. Once the user refreshes they're no longer going to be seeing the finished page.
Instead it's going to redirect them and load the next challenge. Or if there is no next challenge or lesson it will simply redirect to the learn page so that is proper behavior from our app. It's just a bit hard to do this in development mode with such incorrect data which we have to seed like this. So I hope that isn't too confusing, right? So let's go ahead and prepare an image we need here.
So I'm going to open my public folder and we need an image from my public folder here. Let me go ahead and click on public. We need finish.svg. So let's go ahead and download that. So just drag and drop finish.svg inside of here.
And then let's go ahead and go back inside of our WIS component here. And we're going to go ahead and replace this with a fragment instead and then in here we're going to open a div and let's give it a class name of flex flex-hole-gap-y-of-4-lg-gap-y-of-8 And I think if you do any kind of save, oh, it seems to still be working here. Okay, but this is what I want to do. So to make this easier for us to develop, feel free to refresh and this will load the next uncompleted lesson challenge thing we just did and hacked in our seed script. So do this.
Go ahead and run again npm run database seed. This will reset the entire thing so you have no course progress, no user progress, no challenge progress. And then we're going to hard code this to be true. So true or not challenge, and I'm gonna add to do remove true. So now go ahead and choose your Spanish course again and simply go into the first challenge and what you should see is a completely blank screen.
So now you can see exactly what you're developing because none of this, not the header, not the challenge, nothing will load. So we can just focus on developing this. There we go. I think that's a way we can handle this. So all right, we've added on large gap Y is So let's add a max width of LG, max width, sorry, mx-auto, text center, items center, justify, center and full height.
Now inside of here I'm going to add my image component so make sure you've added an image from next slash image and let's go ahead and use this So I'm going to give it a source of slash finish.svg an alt of finish and a class name of hidden-lg-block so only on mobile height for this one is going to be 100 and width is going to be 100 as well So it should not be visible now if you're on a small device. If you expand, it should be visible, but I'm on mobile here. So now I'm going to copy and paste this image. I will keep everything the same but I will reverse these two actions. So hidden on large and visible on mobile and I will reduce the width for this in half.
There we go. So now I have this little confetti here. Perfect. So let's go ahead below that and add an h1 element which will say great job. Let's add a break here and let's write you.
You've completed the lesson. And let's go ahead and resolve this by using an app as a sign there we go now let's style this h1 element by giving it text extra-large on LG text 3xl font bold and text neutral 700 like that And now outside of here we are going to open a div, create a class name of flex items center, gap x4, and pull width. And then inside of here what we have to do is we have to create a component called resultCard so let's go ahead and write a resultCard here like that if you save you're going to get an error because result card doesn't exist. But let's give it a variant of points and let's give it a value of challenges.length times 10. Right, So we are going to know how many points the user earned in this lesson by counting the amount of challenges that the user completed, which if they reach the finish screen is going to be the total length of the challenges.
And we're going to multiply them by 10 because 10 is how many points we get for each challenge. We know that inside of our challenge progress here, we increment the points by 10. So yeah, it would be a good idea to add this into a constant, something like points per challenge and then use that instead of a magic number. That would be a tip I would give you if you went into production with this. So now we have to create the result card.
So let's go inside of the lesson, result-card.vsx And let's go ahead and create a type props here to accept a value of number and a variant, which can be either points or hearts. Let's export const result card here. And let's go and assign the props. We can then destructure the value and the variant. And let's return in here a div and render our variant here.
Let's go back inside of the quiz component and you can import result card from .slash result card the same way we did with Header, Footer, Challenge and the Question bubble. And now in here you should see a label which very simply just says Points. So let's go ahead and copy and paste this below. And now what we need to add here is the result card for hearts. And the hearts are very simply going to be hearts which the user currently has, which we hold and synchronize with the backend using state.
So we know that these hearts are going to be up to date. There we go. So we now have points and hearts. Let's go inside of the result card and let's properly style this. So I'm going to prepare some imports, which I know we will need, which is an image import from next image and we are also going to need cn from libutls that is it.
Let's give this div a class name of dynamic cn. The default classes are going to be rounded to Excel, border 2 and full width. And the dynamic ones, say the variant is points, in that case, we're going to use BG orange 400 and border orange 400. If the variant is hearts in that case BG is going to be rose 500 and border is going to be rose 500 as well. So we have that resolved in our variants here.
Then in here we can remove now this and instead we can open up a new div with a class name. Again, dynamic. Let's write the default classes which is padding of one and a half. Text is going to be white. Rounded is going to only be available on the top and it's going to be extra large.
Point is going to be bold. Text is going to be centered uppercase and text is going to be extra small. And then let's write if variant is equal to hearts, in that case background is going to be rows 500. If variant is equal to points, in that case, BG is going to be Orange 400. And then finally, inside of this div, we can write if variant is equal to hearts, we are going to render hearts left otherwise total xp there we go so this one says total xp and this one says hearts left and then inside of here we're going to open another div and then in here let's go ahead and give it a class name again of CN.
Default is going to be rounded to Excel. Background color is going to be white. Items is going to be center, which means we also need flex. Justify is going to be center as well. Adding is going to be six.
So we add some space to it like that. Font is going to be bold and text is going to be a large. And inside of here, render the value. There we go. So we have 30 points and 5 hearts left.
And now let's add some dynamic values here. So if variant is equal to points, in that case text is going to be rows 500. If variant is equal to hearts. Oh, this is the opposite, sorry. So this should be hearts and this should be points.
And then in here text is orange 400 there we go so total XP 30 hearts left 5 what we have to do now is we have to reuse our hearts and points SVG so you should already have that in your public folder let me find it there we go heart.svg and where is it points.svg so we can reuse that we already use that inside of our user progress component in here. Nope, not this one. User progress component. In here we have the image for points and heart. If you don't, visit my public folder and simply find the necessary images.
But you should already have that if you are up to this part of the tutorial. So image source in here is going to check if the variant is points. Let's actually do if the variant is hearts we're going to use a slash heart.svg otherwise slash points.svg. So a very simple ternary check here. Remember to include the slash.
And now we can use that image source just above our value here. So let's write image here, which we've already added an import for. Up can just be an icon. Source can simply be a result card, my apologies, image source constant, which we have just defined from above. Height is going to be 30, width is going to be 30, and class name is going to be mr1.5.
And there we go. That is our finished screen right here. Perfect. And now we also have to add a footer, but we already have the footer. So we have to do a very, very easy job here.
Let's go back inside of the quiz component here and in here, outside of this last div, but still inside of the fragment, we are going to reuse our footer component. So no need to import anything because in the quiz component we already have the footer. It's right here. It's already imported. So we're just importing it again here and now we're passing in the lesson ID which is our lesson ID but we are not storing our lesson ID anywhere So let's just go ahead and do that.
So we have to prevent this from being revalidated. So the same way we store our hearts and percentage and challenges, let's add lesson ID, set lesson ID, use date, initial lesson ID. Just as simple as that. And we actually don't need set lesson ID. It's never going to be changed from our side.
It's only going to be updated from the outside. There we go. So lesson ID, it seems like I have given our lesson ID an incorrect type in our footer, but that's fine, we'll come to that in a second. Let's give this a status of completed. And on check, it's going to be an empty arrow function here.
And there we go. Let's go back inside of our footer. Let's give our lesson ID a type of number or a type of string if you're working with UUIDs. And look at this now. So now you can see that we have a practice again button which will redirect the user to slash lesson slash the ID of the lesson.
So if they want to practice this again, because if we cause a refresh on this page, remember it is simply going to load the next lesson for them. And if we click continue what should happen is we should get redirected to the we should get redirected to the learn page. So let's go back inside of our quiz component in here. And do I have a router added here? I don't have a router.
So I need to add a router real quick. Let me add const router use router from next navigation. So I'm doing this in the quiz component and I have imported a router from next navigation. Make sure you check that yours is not from next router that is not going to work and it's going to cause errors. Now that you have your router component here you can go inside of this little to do which we to do hack which we have created here and very simply go ahead and add router push slash learn.
There we go. That is it. That is our finish section but we are missing two more things here. So The first one is the confetti. So let's do that.
Let's go inside of our terminal here and let's run npm install react-confetti. Let's wait for this to install. Then go to the top here and you are going to import confetti from react-confetti. And then, find this fragment here, and you're going to render this just inside of this fragment, just above the div, render the confetti, so it's a self-closing tag. You're going to give it a recycle option of false, a number of pieces is going to be 500 and tween duration is going to be 10, 000.
And one more thing that we need to add here is we need to import from React use let's add use window size and then at the top of our app, just above the router, let's go ahead and let's extract the width and the height from useWindowsSize. And then we can assign that to the confetti here. So width is going to be the width and the height is going to be the height. And there we go. Here you can see my confetti.
So when you, if you try and look at the confetti while you are resizing, it's not going to work. So make sure you refresh and try it out So you can see how it gets kind of stuck, but it will eventually even out when you try it out in an actual environment, right? So I had this bug as well, but it does get evened out eventually. So One last thing that we have to do here is we have to make use of our finish.mp3. So if you remember, we've added three files at the beginning.
We added correct, we've added incorrect, and we also added finish.mp3. If you didn't, it's in my public folder. Go and download it. So now what we have to do is we have to add one more use audio here. So I'm gonna add it here at the top.
This one is gonna be simpler. So use audio, and we're gonna go ahead and give it a source of slash finish.mp3. Auto play is going to be true here. And then what we have to do is very simply use finish audio like that. And what we can do then is simply render that just above the confetti.
And if I save here, there we go. It's quite loud, don't get surprised. You should be able to have finished audio inside. So every time you refresh, you're going to hear that sound. Perfect.
So now let's go ahead and let's remove this if clause and let's actually make this work correctly. So only if we don't have the challenge is that going to appear. There we go. So if you want to you can... Let's just do this together so we ensure we are both on the same slate.
Let's go npm run database seed again. So we reset our database, we clear everything we need. Let's go back inside of here, continue learning, select Spanish here, and let's finish this together. So I'm going to select this, there we go. And let me go ahead and lose one heart for example.
So I want to make a mistake. There we go, I've made a mistake. Now let me select the correct one here. Let me select the robot here and now when I click on next, There we go. I told you the confetti are going to be fine.
We have the confetti, we have the sound, we have the hearts left, and we have the total experience again. Perfect. If you click continue, you will get redirected to the learn page, but if you click on practice again you're going to get a 404 but confirm that your URL has redirected you to slash lesson slash one so it will take the ID of your current lesson perhaps it's not one for you if you did some changes in the seed script for example but just confirm that it redirected you to the correct place and in the footer here you can confirm that right here so lesson lesson id and I'm purposely using why am I using window location dot href We could technically do this with router, but I think I've purposely used this to cause a full refresh of our app. Perhaps that's why I did that. We'll see.
You can try out with router.refresh, but for now, keep it this way. I think there might have been a reason why I did it like this. Great, so that's what we're going to do next. We're going to enable the user to practice a lesson and remember there are two more things we have to do. We have to implement a heart model to tell the user hey you're missing some hearts and we have to add a new model for practice to tell the user hey this is a practice lesson.
Great, great job!