So now let's go ahead and let's change this active lesson and the percentage from being undefined and hard-coded to zero to use actual data from our database. So we're gonna have to revisit our queries. So let's go inside of here and let's go ahead and create a few new ones. So I'm going to go here at the bottom and I'm going to export const getCourseProgress. It's going to be cached, it's going to be an asynchronous function, so open an arrow function here, and let's go ahead and extract user ID using await out from clerk, and let's get user progress from await get user progress.
Now let's check if we don't have user ID or if we don't have user progress active course ID in that case you can just return null. There is no progress we can load for this user. Now let's do const units inactive course. That's gonna be await database query units find many. Let's order by units, make sure you have them imported right here, so you need those.
Let's go down here, units and let's destructure ascending and let's return ascending using units.order. Then let's add a filter equals units.courseID and userprogress.activecourseID. And then let's add with lessons and let's add order by to the lessons here. So let's extract the lessons and ascending and return ascending lessons.order. So the same way we did with units order.
Let's also add unit to be true and challenges. And let's also add their challenge progress where the current user id equals the challenge progress user id So similar to what we did with our get units. Challenge progress dot user ID equals user ID from our out method. There we go. Now that we have units in active course, let's find the first uncompleted lesson.
So that's the lesson that we're going to show to this user as the one they are currently active on. So const firstUncompletedLesson is going to go over units in active course. It's going to use flat map, get the individual unit and return unit.lessons here. And then it's going to use find and it's going to find the lesson where some of its challenges are uncompleted. So let's return not challenge, challenge progress.
So if we don't have challenge progress at all that could be a candidate for the first uncompleted lesson or if challenge.challengeProgress.length is 0 So this is in one line like that. And then let's go ahead and return active lesson to be firstUncompletedLesson and let's also add active lesson ID as first uncompleted lesson question mark dot ID just for ease of use. Great so now we have something to call for our course progress but we are not done yet so what we need now is to create a get lesson method so let's go ahead and do that I'm gonna go here at the bottom now we need to export const get lesson. Get lesson is gonna be a cached method it's gonna be an asynchronous function as well. Let's go ahead and extract the user ID, so not cached, just cache from await out and let's get course progress this time from await get course progress and here's one thing I forgot get lesson will have an optional ID which is a type of number or if you're working with UUIDs it's gonna be a string.
I'm working with a type of serial so it's a number. So this time we're gonna be using getCourseProgress. So if user didn't pass an ID then we're going to load a lesson which is located as the first uncompleted lesson in this unit for that course. So, we're going to reuse this method when user wants to practice. So if they click on a specific lesson and we have an ID of that lesson, then we're just going to load that lesson.
Otherwise, we're going to assume that we want to load the first uncompleted lesson for the user. So let's define that by writing lesson ID to be either the ID from the parameters or course progress dot active lesson ID. If we don't have any type of lesson ID, so neither from the parameters nor from our course progress, that means there's nothing we can load. And now let's go ahead and write const data to be await database query lessons find first where lessons make sure you add the import for lessons from DB schema here so let's go all the way down here. Lessons.id equals to lesson ID constant which we have defined here and ensure that we always have at least one.
And let's add with challenges. Let's order these challenges. So let's use order by here. Challenges. Ascending.
And let's use ascending challenges.order. Let's go ahead and include the challenge options and let's include the challenge progress as well. So we know how much of this lesson has the user completed. So only where challengeProgress.userId matches the user ID. And let's see what did we do wrong here.
Challenge progress. .Userid. Do we have userid here? We do have, but something here seems to be incorrect. So we do have challenge progress.
So if I just write challenge progress true, that is fine. But if I add where, that's where it gets stuck. So let's see, do I have an auto completion for .userid? I do have that. Can I manually type something?
I can. So we don't have a check whether user ID is perhaps undefined, my apologies. So let's go ahead and do the following. In here, if we don't have user ID, we are going to break early and return now. No need to even try to get course progress.
If user is not authenticated, we're not even going to waste our resources on this. So we're just gonna break and return null. So now that we have ensured that in this part of the code we have user ID we no longer have that error in our where query. Great! So let me just add these commas here because I need to have them.
And now that we have that, let's go ahead and check. If we don't have data, or if we don't have data.challenges, that means there is nothing we can display for this lesson. So again, we're gonna break early and return null. And if all of our tests have passed, let's normalize these challenges array. So I want to do the similar thing that I did when loading my get units, where I normalize my data with the completed field.
So let's do the same thing for the challenges here so it's easier for us to work with them on the front end. So const normalized challenges are gonna be data.challenges.map. Let's get individual challenge. Let's define whether they are completed or not. So a challenge is completed if we have challenge.challengeProgress and if challenge.challengeProgress.length is larger than zero.
And let's return challengeCompleted. Again, it's challenge and let's pass in completed to be completed or you can use the shorthand operator like this And now let's go ahead and finally return data so we are loading our entire lesson here, right? But we are going to replace the challenges array with our custom normalized challenges here so let's write challenges normalized challenges there we go and let's be consistent and let's just add one more check here so I'm going to add if we so if we have challenge progress so let's do it like this if we have challenge progress if challenge progress length is larger than zero and if challenge.challengeProgress.every let's get progress so if every progress is completed that's also what I want because if you take a look at my previously normalized challenges here I think we do the same thing so let me just check that here. There we go. So in here I do the same thing.
So I check for challenge progress, for challenge progress length and also for progress completed. And inside of get course progress let me see if I should extend that to here as well. So first uncompleted lesson, challenge.challenge progress. I think this is fine because in here we are doing the opposite in here we are looking for the first uncompleted lesson so I think there's no need to check if they are. Oh, well, I guess what we could do is we could also do challenge.challengeProgress.sum progress.completed equals false.
So I guess we could do that. So then what would happen is we are going to check if we never had any progress initiated or if we have some progress but for some reason it's false. Right, so I think that should be fine but I am going to add a little to do here. If something does not work, check the last if clause which is this one. And I'm gonna copy this and I'm gonna write the same thing here just because this is not exactly true as my source code so maybe it will break something later but I doubt it.
I think we improved upon my source code by doing this. Great, so now that we have this we can create one last method which we need which is to load the lesson percentage. So let's export const getLessonPercentage to be cached, asynchronous method, is going to get course progress from await getCourseProgress if there is no course progress or course progress active lesson ID return 0 because lesson percentage should return a number. So now let's get the lesson using await get lesson which we defined above and we can simply pass course progress active lesson ID. So technically we don't even have to pass this because get lesson by default will use that But let's be explicit and let's pass it this way.
If there is no lesson we are again going to return 0 and then let's do const completed challenges to be lesson challenges.filter challenge. Let me just collapse this so it doesn't break into a line. So challenge challenge dot completed. That's our filter and then we can create our percentage using math dot round and we can now use the following so we can do completed challenges dot length divided by lesson dot challenges dot length in this one times 100. I think that should be it and just return percentage.
There we go. So you can see how normalizing our challenges has helped us so we can just easily filter out those that are completed and then we divide the completed challenges by the total challenges in this lesson, multiply them by a hundred and we round the number to get the percentage of how much this lesson has been completed. There we go. So when I hover over this, there we go. It always returns a number.
Perfect. Now that we have a get lesson percentage, we have our get lesson and our get course progress. We can head back inside of page.tsx here and let's add those to our promise here. So I'm gonna go ahead and I'm gonna add course progress data to be get course progress so make sure you add that import. I'm going to add lesson percentage data to be get lesson percentage from queries and I think that should be it.
So make sure you have added the get course progress, the get lesson percentage, the get units and the get user progress from your database queries. So these new ones which we just created. So now let's add those to our promise all here. So we have user progress and units. Let's go below them and let's add course progress data and lesson percentage data.
So after units, we are getting our course progress and after course progress, we are getting our lesson percentage. And now we can go ahead and pass the active lesson here instead of undefined to be course progress dot active lesson. Now this will give us an error here. So what we can do, oh, let's just go ahead and first check whether we have course progress. So if we don't have course progress, we can just redirect again to slash courses here.
So there we go pass in course progress dot active lesson inside of here and go ahead and pass in lesson percentage here. There we go, right, so something has changed. This is now unlocked but all of these are marked as completed so that seems like a bug so I'm gonna debug what's going on here but I think this is okay let me just check one thing so this has unit it seems when I was developing this initially for some reason I got a typescript error here So right now I'm not getting it. But in case you are, this is how I fixed it. So I wrote as typeof...
So let me zoom out for this. As typeof lessons from database schema. So let's go ahead and import that from database schema here. I'm gonna add it to the top. I wrote lessons.infer select.
And then I extended that manually here and added the unit type of units schema so we have to import this as units schema because we have units here already so we can't add that import from the database schema So I'm going to import units as units schema. So we have an alias here. So type of units schema dot infer select. And let's add pipe undefined. So that's what I had to do for my active lesson here.
Right now it seems to work fine without this type but for some reason my TypeScript has lost its scope inside of .map. That was at least during the first iteration. Perhaps I have a newer TypeScript version now, maybe it was a bug fix. Nevertheless, if you don't, this is how you fix that. So just before I wrap up the chapter, I'm going to go inside of my queries here.
Where are my queries? And I want to check if I have any typos. So challenges, I do. So I just don't like this typos. It's supposed to be challenges with an E.
So let me just use the challenges here. But now I have an error here. So order by challenges implicitly has an any type oh if I just reload my window will that fix the typescript error yes it was just some weird type error here and challen again there we go no challenge errors here all right so it seems like we are having a little bug here. These are being displayed, at least to me, as finished. So I'm gonna have to debug a bit why that's happening and we're going to resolve it in the next chapter.
And one more thing I want to work on is instead of this get units, we are loading these units and these lessons and challenges, but I think we need to do this to do. I need to add the order the same way I've added the order right here when loading, I believe my get lesson. Yes, you can see how here I use challenges and I use their order in ascending order. So that's what I'm going to do in the next chapter. We're going to go back inside of this get units.
So I think right now, even if I added more of this, they would be in correct order simply because my seed script has serial incrementing IDs. So obviously it's working fine, even without me using the order by method, but we're gonna play around with that later. Priorities for now are to check why some things are displaying as finished when they obviously should not be and then we're gonna go ahead and do the other stuff. Nevertheless, great, great job. So after that we're gonna go ahead and resolve this 404 page which is going to be our lesson page.