So now I want to change this from a stringified JSON into an actual UI. But just before we do that, I want to revisit my queries here inside of the DB folder, specifically the getUnits query here. One thing that I think is missing here is additional filtering for challenge progress. So right now we are loading all challenge progress for all challenges, which is correct. But let's take a look at our schema.
So inside of our schema, if we visit and find the challenge progress, we see that we have something user ID, which means that we can query by user ID and we should query by that. So no point in loading this query and using challenge progress of other users. So instead I'm gonna go ahead and import user ID. I'm going to destructure user ID from await auth and we already have auth since we use it in user progress here from clerk. So I'm going to extend this if query, so if I don't have user ID or if I don't have user progress.
And then in here, let's go ahead and extend this with a where equals challenge progress. Make sure you've added an import for challenge progress right here from database schema. Challenge progress dot user ID needs to equal our extracted user ID from this awaitAlf method right here. So right now, if I save this and refresh, nothing should change. We still don't have the challenge progress and completed is still false.
Great! Later we're gonna see if we can actually use the order by here and use the order. So I'm simply going to add a comment to do confirm whether order is needed. So I'm going to add this to do and we're going to check that later. Great, so we have this and now let's go ahead inside of our app folder inside of main learn page.tsx.
Inside of here we are rendering our units. So now I want to change this JSON and render a unit component. We don't have that yet so we have an error here but let's already prepare some items. Let's pass in unit ID, let's pass in unit order, let's pass in the description, unit.description, let's pass in the title, let's pass in the lessons, and let's go ahead and pass in the active lesson. For now let's make it null, we're gonna add this later and active lesson percentage for now is going to be zero as well.
So now let's go ahead and create this unit here. So inside of the learn folder I'm gonna create a new unit.tsx component and I'm gonna go ahead and create the props for it first. So the props are going to have an ID which is a number or a string if you're using UUID. Order is going to be a number. Title is going to be string.
Description will be a string as well. Lessons, open parenthesis, are gonna be a type of lessons from database schema, infer select, but we are normalizing them, which means that every lesson of ours is also going to have a completed Boolean. And all of that together is going to be an array. So make sure you add an array at the end. Then we're going to have an active lesson, which is going to be a type of lessons inferSelect And we're also going to have its relation with the unit.
So let's add a type of units from database schema dot infer select, or it can be completely undefined. And active lesson percentage is going to be a number. So make sure you've added the imports for lessons and units from database schema so that you can use their infer select. And then let's export const units here. My apologies, unit.
And let's assign the props here. And let's return a div. Now let's go back to our learn page and let's import the unit from .slash unit. So the same way we'd added an import for the header. In here, I have an error for the active lesson.
So instead, let me write undefined here because we support undefined as we've written right here in the active lesson props. So now I'm going to go ahead and extract ID, order, title, description, lessons, active lesson, and active lesson percentage. And now let's not use divs here instead we can use fragments and I want to go ahead and do the following I'm going to create another reusable component called unit banner and I'm gonna pass in the title and I'm going to pass in the description prop. So make sure you have these two and now we're gonna go ahead and create the unit banner inside of the learn folder as well. Unit-banner.csx.
So let's create our props here which are going to have a title which is a string, a description which is a string, and let's export const unit banner here. Let's assign these props and let's return a div. Let's go back inside of the unit and we can import unit banner from .slash unit banner. And now we should no longer be having any errors. And now let's go ahead and actually make this visible by destructuring the title and the scripture from the unit banner.
Now let's give this div a class name of full width, rounded extra large, background green 500, padding of 5, text of white, flex, items center and justify between. Now inside of here I'm going to open a new div with a class name of space y 2.5 and inside I'm going to render an h1 element and write title inside and then below that a paragraph with a description inside. And there we go. Unit 1, learn the basics of Spanish. Let me change this to be an h3 element instead so it shouldn't exactly be a header and now I'm gonna go ahead and give this a class name of text to excel and font bold and in the paragraph section I'm going to give it a class name of text large as simple as that and then what we are going to do is go outside of this div encapsulating our h1 element and the description and we're gonna add a link from next slash link let's add a closing tag to it and let's give it an href to go to slash lesson and then inside of here let's use a button component so make sure you've added the button component the button import from our reusable components here Inside we're gonna add an icon notebook text from Lucid React.
So I'm gonna move that to the top here. Let's give notebook text a class name of mr-2 and let's write continue. And for the button let's give it a size of large let's give it a variant of secondary and let's go ahead and give it a class name of hidden but an LG is going to be visible so if you want to see you have to zoom out a bit there we go now it should be visible LG is going to be flex border dash two border dash bottom is going to be four an active border dash bottom is going to be two So let's go ahead and see how this looks. I'm going to zoom in a bit, there we go. So if I click continue here I'm redirected to a 404 page which right now doesn't exist but it is going to exist in the future.
So we do have a kind of a edge case here I suppose. If you want to you can change this to only display on to Excel maybe or Excel. Let's see if that exists. It does. So let's see how that looks like.
So here it's still visible. Yeah. Maybe this is a bit better. So it only shows on very large screens because there isn't really a need for this button because the same effect is gonna happen if we click on an individual lesson here, right? So I think, yeah, this is okay.
So let's go ahead and go back inside of our unit component here because we are finished with the unit banner. And inside of here we're going to go ahead and create a div with a class name flex-items-center, flex-column, and relative. And then inside of here we're going to map over our lessons. So let's extract the lesson and the index of the lesson. Then inside of here let's open the method and let's define isCurrent by using lesson.id to be equal to activeLesson?id and const isLocked is going to be if lesson is not completed and if lesson is not current.
So we are not going to allow user to select a lesson in the future and we can do that quite easily by determining has the current lesson we are iterating over already been completed. If it has we are not going to lock that lesson. User can always go back and practice it But if the user never finished this lesson and if this lesson is not the current lesson that can only mean that it is a future lesson so we are going to lock that for the user. And finally let's go ahead and return a lesson button. Like that.
So let's go inside of the learn component and let's add a lesson-button.tsx. Now in here, I want to mark this as used client because it's going to be interactive. Let's create type props here with an ID of number, index of a number, total count of a number as well, locked of an optional Boolean, current of an optional Boolean, and percentage of a number. And export const lesson button here and let's assign these props. And for now you can just go ahead and return a div component like that.
Let's go back inside of our unit and let's import the lesson button from ./.lesson-button. Now in here I'm going to pass in the key to be lesson.id. I'm going to pass in the id to be lesson.id. Index is going to be index. Total count is going to be lessons.length minus one, current is going to be is IsCurrent.
Locked is going to be IsLocked. And percentage is going to be ActiveLessonPercentage. So we are going to use this total count later so that we can determine whether this has been finished and where does it stand in comparison to other lessons. Great! So now that we have that let's go ahead and simply render for example well we can just render the ID.
Let me destructure these props here. ID, index, total count, locked, current and percentage. There we go. You should see a number 1 inside of this, representing the ID of this lesson. So what we have to do next is style this lesson button.
Now, this is how I want to style my lesson button. So styling it is easy, but positioning it is difficult. This is what I'm talking about. So this is the finished app. As you can see, every single one of these lesson buttons has its own position.
And This is calculated in a way that it will go one, two out, one in and then back inside. So it would go this as many lessons as we have. So we're gonna have to do this mathematically, right? So let's go ahead and do the following. Let's go back inside of here and let's define the logic for this.
So we're going to start with defining the cycle length to be 8. Then we're going to create a cycle index by using a modulus operator on the cycle length and index. Then let's define the indentation level. Now I'm going to check if cycle index is less or equal than 2. That means that I'm moving right from 0 to 2.
So I'm going to create an indentation level here to be cycle index. Else if cycle index is less or equal than 4, I'm going to move back from the center from 2 to 0. So indentation level equals 4 minus cycle index. Else if cycle index is less or equal than six, I'm going to move from left to negative two. So now we go in the other direction.
So indentation level equals four minus cycle index. Else we are going to complete the cycle and go back to the center. So indentation level cycle index minus 8. Now let's calculate the right position on this using this indentation level here. So const right position is going to be indentation level and times our spacing.
So you can experiment with this depending how far wide you want elements to go from one another. I found 40 to be the best looking one. You can of course experiment yourself. Now I'm gonna add a couple of more useful elements here. Const is first, is very simply gonna check if index is zero.
Const is last, very simple, is going to check if index is equal to the total count of all of our lessons. And const is completed is very simply going to check if we are not on a current one and if this one is not locked. That's how we know it's completed. And then let's create a dynamic icon. If it's completed, we're gonna use check icon from Lucid React.
So make sure you add check from Lucid React. Let's also add a crown and a star. So check crown and star from Lucid React. Now in here, isCompleted is going to do check, otherwise, if isLast is going to do crown, otherwise, it's just going to be a star. And let's do an href.
If is completed that means the user can click on this and they can go back to slash lesson slash the ID of the lesson. Otherwise, user is just gonna go to slash lesson and from there, we are going to load whatever is the current active lesson. So we are not going to use slash ID in order to go to the current active lesson. That's just gonna be slash lesson and then we are going to decide which one that is. So I'm following the Duolingo logic if you look at their app architecture they do the same thing.
So if you go and practice a previous lesson, they use IDs inside of their URLs. But if you click on your current lesson that you need to practice, which you have not finished yet, they just use slash lesson and then they determine which one to load. So we are doing the same logic here. You're gonna see that later. Right now this is a 404 page but later when we create logic it's gonna make a bit more sense.
So before we start styling this I want to install a package react-circular-progressbar. So let's go inside of here and do npm install react circular progress bar. Progress bar is spelled together like that. Now that we have this let's go ahead and add an import for this. So I'm going to import Circular progress bar with children from react-circular progress bar.
And now we also have to add react-circular-progress-bar We also have to add react-circular-progressbar-dist-styles.css so it has the styles. Let's also prepare a CN library from libutils and let's import a button from components UI button and let's wrap it up by importing link from next slash link. Now let's go back inside of our actual return method here and let's wrap the entire thing in a link component. The link component is going to go to our dynamic href constant. Now let's give it an area disabled of locked so the link doesn't work if the lesson is locked.
And let's also use the style here to disable the pointer events if we are locked. So if we are locked we're going to use none otherwise auto. If you want to you can collapse these elements so that they are more readable like this. Now inside of the link component, I'm going to open a div here and we can remove this ID. So our div is going to be our wrapper and we're going to use it to position.
So class name is going to be relative. But we are going to be using style here for some dynamic stuff. So the right is going to use backticks right position constant pixels. Margin top is going to check if is first and if it's not completed it's gonna be 60 otherwise 24. So the reason for that is is that the current lesson is gonna have a little floating pop-up like a little tooltip saying start.
So when it's first, we need to move it just a little bit away from this banner otherwise it's going to hover over the banner and it's just not gonna look that good. Great! Now let's go ahead and do that little floating banner. So if we are current, let's go ahead and create a div here. And let's go ahead and kind of hack this.
So I want us to hard code the current. So let me go inside of the unit current. I'm gonna do this. Go back inside of the unit.tsx, find the current and for now do true pipe pipe is current and I'm going to add to do remove hard-coded true. So I want you to trigger this to be true.
The current has to be true right now. So we can show this right here. So let's just add an alternative here. Give something. There we go.
So you can see how something is not rendering. So now if I write current, current is rendering so that should be the same thing for you make sure you put true here so that then when you destructure the current from the props for the lesson button you can see what you're working on right now. Let's go ahead and give this div a class name of height 102 pixels and a width of 102 pixels and a relative class name. Then let's create a div here. So this is going to be our floating little element here this div is very simply going to say start let's give this div a class name of absolute minus top minus six is this correct?
Absolute. All right. Minus top minus 6. Left dash 2.5. Px of 3.
Py of 2.5. Border of 2. Font is going to be bold and uppercase. Text is going to be green 500. Background color is going to be white.
Rounded, extra large so we have some roundedness, animate-bounce, tracking, wide and z-index of 10. There we go. You can see how we have a nice little start floating element here. What's missing is a little chevron here at the bottom. So let's create it.
Just below this we're going to create a self-closing div. This self-closing div is going to have class names to create a chevron. So let's give it a class name of absolute left one and a half minus bottom minus two width zero height zero border x eight border x transparent, border top 8, transform and minus translate dash x dash one and a half. And there we go! You can see how we now have a little chevron here.
And you can see how this one is bouncing and it's gonna be bouncing just above our current active lesson. So now we solved this little bouncing element. Let's go ahead and create the actual circle. So we're gonna be using this import here. I've given up on trying to pronounce it every time so just copy that from above.
Go outside of this div which has the start text, so outside like this. Render that and it's not gonna be a self-closing tag because it's gonna have children. So first let's pass in the value which is gonna check if number is nan on the percentage. Default it to zero otherwise use the percentage. Then let's go ahead and also pass in some styles here.
So styles are gonna look for the path. Let's go ahead and write a stroke here and let's use 4ADE80. And let's use trail stroke E5E7EB. And then inside of here let's go ahead and use the button component, which we've added an import for. And let's go ahead and give this a size of rounded.
So make sure that you have added this. Go inside of your button component and check if you have the size of rounded. There we go, rounded full. Then go back inside of the lesson button here and besides that give it a variant prop with a ternary. So if it is locked We're gonna go ahead and we have to create a new variant called locked so for now leave it like this and then add a secondary here.
And let's go ahead and give this a class name of fixed height 70 pixels and fixed width of 70 pixels as well and the border bottom of 8. Now let's go ahead and create an icon element here which is going to be a self-closing tag. So this icon already exists because this is a dynamic constant, which we have created from here. So let's go ahead and let me just see if I can, okay, this is not in hex. I thought this might be in a hex code, but it's not.
Okay, never mind. Let's go back to this icon and I want to give it a class name. Let's make it dynamic. So using CN, you should have CN imported from libutils. Use h10 with 10 and then if it is locked go ahead and write text neutral 400 stroke neutral 400 otherwise fill primary foreground and text primary foreground foreground and text primary foreground comma now let's check if is completed fill is going to be none and stroke is going to be four so not four pixels just four there we go Let's go ahead and save this now.
So now what I want to do is create the locked variant. So let's revisit our button component and let's go ahead and just above default let's create lock. Let's give it a BG neutral 200 then let's go ahead and give it text primary foreground hover BG neutral 200 slash 90 border neutral 400 border bottom 4 and active border bottom 0. There we go. So now we have our locked state right here.
And let's just go ahead and see where we... How do we get locked so we get it from above. Oh, so let's do it like this. I'm gonna remove this to do from here. I'm gonna bring this back to is current.
And I'm going to change the constant from here. So this is where I'm gonna write true. Or do the comparison. So this is the to do remove later. Right.
And then there we go. So this is how our first one is going to look like. Right. So let's try this. Let's go inside of our seed script here.
And in here where we define we have our lessons here so how about we create two lessons so give this an id of two keep the same unit id give it an order of two and give this verbs for example So I'm going to go ahead and do npm run database seed here. And let's see if that's going to make ours a little bit different here. So both of this should now be active. Yes, perhaps we've created a little bit of a problem here with our hard coding. But this is what I want you to see.
You can see how they have different icons now. So only the last one has the crown representing the end, right? Others will have a star. And you can see how this one moves with an offset. So let's go ahead and create a couple of these, right?
So I'm gonna go ahead and give this an ID of 3 and an order of 3. The title really doesn't matter. So let's go ahead and give this a 4, an order of 4. Then this is going to be 5. And let's see how that is the look, how that looks like.
I just want to create a full cycle. Perhaps we're going to need 8 for that. Let's just see. Oh 5 is enough. There we go.
1, 2, 3, 4, 5. So you can see how we have a nice first half of the cycle. So if I were to create 5 more, then now it would go in the other direction, right? So that's what I wanted to achieve. And now it looks funny because all of these have a start, right?
But we are going to change that later. So we can already go to the lesson button here. And we can, not to the lesson button, to unit, right here. And we can remove this to do remove later and go ahead and remove that right now. So now all of this should be locked I believe.
There we go. So all of these now have just this alternative something text So we have to create that as well. But this one is going to be easier. So for this one, what you can do is just copy the button. So go inside of this circular progress bar with children and copy the button component with the icon inside and all of the logic and go ahead and instead of rendering a div just render that button like that.
There we go. So now it looks like these have a completed status. I believe that simply because we don't have the proper fields that we are sending in here. So these would technically be locked, right? But they are not locked right now because we are lacking the logic, right?
So we are gonna go ahead and do that later and then they are going to be locked. If not, if we made some mistake, I'm gonna go ahead and make sure that we fix that. But there we go, we have created this logic now. So what we're going to do next is we're going to go ahead and create the logic to load the current active lesson, which we currently don't have. So right now if I go inside of my page here, you can see that the active lesson is undefined.
So we have to create a method which will load the current active lesson and its percentage and then we'll see if the other lessons are going to change their status or did we make a mistake in the logic. Great, great job!