So now that we have our subscription it's time to fill all of our to-do lists. So first things first that I've noticed. In the shop page I have unlimited hearts, but in the learn page I don't have unlimited hearts. So let's go ahead and resolve that. I'm going to go inside of my app folder main learn page.tsx and in here I am not calling the active subscription.
So let's go ahead and go inside of the learnpage.vsx here. You should be here, right? And then what we are going to do is we're going to fetch the user subscription data using getUserSubscription. So make sure you've added an import here for that and then we're going to add that here last in the list and add the user subscription as the promise result. So now we have user subscription here and then let's go ahead and add this here so double exclamation points question mark is active and that should resolve my inconsistency between the hearts.
There we go. So now it's infinite here and it is infinite here. Great. The other thing that I'm pretty sure is missing is the infinite hearts in here. So in here they are still five.
And if I select an incorrect option I am losing hearts here, right? So we have to fix that on both the back end and the front end so we don't lose any hearts. So first things first, let's go inside of the lesson folder and let's go inside of page.tsx. And in here, there we go, we have a to do here, which says that I need to add user subscription. So let's go and add user subscription data.
Get user subscription. Make sure you've added an import right here from database queries. User subscription data. And user subscription is here. There we go.
And now we can very simply just pass in the user subscription and we can remove this to do right here. And there we go. So now you can see that this is infinite and there seems to be a little issue here. When I go into mobile mode it's not really visible. So let me go inside of the header component here.
Where is it? Right here, header component. And let me try and give this infinity icon a shrink of zero. There we go. So now it doesn't get smaller.
Perfect. So now we have the infinity tag, but I believe this still doesn't mean on the backend that it's working properly. Let's go ahead and ensure that. So inside of page lesson, we now pass this to the quiz and let's see where do we use this. Oh, we have it to do here as well.
So let's do that. Type of user subscription from database schema dot infer select. There we go. Make sure you have imported user subscription from database schema. So now my lesson seems to be having an error here.
And that is because the user subscription can be... Oh yeah, so this is the problem. Let's get inside of Quiz. So my GetUserSubscription here returns a little modified version of that. So it returns the data but also is active.
So we have to account for that as well. So let's open this up. Add is active, which is a Boolean. And let's make this whole thing optional. There we go.
Now there is no errors in our lesson page when passing the user subscription here. So let's go inside of here and see all the places that the user subscription is actually used. So in the header and that seems to be it. I just want to see if that is the entirety of it. Looks like it is.
And now let's just go inside of my actions here. So in here we have the challenge progress. And there we go. To do not if user has a subscription. So Let's go inside of the challenge progress here.
Let's go inside of the first one, absurd challenge progress. And just below the current user progress, I want to add the user subscription, await get user subscription and import this to do. So just make sure you've added the query. You now have the user subscription here. And then in here where we have our practice, let's remove this to do.
And if this is not a practice and if we don't have an active user subscription. So let me collapse these elements. There we go. So we are only going to return the error for not enough hearts if the user has zero hearts and this is not a practice and the user doesn't have an active subscription. So if this returns true, in that case, this error will not be thrown.
But if this is false and this is not practiced and it's zero, then an error will be thrown. Great, so we now have that. And let's go ahead and just confirm is this the only place where I need that I think that it is right I don't need anything else here Now let's go ahead and let's go inside of my user progress here. So inside of the user progress I believe... Oh, so this is in my upstart user progress.
How about I close that and go inside of the reduce hearts instead. So let's go do the reduce hearts instead. So in here, I'm going to go ahead and do the same thing. I'm going to remove the to do. I'm going to get the user subscription using await get user subscription query.
So again, make sure you've added getUserSubscription. And then in here, I'm gonna go ahead, and first we get the challenge, oh wait, where am I? Reduce hearts. All right, so in here, we check for the user ID. Where is my isPractice?
There we go, isPractice. So if there is a practice, we return practice. If there is user progress, we return not found. And then I have to handle this before I check the hearts. So if user subscription is active, in that case I'm going to return an error subscription and that will prevent the reduction of hearts from going on, right?
So we won't do this part at all. Perfect, so we have that. And while we are here, it looks like we already have some to do here. I'm talking about this one, upstart user progress. So to do enable once units and lessons are added so I think we can do that right course that units get course by id so I think that I have to go inside of this getCourseById method and in here we have a to do to populate with unit and lessons.
So let's add with. So this is getCourseById inside of my queries, right? Here it is, getCourseById inside of database queries. So let's add with units, order by units, ascending, ascending units order, with lessons, order by lessons, ascending and let's do ascending lessons dot order. There we go.
And then we can go inside of here and we can remove this to do. So this is back inside of the absurd user progress in the user progress action. So if there are no course units length or no lessons in the unit, we're going to return an error that course is empty. So I think you can already try this out. So if I refresh this now, and if I mistake something for a robot, there we go.
I don't think my hearts are reduced. So no matter how many times I try and do this, I've definitely done this enough times to be shown an error. But since I am an infinite user, I'm a pro member, I can do this as many times and I can still answer correct answers because I have a pro model. Perfect! So now that we have this, let me go ahead and see are there any places where I have left my to-do.
There we go inside of lesson ID. Oof, yes! So in a repeating practice is the place I've missed it. There we go. So in here it still says that I have four hearts.
So let's just go ahead and resolve that. So I'm going to add const user subscription data to be get user subscription from database queries import. Let's add the user subscription here. And user subscription data is going to be a promise all. And then I can pass that in here and I can remove this to do.
There we go. And now there we go. Now my practice lesson shows my infinite hearts as well. So just make sure you have added the get user subscription import here. Let's see something else here.
So this to do user ID in the challenge progress seems to not have broken my app, so I can remove that. Let's see, what about this to do? This seems to be working fine. This seems to be working fine this query is right here I guess we could order them yes we should definitely order them so this is what I want you to do I want you to go into get units, which is in the queries right here. And in here, we're going to go ahead and we're going to do our order by.
So we're going to use lessons and we are going to extract ascending and we are going to return ascending lessons.order right so that's what we're going to do and then we're going to do the same thing with challenges right challenges.order exists, I believe. There we go. So I think that is fine and units need that as well, right? Units, units.order, like that. Let's try, let's quickly try this out if everything is still working.
It seems to be working just fine, but we can easily confirm by going inside of the terminal. Let's close the database studio one and let's do npm run database seed. So that's going to add our new, it's going to reset everything Basically. Let's just confirm that the first lesson in the first unit is going to be working. Still.
If I click here, there we go. This is exactly what we had previously. So the order is still working. That's what I wanted to ensure that the order won't mess things up the reason it was working previously is because it was ordering them by IDs right and inside of our seed script the IDs are 1 2 3 4 5 right there they are incremental So technically you can still rely on IDs, but if you want to, you can also rely on order and I think this is a much better way of doing this, right? Perfect.
I think this should be fine, right? So units find many order, lessons, we're ordering the lessons, challenges, right? That's how you do that, I think. So we can remove this to do here. And I just want to find one order by, so I can ensure that one that is not inside of get lessons.
So this one, challenges, challenges, ascending. Yes, this is how you do it. Great, so to do what's left, move alongside item components into a constant file. We can do that. So in the root of our application, let's create constants.ts and let's go ahead and find this to do that I have written here.
Let's add it here. Let's export const points to refill. Let's remove this from here. Let's go all the way down and let's import this from add slash constants. There we go.
Let me move that here and we have one more place where we have to do this. Not in the constant, not in the user progress, but in the main shop. So we remove this and instead we import the ones from Constance. There we go. Perfect.
So we just filled that up. Now what I want to do is I want to ensure there are no more to-dos in our app. Great. Now let's go ahead and let's create the leaderboard which is currently a 404 page. So all of these things are going to be quite static until we get to the admin dashboard.
So let's go inside of main and in here, I'm gonna go ahead and copy the shop because it's quite similar and let's call it a leaderboard. And that should, there we go. Now inside of page here, let's rename this shop page to leaderboard page. There we go. And let's go ahead and let's actually style this, right?
So I'm gonna go ahead and do the following. So user progress can stay the same as usual. And the feed wrapper in here, we are not going to use the shop. Instead, we're going to use the leaderboard. And leaderboard is going to be the alt.
And then the h1 element is going to be leaderboard and the paragraph is going to say see where you stand among other learners in the community like that there we go perfect And now what I want to do is I want to remove the items here and just add to do add user list. So there we go, now it's empty. Let's remove the items. Let's delete the items from this leaderboard folder. Don't accidentally delete the ones in shop, right?
So we need those. User subscription data is needed here, but there is one more thing that we need, which is a method to get the top 10 users. So let's go inside of the database, let's go inside of queries here, let's go all the way to the bottom and let's export const getTop10Users to be cache and let's make sure this is an asynchronous method which will return the data by using await database query, user progress, find many, order by user progress, So let's go ahead and use that on user progress.points. And let's go ahead and limit this by 10 columns. So columns are the equivalent of select in Prisma.
We need the user ID, the username, user image source, and their points. That's what we need. And return data. There we go. Perfect.
And let's also, let me also do this. So I wanna fast see if we are authenticated and if we are not, I'm gonna return an empty array. So I just wanna ensure this query the same way I would ensure an API route. There we go. And now let's go ahead back inside of our app folder, main folder, leaderboard, page.tsx and in here let's go ahead and let's get top 10 users data to be get top 10 users and then we will have top 10 users matching the top 10 users data promise like that.
Or we can maybe call this leaderboard data and then call this leaderboard. There we go. And now I want us to go ahead in here, in this to-do list and let's go over leaderboard.map. Let's get the individual user progress and index. And let's go ahead and return a div.
Or we can also return like an immediate return. So instead of opening a function here, you can open an immediate return and return a div here with a key, user progress that user ID. And inside you can render user progress dot username there we go so this is my dummy name account perfect now I want to add a couple of things from Shazian UI. So let's add npx Shazian UI latest add avatar. That's the one we need.
And we also need a separator. So let's also go ahead and add a separator component. There we go. So let's add a separator just from here, from components UI separator, just above the leaderboard. So make sure you've added this import here from components UI.
Give this separator a class name of MarginBottom4, height of 0.5, and roundedPool. And now inside of here, let's go ahead and style this a bit better. So besides key, this div is gonna have a class name of flex item center, pool width, padding of two, PX of four, rounded of extra large, hover, BG gray 200 slash 50. Like that. And then inside of here, I'm gonna go ahead and write a paragraph for this, which will say index plus one.
So we're representing the position of the user. And let's give this paragraph a class name. Font, bold, text, lime, 700 and MR of four. And then I'm gonna use an avatar component from components UI avatar. So make sure you have added avatar and avatar image from components UI avatar.
And inside of here, I'm gonna use an avatar image here. I'm gonna pass a source to be userprogress.userimageSource. Let's go ahead and give our avatar a class name of Border, BGGreen500, height of 12, width of 12, and ML of three, and MR of six. Image let's give it a class name of object cover and then I want to go ahead outside of the avatar and I want to add a paragraph user progress dot user name to render our name and another paragraph saying user progress.points XP. Like that, there we go.
Let's give this class name a Text of muted foreground. Let's give this one a class name of font bold and text neutral 800 and flex of one like that. And I think that should be it. There we go. So now we have my user right here.
I have 30 points and this is my name and other users will appear below. Perfect! So we now have a functioning leaderboard. Great! And now let's go ahead and let's create the quests page.
So quests page will be quite similar so we can copy the leaderboard and create the quests page. So let's go inside of the quests page right here. Let's rename this requests page. And in here we are not going to need to load the leaderboard data. So we can remove that from our promise all and we can remove the get top 10 users from here.
We don't need that. We just need the user progress and the user subscription in here. So make sure you have those. This can stay the same. The isPro can stay the same.
We are passing that to the user progress. That is good. And now in here, I'm going to use the image of quests and alt is going to be quests. And the text is going to be quests. And the description in here will say complete the quests by earning points.
There we go. And I'm going to remove the separator. I'm going to remove this entire thing here. I'm going to simply add to do add quests. So this is our dummy quests page we don't need the separator or the avatar import so let's go to the quests page now it should no longer be a 404 instead it should be this initial text which we have just written here let me just see something so we use a dot here How about I add a dot in the quests page here as well.
And now let's go ahead and let's write some quests. So I'm gonna go ahead and write that here. Const quests is gonna be an array of objects. Title is gonna be earn 20 XP. Value for that is going to be 20 and then we can just go ahead and copy and paste this makes this earn 50 XP value is 50 then it gets a bit harder with 100 XP, and then with 500 XP, for example, right?
Something increasingly more difficult. And after that, we can do one last one of a thousand. You can of course make this dynamic later. And now let's go ahead and let's just make these quests. So inside, so just below this paragraph here, I'm gonna open an unordered list with a class name of full width.
And I'm going to go ahead and write quests.map. I'm going to get the individual quest. So quests is the name of my constant here, right? So in here, first things first, I open the function on purpose because I want to calculate the progress by using the user progress points and divide them by the value of each quest times 100 so I get the percentage. And then I can return a div, I can give my div a key and it can simply be quest.title and let's give this a class name of flex-items-center full width padding of 4, gap x 4 and border top of 2.
Let's go ahead and let's use an image component which we already have imported. Otherwise it's from next slash image. Let's give it a source of slash points.svg, an alt of points, a width of 60 and a height of 60. There we go. So we are reusing our points image, which we are already using throughout the app.
Outside of this image, let's create a div with a class name flex-col gap-width and full-width. Inside of it, I'm going to create a paragraph quest.title and a class name of TextNeutral700, TextExtraLarge and FontBold. There we go. And then let's add a progress component from component UI progress. We already have that.
We use it in the header of our quiz component, right? So let's give this a value of progress, which we define in each iteration of quest.map. And let's give it a class name of height of three. And there we go. Perfect.
Except the first one, which doesn't seem to be working for some reason. Not sure why. Earn 20 XP. What if I write 10? If I refresh, does it work now?
Oh, it seems to be just some weird cache bug, maybe? I just returned it to 20 and it seems to be just fine, right? I don't know, I guess it is fine. If I made a mistake in the progress here, let me go ahead and just console.log the progress in each of these And let's also get the value of quest.value so I know exactly what's going on. And this is a server component so it's going to be logged here in this one.
So progress is 150. Is that a problem for our progress component I think it can accept like if I write 99 here that's almost finished if I write 100 that's finished If I go 150 that's still finished. So yeah this should work just fine. Great so that's it for our quests component. So that wraps up the shop, we have quests, leaderboard, we have learned.
One thing that I want now is some more items here in the sidebar. One of them will include a promotional component to upgrade to pro. So let's go ahead and let's do the following. Let's go back inside of our scripts seed and let's also add a deletion of user subscriptions right for our seed script. Let me just see where is this button going so that's okay that Those errors are for some deleted file.
So let me close my npm run dev here and let me do npm run database seed. And now Let me go ahead and do npm run dev again. So I could have done that here, but fine. And now I should no longer be having any progress, any points, nor should I be having any subscription. So I'm gonna select Spanish here.
And there we go. I am back at the beginning. Perfect. And now I want to create some elements here on the side. So first let's do that in this learn page right here.
So I want to go back inside of my app folder, main learn page.dsx and below the sticky wrapper user progress we're going to add a promo component. So we are going to get an error right now. So let's go inside of components here and create a new file promo.dsx. So this is going to be reusable. So this is where you put it.
And now let's mark this promo as use client and let's export const promo. And let's return a div. There we go. Now let's go ahead and import this from components promo. So let me just move it right here.
There we go. And now you should no longer be having an error for the promo component. So in order to see what you're developing, you have to zoom out until you can see that little fixed sticky sidebar that we have. Let's go ahead and style this. So I'm going to go ahead and give this div a class name of Border2RoundedExtraLarge, padding of 4 and space Y4.
And inside of here, I'm going to create another div with a class name of space y2 then another div with a class name of flex-item-center-gap-x2 and in here I'm going to import an image from next slash image so make sure you add that with a source of unlimited.svg So I'm going to reuse our unlimited hearts icon. Let's call this a pro. And let's go ahead and give this a height of 26 and a width of 26 here. Like that. And outside of this div encapsulating the image, my apologies, inside of this div, alongside image, add an h3 element, upgrade to pro.
And let's give this a class name of font bold and text large and then below that outside of this div what I started to do was a paragraph which will say get unlimited hearts and more. Let's give this a class name of text muted foreground. And then outside of that div, we're going to add a button from .slash UI button or components UI button. In here, we're going to say upgrade today. Let's give this a variant of super so we have that we do have a super variant right I just want to confirm that's not something I have or you can use any other variant that you like.
There we go. We do have a super variant. So use the variant super. Let's give it a class name of full width, a size of large, like that. And we can make this entire thing be a link.
So let me just wrap that in a link, which will just go to shop. So import link from next link. There we go. Oh, but now we lost the styles. So let's put the link inside of the button instead like this.
And then just align this. How do I do this? Oh, okay. And now we have to also add as child prop here. There we go.
So now when I click here, I'm redirected to shop. Perfect. And we only want to show this if we are not subscribed, right? So I'm gonna go ahead and do the following. I'm going to use if, so we can now, it would be quite useful for us to do that little promo pro thing so let me write const is pro So I can do that here now.
IsPro and then in here if it's not Pro only then show the promo. Like that. Perfect. And now I'm getting this weird TypeScript error. This will go away on refresh here.
So if I go ahead and copy this and I want to add it now to all my other pages so that is quests so let me go here in the quests here we already have is pro and I will just add it below here and import it from here, from components promo. And then I will do the same thing for the leaderboard. So find my sticky wrapper and add it here. If you want to, you know, you can explore ways to reuse this so you don't have to reuse the sticky wrapper and feed wrapper all the time. But I found it that it's useful to do it this way when so you can pick and choose on what pages do you want to show the promotion and where you don't want to show it right so it just gives you some more control.
So we have leaderboard, learn and we should also do it in quests. We did that and also in the shop page. That's also somewhere where we could display that because why not. So just make sure in all of these pages you have added the promo from components promo, right? And now they should be everywhere.
There we go. So it's in the learn page. It is in the leaderboard page right here. It is in the quests and it is in the shop. And one more thing that I want to do is I also want to create a little sidebar item for my reusable quests.
So for the quests I want to go ahead and close everything, go inside of my components, find the promo and paste it here and replace this with quests.tsx export const quests from here and this actually none of these have to be used client yeah no need for promo to be used client. No need to break the boundary. So for the quests here, let's go inside the learn page and let's add them below the promo. So quests from components, quests, don't accidentally import the page, right? We are importing this reusable component which we can use.
So let me just expand this. There we go. So now you should have two identical components here. So let's go inside of quests here and we're going to change this a bit. So this is not going to have anything like this here.
So let me just remove everything besides the main div here. And let me open a div with an h3, which will say quests. Let's give the h3 element a class name of font bold and text large. And besides them I want a link component which will take me to the quests page and it will simply render a button which will say view all. The button will have a size of a small and a variant of primary outline.
So right now these two are below another so I'm going to use the outer div encapsulating those to turn into a flex item center and it's going to be justified between for them let's also ensure that taking the full width and space y2 there we go perfect So now we have that and now what I want to do is I want to make sure this points can accept the points here so which can be a type of number type props is going to be points, which are a type of number, props, all right. And now I wanna go inside of here, of the learn page where we render this and passing the points from user progress dot points like that so this is the learn page right learn folder page and now I want to go inside of quests and I want to revisit my old, where is it? My quests page, because I want to copy the quests. So perhaps we can move them to constants. Let's do that.
Let's move the quests in the constants file so we can reuse them. There we go. So they are here now. So I can remove them from here. And then I can use them here by importing them from the constants.
There we go. Import quests from constants. Perfect. So now I'm going back into my reusable components quests and I will add an import for that. And now I can go ahead and iterate over them.
So let me go ahead and this is fine. Let me just go, where do I want to go? Just below here. And I wanna add a UL with a class name of full width and space width of 4. Let's do quest.map.
Let's get the individual quest. And Let me just copy what I already have in the quests page so we can speed up the process. So we don't need no console logging anymore, but I could copy the entire return here. There we go. And I'm just working with points here because in here it's just a prop.
But I think by default oh yeah I'm missing the progress import so let's import this from ./.ui-progress and let me switch it to components UI progress and I think well it actually looks fine. If you like it like this, you can leave it like this, but perhaps I will modify it just a tiny bit here. Let me see. I will remove the border top. I don't think that is needed.
Yeah, or maybe it looks better with it. You decide for yourself. You can style this separately if you want to. So let's go ahead and play around with this now, right? Oh yeah, and let's just add the quests somewhere.
Oh yeah and let me change this. Yeah let me reduce the height and width of this to 40 and change the text extra large to be text small in here. This can be fine. Gap X. Let me change this to Gap X 3 and let me remove the border.
So I do want it to have just a little bit of a different style and change the height of the progress to height 2. So Now they look a bit smaller, right? I think that is fine. And instead of padding 4, I just want to use padding bottom 4. There we go.
Now they look smooth. Perfect. And now, inside of the learn page, let me see how I use quests. So very simply, I just add them below the promo. Now let's do the same thing for the quests.
Actually in the quests, we don't have to do it because we are there, right? So we have to do it in the shop. So below this, I added import quests from components quests, make sure you've added that. And besides shop, we also need this inside of my leaderboard page. So below the promo, we are adding the quests.
So let's import that from components, quests, and there we go. So if I click on view all, it redirects me to the quests where I don't have the same information repeated to me in the sticky wrapper. So that's why I didn't create a reusable one. So you can pick and choose when you want to add things. And if you want to, you know, create some different promotions on different tabs, right?
Because that's what the original Duolingo app does. There we go. So we can now see quests on all important websites here. Let's also do one more thing. Let's copy the loading page from the app folder main learn.
So we have loading here. Let's paste it inside. We have it in the courses. Let's paste it in the leaderboard. Ignore these errors, they're going to resolve this and see.
Put it in quests and put it in shop here. And there we go. Now all of them should have nice little loading states and not just the learn page. Perfect. So let's try and play our game now.
So if I go ahead and get some new points here, I should be having some... I should be having some changes in the sidebar. So I wanna lose some points on purpose now. So let me break this and there we go. So you can see how much progress I've made by earning 20 pixels, 50 pixels, 100, 500, 1000, right?
So I'm not close to finishing these ones. And if I go into a shop, for example, if I refill my hearts now, there we go, you can see how everything gets revalidated. And now let's go ahead and see if our promo will leave once I upgrade. So for this you have to ensure that you have your webhook running otherwise it's not going to work so make sure you have this running the stripe listen forward to command. Let's go ahead and upgrade this.
I'm gonna use the dummy information that I have. My dummy name here, let's pay and subscribe. And this should create a completely new, there we go, 200 everything seems to be working just fine. I'm redirected back and there we go I have infinite hearts and my promo is no longer displayed anywhere. Perfect!
So I'm pretty sure we've wrapped up everything important needed for here. Also, if you try and choose any other language, I think now you get an error. That's right, because we hover that because in the seed script, there are no lessons for that. So we kind of cover that edge case. What we're going to do now is we're going to build the admin dashboard and then I'm gonna show you a seed script that you can copy and paste from my app to test out whether there are any outstanding bugs in your application.
But that is pretty much it. You finished the entire app. Great, great job!