So now let's add the ability to actually mark a challenge as complete. So once we add our selection and click on check, I want the app to give us feedback whether this choice is correct or incorrect. So let's go back inside of our quiz component in the lesson folder and in here I want to prepare a method called onNext. So let me go ahead here. Let's actually do this below these two constants.
So const onNext is very simply going to call the current set active index is going to get the current and it's going to increment the current by one. So just a very simple reusable method so we didn't have to write this every time. And now we're gonna go ahead and below onSelect we're going to create our method const onContinue. So onContinue will trigger every time inside of this onCheck on the footer. So let's already replace this empty arrow function with onContinue.
So onContinue will be able to be fired even if the answer is wrong or if the answer is correct alongside status being none and there's a reason for that. So first things first, we are never going to be able to continue if there is no selected option at all. So let's break that function immediately. Then if status is wrong and the user presses a button in the footer, we are very simply going to allow them to retry because the button in the footer will simply say retry. So we set the status back to none and set selected option to undefined and we break the method.
The breaking part is important so make sure you do that and then let's copy and paste this and do the same thing if the status is correct. So if the status is correct we are very simply going to call on next, meaning load the next question or challenge, set the status back to none and set selected option back to undefined. Great. Now we're gonna handle the third case and that is if we don't have neither wrong or correct status. So let's find the correct option.
So options.find, option, option.correct. So now we know which option is the correct inside of our list passed from the props here because we added the options from this challenge here. And now let's go ahead and do the following. If we have a correct option and if correctOption.id is equal to selectedOption which we just chose let's go ahead and console.log correctOption And then let's go ahead and handle the else statement where we are going to log an error incorrect option. Let's go ahead and try this out.
So I'm going to go ahead and open my my inspect element here and I will select El Hombre. Let me just hide this and click check. There we go. This is the correct option. If I select La Mujer, that is an incorrect option.
If I select LROBOT, that is an incorrect option again. Perfect. So our code is working. Let me go ahead and demonstrate why this is working in the Drizzle Studio. So npm run database, Studio.
So in here, let me just show you, we have the challenge options and only one of these challenge options is labeled as correct, which is Elombra. So that's why when I selected Elombra, it compared the ID of one with our correct option, which we searched for in the array right here. And we found, all right, that is the matching ID. Great. And you can also do this.
If there is no correct option, we can just break the method. There is no need for us to check if there is a correct option. There always needs to be at least one correct option. Otherwise our code can't even work. So technically telling user that their option is incorrect is not correct because we are the ones who forgot to add the correct option.
So what I want to do now is I want to create a method that will be able to actually mark a challenge as completed or not. So let's go ahead and close this. Let's go inside of our actions here and let's create a new file challenge underscore progress dot ts. Let's mark this as use server so this is quite important make sure you do that and then in here let's go ahead and write export const absurdChallengeProgress, which is going to be an asynchronous method which will accept the challenge ID which is a number. Now in here we are going to go ahead and destructure the user ID from await out from clerk next JS.
If there is no user ID, we are going to throw a new error and let's just properly write the if clause we're going to throw a new error here writing unauthorized otherwise let's go ahead and let's get the current user progress using a way to get user progress from database queries. So I purposely named this current user progress and not user progress because we're going to have to import the user progress later from the schema. So I don't wanna create any conflicts with the constants here. And that's gonna be it for now. I'm just gonna go ahead and add a to do here, handle subscription query later.
So we don't have that yet. We're gonna do that later. If there is no current user progress, we're gonna go ahead and write throw new error user progress not found. So we need the user progress. User progress is what holds the current active course ID, the hearts, the points and the active course.
If that doesn't exist during this upsert challenge progress there is nothing we can upsert. Now let's go ahead and let's find our challenge which we are trying to absurd so challenge await database from database slash drizzle so let me just separate this too there we go so database dot query dot challenges find first where equals from drizzle or m make sure you add the equals comparison challenges from database.schema so you need to have the challenges here challenges.id needs to match the challenge id which we have passed from our prop here if you're working with uui this this would be a string You can see how that gives me an error because I'm using serial inside of my schema. So I need to use a type of number. If there is no challenge to absurd, we're going to throw a new error. Challenge not found.
Now let's go ahead and let's extract the lesson ID. So const lesson ID will be challenge lesson ID. So what is the lesson of the current challenge, which we're trying to find. And then let's go ahead and write const existing challenge progress. So await database query challenge progress.
Find first inside of here, we are going to write WHERE AND, so make sure you import AND from Drizzle ORM the same way you added the equals so let's use the AND. So AND is used to combine multiple filters. So we're going to have two equals inside of here. Let's go ahead and write for the first one. If challenge progress, which we need from database schema.
So make sure you added the challenge progress alongside challenges in the database schema import. So challenge progress dot user ID needs to match the currently logged in user ID and challenge progress challenge ID needs to match the challenge ID from our props. There we go and now we using the existing challenge progress we can know whether the current modification of or viewership of this challenge is a practice or not so if this is users first time creating an absurd and creating this model challenge progress. It means that they are in an active lesson. But if this challenge progress already exists, that means that user is practicing again on this challenge.
So we can derive that. So let's write const isPractice and we can simply turn the existing challenge progress into a boolean by using double exclamation points like this. So now we can simply derive the status of this practice, the status of this challenge. So is this the practice or is this a live first time seeing user challenge simply by deriving it from whether the existing challenge progress exists or not. So this is what we are going to do now.
If currentUserProgress.hearts is equal to 0 and if we are not practicing. So on practice we won't prevent the user from practicing if they don't have hearts left. The hearts are only going to be affecting the current active lesson. So if user has no hearts left and this is not a practice we are going to return an error which will simply say hearts And I'm going to add a to do here, not if user has a subscription. So we currently don't have that implemented, but I'm already adding a to do to remember that.
And now let's go ahead and do the following. Let's write if this is just a practice. We are going to await database update challenge progress. .Set and in here I'm going to write completed true so that's the only thing I want to update just in case completed was somehow false right so challenge progress dot id comparing with existing challenge progress dot id So when you do a database update you need to chain it with a .where and inside of .where you would pass your query here. So let me go ahead and just separate this like that.
There we go. So that handles the update of the challenge progress for the practice. And now let's also update the user progress. So this is a server action, absurd challenge progress, that would get fired if the user answered a correct answer. So in practice mode, we also have to increment the user's hearts.
And also, for every correct choice, the user's points will get incremented by 10. So we have to do a WaitDatabaseUpdate, and we have to update the user progress here. So let's go ahead and set, and let me see if I have user progress imported. I don't. Make sure you have user progress from database schema here.
So we need user progress.set. Hearts are going to be math.min. So we're going to use the lower of the two values so currentUserProgress.hearts plus one but if user already has five hearts I don't want them to have six hearts, because the maximum value is going to be five four hearts, right. So what math dot min does is it chooses the lower of the two values. So if current user that progress hearts results to six, it's not going to give users 6 hearts.
Instead, it's just going to fall back to the default 5 hearts. Because in our schema here, we have in the user progress, right here, default of 5 hearts. It would be a good idea to create a constant to replace this number five here and here to stop being a magic number and instead make it a constant like default heart or maximum hearts something like that to make it clearer. And let's go ahead and increment the points. So there are no limits for the points at least not in my iteration of this app, in yours in production it might be a smart idea to somehow limit the points.
So we updated the hearts and the points. And what we have to do now is write dot where again equals user progress, user ID. So make sure that this user progress here is the one you're importing from database schema. Make sure you didn't name these constant user progress. So now we're going to use this user progress.
Actually, we don't have to use this user progress. We can simply match it with the user ID, which we have. There we go. And before we go outside of this, is practice if clause, let's do the following. Let's revalidate the path slash learn.
So let me go ahead and import this here. Next cache, revalidate path. We are going to revalidate the path learn, lesson, quests, leaderboard, and also backticks slash lesson, lesson ID. So we are going to revalidate all paths which use any of this values like hearts, points, the current active lesson, is practice, all routes like that. And These are the ones that we need.
Great. And then we can go ahead and go outside of the if clause. Oh yes, let's not forget to return this, right? So I'm not going to revalidate any, I'm not going to redirect anywhere. I'm simply going to break the method.
This is very important. Make sure you are breaking the method inside of the isPracticeIf clause. And now outside we don't have to write any additional else. We can just go ahead and write await database insert challenge progress. So this time we are using insert instead of database update.
So in here we use database update for the challenge progress, but in here we are inserting it for the first time. So let's write challenge ID, user ID, and completed to be true. That's it. So where do we have the challenge ID from? We have that from the prop.
We have the user ID from the auth from clerk, and we set the completed to true, because we know that we are calling this when user answers a correct question. And let's do await database.userprogress, my apologies, database.update, userprogress.set, database.userprogress.set and in here let's call points.currentUserProgress.points plus 10 and we also have to write where user progress user ID matches the user ID from our app. Let's go ahead and just copy this revalidation here and that is it. There we go. So that's how we are going to control our correct answer choice.
So if you want to you can extend this absurd challenge progress and besides accepting the challenge ID you can also accept the option ID and then you can do another validation here on whether the choice was correct. So let me go back inside of my quiz lesson here. So you can see that we do the correct option check here on the front end. So that's great because it we immediately know whether it's correct or not right the user doesn't have to wait but it might be a good idea to also pass that to the back end. We'll see for now I'll just do it like like I am doing it so let me do the following go back into the quiz component and import use transition from react now in here let's go ahead let me go to the top here and let me add start transition and is pending or my apologies is pending and pending and and startTransition.
It doesn't matter the naming, but the order matters. And let's add useTransition here. So I can just see if this is the correct order. Yes, this is the transition start function and the pending should be a boolean perfect and now inside of the on continue method which we've just started developing here so far the only thing we've done is added the console log for the correct option so what we're going to do now is the following we are going to add start the transition here and in here we're going to go ahead and call absurd challenge progress so make sure you've added an import for the absurd challenge progress from action challenge progress and in here we are expected to pass a challenge.id and let's use .then here let's get the response and if response question mark dot error is equal to hearts. I'm going to add a console error here, missing hearts.
And I'm going to break the method. So what I'm doing here is I'm checking for this error right here, where is it? There it is. If currentUserProgress.hearts is 0 and if this is not a practice, we will return an error, hearts. So in here, I check for that error and for now, all I do is show a console error.
Later, we're going to create a modal which is going to show that. Otherwise if this method did not break we continue forward and what we do is we set status to correct. And then let's call setPercentage and let's call the previousPercentage previous plus 100 divided by challenges.length like that. So we increment the percentage by one of our, by one new completed challenge. And if initialPercentage is equal to 100.
So what does this mean? If we loaded a challenge, a lesson, and if the initial percentage of the entire lesson is a hundred percent it means that all challenges have already been completed which means this is a practice. So what we are going to do here is we're going to update the hearts on the front end as well because we know we've updated them in the challenge progress here. So if it is a practice in here, I already know that I have added some new hearts here. So now I want to do the same thing here and I can use the exact same method with Math.min.
So I'm gonna do Math.min here and I'm gonna add previous plus one and limit it to five. There we go. So in practice we get new hearts on the front end. And let's do a catch method here and call toast from sonar. So let me just add it here.
Toast.sonar, where was I? And I'm gonna pass in an error here something went wrong please try again and here's what I want you to do before you try this out so for now actually no this is fine you don't have to do anything let's go ahead and try this out do I have the app running I do so in my drizzle studio I currently have no challenge progress so nothing exists yet so if I select El Ombra and click check there we go so something just happened really quickly Let's go ahead and refresh our challenge progress. There we go. We have a challenge which is completed and we have the matching relation which says which one of these is the man. So let me go ahead and open this.
There we go. Which one of these is the man. So let me go ahead and open this. There we go. Which one of these is the man?
And we selected a correct option for it. Perfect. So now what we have to do is we have to go, and there we go. We even have a sign that this is now completed. Perfect.
But now we have to update this so it shows a finish screen as well. But first let's go ahead and let's go back. Let's see if I can go back. I cannot go back currently because we are using a different URL to show the practice lesson. So let's see what is the best way to go forward with this.
I think the best way to go forward is not to handle the practice but to still work inside of this current active lesson, in order to do that, we're gonna have to add some more challenges. So let's go inside of our scripts here inside of seed, and let's just add more challenges. So in here I have these challenge options, right? And I have these challenges. So let me add another challenge here.
So lesson ID can stay the same. The type can be select. And the, let's go ahead and let's actually change the type here to be assist because both should work. Let's change the order of these challenges. So this is an order of one, this is an order of two and the question here since this is a assist it's simply going to say for example the man because remember our assist will be a mascot asking a certain phrase like the man and then the user will have to select from the list of options.
So change the second and yeah let's also change the ID to the number two So we have challenges ID 1, ID 2, lesson ID 1. There we go. Let's go ahead and do the following now. So we now have these challenge options for... This is for the first challenge.
So let's copy the entire thing. You could technically do it all in one, but let's separate it. It's easier. So now this is ID 1, 2, 3. So this one would be ID 4.
This one would be ID 5. And this one would be ID of 6. And then the challenge ID here would be 2. So this wouldn't be that question, it would be just the man, right? And this would be the correct one and we don't need an image source for this one.
We would just need an audio source. This is again challenge ID 2. We don't need an image source. This is a... Oh and this would be the opposite I believe or perhaps...
Oh yeah you can also play around. Okay Let's just continue working as we are right now. Challenge ID here is 2, image source doesn't exist. I think this should be fine now. And let's go ahead and I already want to prepare the third challenge here.
So if you want you can just copy my seed script you don't have to write this yourselves. So let me add this. This will be the third option here. So the lesson ID here is 1 and the third option would be select again. The order of this is number three and the question of this one can be which one of this is the robot for example.
So we have this handled we have this handled Let's copy the first one again and let's go to the bottom here. The challenge ID here would be 3 I believe. Yes, the ID is 3 which asks which one of these is the robot. So inside of my latest insert of the challenge options the challenge ID 3 would ask which one of this is the robot so this would be false this would be false as well and this would be true and I believe we we don't have to write IDs for challenge options. They are self-incrementing and we don't need access to them later.
So yeah, let's make this easier for us. We don't need to write IDs for inserting challenge options. We know they're gonna be incremented. So let me go ahead and remove them just like that. But we do need to write IDs for these ones above because we need to map them to the challenge ID but we are not using the challenge options anywhere further.
And yeah, let's change all challenges IDs from one to three. That's also important. Otherwise the code is not going to work. So let's try that out now. If I go ahead and go into the terminal and let me shut down the Drizzle Studio and instead run npm database seed now.
I think this should work just fine. There we go, seeding finished. Looks like everything is working. So if I just hit refresh now, I should be redirected to the courses page. I will select Spanish because that's the only one I have.
And let's go ahead and see this now. So if I click Alhambra now, Alhambra and click check. There we go. Now you can see that we have some progress and we were not immediately redirected back. So now this is actually working great and if I go back now and click end session I should have a progress here as well and I do and if I go back here there we go You can see how I'm redirected to the second challenge, which is our assist type of challenge.
So there we go, selected the correct meaning. This one says the man and I have to select El Hombre again. If I click check, this one is correct. And there we go. I can now go further.
And if I click next manually, there we go. I'm loading the last question, which asks me which one is the robot so if I click check again there we go so we just have to handle this last part where we finally finish the challenge we have this weird redirect so I don't want it to be handled in that way so what I want to do now before we wrap up this chapter is run database seed again again I'm going to leave a separate seed script So I'm gonna have multiple seed scripts in my project probably. I'm gonna call this one, you know, testing the lesson seed or something like that. Or maybe I will just have seed one, two, three, and four and just find the one which looks the most like this one. Or you can simply use my git commits inside of my projects.
In my GitHub, every single git commit that I do matches the chapter exactly. So you can just find the exact commit for this one. But don't worry, there are gonna be enough seed scripts for you to try out if you are having problems writing your own. And now let's go ahead and let's go back inside of the app folder, lesson in inside of quiz here. We've handled the correct one right But what we are missing is handling the invalid one.
So let's go ahead and do the following. Let's also make use of this pending state. We are not using pending anywhere. So let's find all the places where we have disabled false. So we have one right here where we render our challenge.
So let's change the disabled to be pending. And then in here, let's add pending or selected option for the footer. Like that. So we've handled the correct option. Now it's time for us to handle the incorrect option.
So in here to handle that I want to do the following. I want to go inside of my actions, user progress. We already have absurd user progress but I want to create a separate method here called reduce hearts. It's going to be an asynchronous method which accepts the challenge id which is a number or a string if you're working with UUIDs. Let's extract the user id from await out and then let's go ahead and write if there is no user ID, we can immediately throw a new error here, unauthorized, and then let's go ahead and get the current user progress using await get user progress, which is above.
And let's go ahead and write a to do get user subscription. So we don't have that yet. Then let's go ahead and let's find the existing challenge progress. So const existingChallengeProgress is going to be await database.query.challengeProgress.find where we're going to use an end operator. So make sure you have added end from drizzle ORM and also equals.
So we are going to need both of this, but for now let's use the end one. So in here in the reduce hearts method we're going to use end and then we're going to use two equals. So if challenge progress from database schema so make sure you have added the challenge progress from database schema alongside your user progress. This is a new file, this is user progress not challenge progress. This is where we are writing reduce hearts.
So if challenge progress user id matches the current user id, so that's the type of existing challenge that we want to find but also if challenge progress dot challenge id matches the challenge id from the prompts which we just passed so that's how we know that we found the existing challenge progress and that's how we can determine whether something is a practice or not. So if this is a practice it means that we have an existing challenge progress. So on the front end we are using the initial percentage 100 to derive whether something is a practice or not. But on the back end, we're going to use the existing challenge progress state. So if isPractice, we can simply break this method and we can return an error here.
Practice. That's it. And we're simply not going to do anything, right? If you want to, you can display something on the frontend to indicate to the user that they will not lose hearts by making a mistake in practice or you can simply ignore it. There we go and now let's go ahead and do the following.
So let's do some additional checks. If there is no current user progress We can throw a new error here. User progress not found. Also notice the difference between this error that we are throwing here and this error. So this is an actual error that I want to completely break this app.
So this is critical. The user progress needs to exist. This one is not exactly an error. This is a normal API response. Imagine it like that and instead of having data we pass in a field called error.
I'm pretty sure there are arguments that this might not be the best practice and I'm specifically talking about naming the field error, right? So this is completely fine, but the field could have been named something like reason maybe, the reason I broke this, the reason I returned this early. But I personally like error. I know how my API works. I know what to expect.
So the error will not cause any problems in my architecture, right? And we do the same thing in the challenge progress, I believe. So if user doesn't have enough hearts, we don't break the app, we just return a response with a field error, which we read later, so we know what to tell the user. Why didn't this method go through? Whether these errors, this error means, you know, I can't even attempt to do something.
This is even a security issue at this point if you don't have a user progress. How did you even get to this point right? And now let's add a to do here. Handle subscription. So again if if it's not practice and the user has a subscription, we are not going to reduce their hearts.
So this is a method for reducing hearts. So if there is a subscription active, we will just break the method. And let's do another one. If currentUserProgress.hearts is equal to zero, there is no point in reducing the hearts even further. So we will just return an error hearts again, meaning you don't have enough hearts for me to reduce hearts, right?
And let's go ahead and do the following. Let's do await database update, user progress.set, and let's pass in the hearts, math.max this time, current user progress.hearts minus one or zero. So we are doing the opposite logic of giving, of adding additional hearts, which we did in the Upstart Challenge progress. So in here, we are choosing the larger of the two values. Right?
So in case the hearts is already zero and we decrease that by one, that's going to be minus one. We don't want to show that the user has minus one hearts. We want the minimum to be zero. So that's why we're using the larger of the two values and zero is larger than a potential minus one. So it's going to choose zero.
That's why in here, we're using math.max. And we have to add a where here. So where user progress, let me just see, you need to have user progress imported from database schema. I don't know if I told you that already or not. So you can do the query, user progress dot user ID and user ID from destructuring await out.
I think we already have user progress, right? We have challenge progress, challenge ID. Yeah, so I don't think you've added that then or perhaps you use it somehow but just ensure you have challenged progress and user progress from database schema otherwise you will not be able to do your queries properly all right so we have that finished And now what I want to do is revalidate things. So let's revalidate path, shop, learn, quests, and leaderboard. And let's also go ahead and do slash lesson lesson ID.
Do I have the lesson ID? I don't have lesson ID but I think I should have it here so let's go ahead and learn how we can destructure it so very easily if we have the challenge we have the lesson so we have the challenge ID here but we don't load it anywhere so is there a smart way I can do this where do I do this So immediately after my current user progress. So how about I do this? Const challenge await database query find first, sorry, challenges find first where equals challenges from database schema. So make sure you have added challenge progress, challenges and user progress from the database schema.
So now that I have the challenges from my schema, I can do the query challenges.id and I can make sure that it matches the challenge ID from my props like that. And if there is no challenge here, I'm going to throw a new error here, challenge not found. And let me just not misspell the challenge. And then I can destructure the lesson ID from challenge lesson ID. So yeah, it's a good thing to check this because there is no point in reducing hearts if the ID for the challenge I'm trying to do it doesn't even exist something is obviously wrong in that case.
There we go. So we now have a method to reduce our hearts so let's go ahead and let's use it in our quiz component. So go inside of the app folder, lesson, and in here we have the quiz component. And let's go in the else here. Start transition.
Reduce hearts from user progress. So let me go ahead and add that here. Reduce hearts is the one we've just created. We are going to pass in the challenge.id. Then we're going to get the response.
So again, if response error is hearts, I'm going to add a console error here, which will simply say missing hearts, the same way we have that above. And I'm going to break the method. So breaking the method is very important. And now what I'm going to do is I'm going to set the status to be wrong. And then I'm going to go ahead and do the following.
If there is no response error, so why am I doing this? If there is an error in my response, which is not hearts, because I've handled hearts above. So the only two types of errors that can be is the subscription error or practice error. So if any of those two are happening, there is no point in me reducing the hearts on the front end, right? So only if I no longer got any errors in my responses, meaning that my Reduce Hearts method has surpassed its practice and it has surpassed this and it will surpass the handle subscription.
That means it has gone to this point, which means the backend has reduced the hearts. So now I have to reduce the hearts on the front end using set hearts previous value math dot max so the same thing previous minus one and zero to ensure we never go below zero and dot catch here, toast dot sonar, toast dot error. Something went wrong. Please try again. There we go.
So let's go ahead and try our function again. So now we are handling the incorrect states as well. So if you run npm run database seed and click on learn you should be able to select the course from the start again. So this is my first time there we go. Let's go ahead and try and decrease our hearts.
So I'm going to select a robot here click check. You can see how I had a nice little disabled field now because I added the pending state there. There we go. This is incorrect and look at this my hearts are now four. What happens if I exit the session?
There we go. They are four again. If I refresh my page they are for again. Perfect. And you can see that I have made no progress here.
So what happens if I try and make these mistakes again. There we go. I'm making mistakes. I'm making mistakes again and I want to prepare my console here because I want to get that message that will tell me that I don't have enough hearts so there we go I'm at zero hearts so I'm gonna click retry again and now it doesn't matter if I try selecting a correct answer there we go I have an error missing hearts if I try and selecting a wrong answer it shouldn't matter missing hearts again perfect so that's what we're going to handle in the next chapter we're going to handle improving the feedback on missing hearts we're going to show the pop-up which is going to say hey you have missing hearts You can upgrade to pro to get unlimited hearts, or you can purchase hearts at the shop. Or what we're going to do is, sorry, not or, and we're also going to handle some audio effects when we get a correct answer and when we get the incorrect answer and we also have to handle the finish screen so I don't want us to just get redirected I want us to see the finish screen.
Nevertheless, great, great job!