In this chapter, we're going to develop the homepage. This will include creating the home layout, the homepage component, which consists of project form and project list. Let's go ahead and let's start our app. Running npm run dev in one terminal and starting ingest dev server in the other. And now let's go ahead and make sure we are on the main branch.
And you can click on synchronize changes just to make sure everything is up to date. Inside of your source control the last merge should be number 13. Now let's go ahead and let's fix one thing that's been bothering me. So right now I'm loading my previous project here. You can see that when the project loads no fragment is selected.
This is because in the messages container here we commented out this use effect which selects the last fragment because it was causing problems. You can see that when I enable this then it works, this is selected. But it's annoying because if I want to select this one and look at it you can see that it automatically moves it after five seconds. Why after five seconds? Because we refetch every five seconds.
So this use effect can obviously be improved. So let's go ahead and make it a little bit simpler. I'm gonna go ahead and remove everything in here for now and I'm gonna start by adding a new ref right below the bottom ref. Add last assistant message ID ref which can be a simple use ref of a type of string. And now, inside of this use effect here, let's go ahead and find the last assistant message.
So last assistant message will use messages, find last, and then simply find the message whose role is assistant. So messages.findLast() API is what we are using. And then in here we're going to open an if clause. And what we are going to do is we are going to check if lastAssistantMessage fragment exists and if lastAssistantMessage.id is not identical to lastAssistantMessageIdRef.current. Let me just remove this.
So if that's the case, only then are we going to call set active fragment and do last assistant message dot fragment. And then we have to update last assistant message ID ref dot current to be last assistant message dot ID. And this way we won't have any unnecessary updates and we can remove this to do here. So let's go ahead and do our refresh again and there we go you can see how it selects the fragment, the last assistant message it can find. But if I manually select this one, let's wait for five seconds.
And you can see that nothing will change it, right? Simply because this last assistant message ID ref is stored. So the only time that we are going to override user's selection is if an actual new message arrives. I think that's an okay UX. If you want to, you can improve this logic even further by creating two separate states.
One for the automatic selection of the active fragment and one for the user selection of the active fragment. And then you can overrule one over the other, if that's something you prefer. Because you can see now, nothing can change the fact that this fragment is active, unless I do build a yellow landing page. So if I add this, still nothing is happening. I'm still having this older fragment as selected, only after this finally responds with some new content will the new fragment be automatically selected.
Because it is constantly looking for the last new assistant message. So our message wasn't able to trigger that use effect. And if it simply calls the refetch request and it receives the exact same messages, we compare the last message ID with our ref ID and if it's the same we don't change anything and there we go you can see how it works I didn't change anything it's just generated a new landing page and it's selected that fragment That is the exact behavior we hoped for. Amazing. So what I want to do now, which you can choose if you want to or not, I just want to remove this handle from here.
I don't like it. So this is what I'm going to do. I'm going to open both the project view and I'm going to open the file explorer. And the only thing I want to do in the file explorer is copy the class name from the resize handle. And then I'm going to go inside of the project view.
I'm going to find the resizable handle here, remove the prop with handle and just paste the class name here. And there we go. Now I have this type of resizable. And there seems to be some kind of problem. You can see when you have two resizables active, You can only move one of them, right?
So you can't move this one. I'm going to explore at the end of the tutorial if that's something we can fix. There might be some solution, but you know, it's not too big of an issue. Great, so now let's go ahead and let's actually build the home page. So we're going to go and do the following.
Inside of your source app folder, create a new folder, home. This is a route group. This will not be a part of the URL, But it can hold things like layouts. So let's go ahead and build a simple home layout here. The first thing we have to do in a layout is create props which hold the children.
And then we have to do a default export, like this. And in here we assign the props and we extract the children. And then inside of here let's add, instead of div, let's add main. And let's give the main a class name of flex, flex column, minimum height of screen and the maximum height of screen, like so. And in here, let's add a div with a class name, flex1, flex, flexColumn, ex of 4 and paddingBottom of 4.
And inside, render the children. And then what I want you to do once you have this layout.tsx, it is important that this is called layout. This is a reserved file name, just like page, right? So it's important that you use layout and it's important that you do a default export here. What I want you to do now is I want you to move the page.tsx from the app folder, the global one, and drag it inside of the home folder.
So move it inside. And sometimes this can trigger some unsaved files. So if you get any unsaved files here, you can just close them. And if it asks you if you want to save it or not, you can just click yes. If nothing happened you can just continue.
What basically happens if that does happen to you is cache, right? The hot reload is currently active so sometimes the cache inside of this folder gets confused when you move a page that's currently active. So what did we do now? Well, if you go and click back to the dashboard, nothing changes, right? That's because what we just did is we created a layout for all of our home-based pages.
Right now, this doesn't make too much sense because we only have one page, the home page, right? But later in here, we're also going to have pricing and we're also going to have login forms. So that's why instead of copying this code every single time into each page, we're just going to create a nice little reusable layout, like so. Now in here, let's go ahead and let's do the following. I want to create a self-closing div, like so, and give it a class name of AbsoluteInset0-Z10 height of full, width of full, BG, background.
On dark, use BG radial-gradient like so, And then inside of here, write 393E4A underscore 1 pixel, comma, transparent, and then underscore one pixel. So this is all one class name. Dark, background, radial, gradient, transparent. So all of this is one class name. What's important is that when you hover over this, if you have the Tailwind extension, you should see the underlying CSS.
If you accidentally add space somewhere, that breaks the class. You can see how now it's not working. So just be careful. Don't add any spaces. I mean, this is not important, this is just for a cool effect you're gonna see in a second.
And now what I want you to do is I want you to copy this again, paste it, but without the dark prefix here. And you're going to change the color of this to not be this one but instead be dadde2 and this can still be transparent and then just add another one background dash size 16 pixels underscore 16 pixels And now you will see a bunch of dots all over your page. So now let's go ahead and let's actually develop this. So I'm gonna go back inside of my home page right here. And we're going to do the following.
I'm going to remove all of these things here because we're not going to need any of them. I'm going to, Well, I'm just going to clean the entire thing. I don't even need useClient here. I'm going to open a div, and I will add a class name here. Flex, flex-column, maximum width of 5xl, maximum width of auto, and width full.
I will then add a section with a class name space y6 py of 16 pixels my apologies 16 VH to Excel will be py 48. Now inside of here I will add a div with a class name flex, flex column and items center. In here we're going to render an image from next image with the source of logo.svg out of vibe, width of 50, height of 50, class name of hiddenMdBlock. Outside of this div, encapsulating that image, I will add an H1, build something with vibe or the name of your project. And we're going to give this heading class name Text2Excel, MediumText5Excel, FontBold, and TextCenter.
And you should already be seeing something here. Now below this heading, add a paragraph. Create apps and websites by chatting with AI. And give this a class name of text large, medium, text extra large, text muted foreground and TextCenter. There we go.
And now, below that, add a div with a class name, maximum width of 3xl, mxAuto and width full. And nothing will appear now, that's because we have to create a new component called Project Form. And we're going to call it, Excel MX Auto and with Full. And nothing will appear now, that's because we have to create a new component called Project Form. Now, the cool thing about Project Form is that you already built this, You just don't know it.
So what we're going to do is we're going to reuse one component that we already have. And we're going to go inside of modules, projects, UI, components. And in here, we have the message form. Now, technically, we could modify this message form with a prop. I could just pass a prop here, like is home page, is landing page, and then we can modify the CSS.
But honestly, I would rather keep components separate than creating this magical components, which can be used a million times. I'm OK with copying my code if it's for one, two, three instances. I would rather do that than creating this ambiguous abstract code that's impossible to keep track of. So this is what I'm going to do instead. I will copy that message form, and I'm going to go ahead inside of modules, and I will create Home module.
And inside of here, UI, and then Components. And then in here, I will create ProjectForm.tsx. And then I will copy everything inside of the project's UI components message form and I will paste it here like this and then I will remove the props because we don't need them And this will now be called project form. There will be no props for this. The value will still be the same, but it will not be creating a message.
It will be creating the project. So this will be called create project. So let's go ahead and see what we have to do. We will reset the form. Actually, we don't have to reset the form.
And I'll tell you why. Because on success we're going to re-invalidate anyway. So let's go ahead and do this. After we reset the form, the only thing we should actually, I'm sorry, after we successfully create a project, the only thing we should do is we should call prpc.projects and we should just refetch getMany. That's the only thing that should happen.
And then also we should invalidate the usage status. So we can leave this to do and the same thing for this. But also one more thing that should happen here is that we add router, use router from next navigation. Let me just move this here. When you successfully do this, let's do router.push and we push to the newly created project.
So that's going to be forward slash projects data ID. As always, we have this data because in the project's create procedure here when we create the new project and then we invoke a background job we return that new created project so we have access to it right here. Great! So on submit we'll be calling create project and we don't need the project ID here at all. For the isPending, we will have createProject isPending, like so.
We can remove the showUsage from here entirely. We don't need it on the home page. So you can remove this showUsage here, like so. What would you like to build can stay the same, to submit, to submit. Honestly, I think everything else here works just fine.
So yes, just a slight modification here. And now let's go ahead and use it inside of our app home page.tsx. Let's import project form from modules UI components project form. And I think we also need to add use client here because it's imported in a server component so it wouldn't work and there we go this is how it's going to look like and you can already try it so build a landing page I like to use this example because I think it's super simple and works almost every time. And there we go, you can see what happens.
So from the landing page, we create a new project with build a landing page initial message. Perfect, so if you want to, you can wait for the result. I know it's very fun to always see the results, I completely understand if you want to, but I'm going to go back to the project form. And what I'm going to do now is I'm going to show you how you can create some predefined prompts for the users so that they can easily click on them here so that they can see the results faster. So this is what we're going to do.
We're going to do this inside of the project form. So in here, go outside of this native form element and create a div with a class name flex-vrap-justify-center-gap-to-hidden-md-flex and a maximum width of 3xl. And now in here what you should do is you should create something called project templates. So you can go inside of the public assets folder, which you can see the link for on the screen, or you can use the link in the description. And in here, you can find constants.cs.
And in here, I just created a bunch of project templates for you. And you're going to have to test each of these out depending on the model you will use, and what works for you and what doesn't. Because it's a good idea to showcase your project templates on something that you know will always work with your AI model. Right, so I'm going to put this inside of home. I will create new constants.ts and I will paste that here.
So basically something like build a Spotify clone, build an Airbnb clone, build a store page, YouTube clone, file manager, and I'm just using very descriptive prompts here because it will work better if you give it a good description. But the cool thing is that you have full freedom to improve the prompt in any way. In here, when I select build a Netflix clone, the full prompt will be build a Netflix style homepage with a hero banner, use a nice dark mode compatible gradient here, movie sections, responsive card, and a model for viewing details using mock data in local state use dark mode right so it's a very descriptive prompt but depending on what model you use you might be able to do it with just build a netflix clone right it will just depend on the prompt that you're using and the model that you're using. For example, Claude Sonnet understands your instructions very, very well. But with OpenAI, I sometimes have to tell it, if you're using dark mode, make sure you use Next Themes because you have ShadCN installed.
I have to tell it more in-depth about what's going on. So make sure you have this project templates. And now what you're going to do is you're going to iterate over them. So project templates, which I've just imported from ./.constants here, .map, and for each template, I'm going to return a Button component. I'm going to give the button a key of template.title.
And then I'm going to add some additional attributes to the buttons. So variant of each will be outline. Size will be small. Class name will be background-white and dark background-sidebar. OnClick here.
OnSelect will be called, which we don't have yet, so let's just leave it as empty and then let's put template emoji, and let's put template title. And let's see that now. And there we go. So you can see that now, beneath this big input bar, you can select any of these. So let's go ahead now and just properly space these things out.
So what I want to do is I want to wrap my form instead of a section with a class name space y6 like so And just encapsulate all the way to here. Like that. And then you can indent the entire thing. And now you have a nice spacing between. And now we have to create the ability to actually select this.
So for this, I'm going to add const on select, content string, form set value, content, or whatever you use, let's see. So we use value. Value is the one we use. So set value to be content or you know you can just put value here, a lot of value. And what's important you do is you enable all three, should dirty to true, should validate to true, and should touch to true.
This will basically simulate it to be in the same state as if the user actually typed this. So now what you have to do is you have to add the onSelect to the buttons. So call onSelect and pass in the template.prompt. Like so. So now when you click on build an admin dashboard, there we go.
You can go ahead and run this. So I suggest that you try running this And also keep in mind, some of these are larger tasks. So they might actually time out. So be mindful of that. The good thing about ingest is that if it notices a rate limit, it won't retry immediately.
It will retry with exponentially longer pauses between each retry, which if you're using OpenAI is perfect because OpenAI has reasonable timeouts. So when you hit a limit in OpenAI, they punish you with like two seconds of waiting time. So Ingest will wait for even longer than that. And if it happens again, it will wait for even longer. So you don't have to worry.
Ingest and OPA and I are quite a good combination. And you can see that with this longer prompt, right where I told it, let me just see, create an admin dashboard with stat cards, placeholder, all of those things, blah, blah, blah. In here, it takes a bit of a longer time. You can see almost a minute, but as I said, you can speed these things up by using a different model. You can create a smoother prompt, right?
A lot of things you can do. So let's just see this result. I'm very curious if it will work or not. And there we go. So almost the exact same thing as we saw in the initial demo.
Amazing. And you can see the code here. Very, very good. So I would suggest that you you know, try a couple of these. And if some are obviously failing, well, you can try and you know, fix them in the prompt.
Or you can simply replace them with something simpler. Because if you're actually building this as a business, it's a good idea that you allow the user to select something that will 100% work. You don't want to give them something that might work or might fail. Perfect. And I'm just super interested.
Let me just go back here. I want to change this to dark mode. I want to see how this looks like. Looks pretty good. Great.
But I actually prefer working in light mode. So let's go ahead now And let's develop the bottom part, which is the project list. So, so far we created the project form and the layout. Now let's create the project list. In order to do that, we have to go back to our page.dsx, where we render the project form.
And we have to render the project list outside of this section so project projects list like this and then let's go inside of our home modules here so home UI components projects list dot TSX let's mark this as use client And let's import everything we need here. So link from next link an image from next image, format distance to now from date FNS, use query from 10 stack React query, and use the DRPC from DRPC client and button from components UI button. Let's export const projects list here. And let's start by defining TRPC. Then let's define data projects to be used query.
TRPC projects get many query options. Like that. And then in here, let's return a div with a class name, full width, background color of white, dark background color sidebar, rounded extra large, adding 8, border, flex, flex column, gap y 6, sm gap y 4. Then let's add an h2 element, which will just say previous vibes or saved vibes. I thought it would be fun to call all projects vibes because the project name is vibe, right?
You can of course just say old project, saved projects, whatever you want. So text to Excel and font semi-bold. Later this will say Antonio's vibes or whoever is logged in, but since we don't have out yet, we can't display that just yet. So now let's just import the projects list simply so we can start seeing the progress. So right here at the bottom, you should see saved vibes right here.
It should look like this. So now let's go ahead below this, and let's create a div with a class name of grid, grid columns 1, sm grid columns 3, and gap of 6. And then in here, check if projects.length, this should be a question mark. So if projects.length is equal to zero, in that case, let's display a div with a class name, all span, full and text center. And inside a paragraph, no projects found.
And a class name, text small and text muted, foreground. Otherwise, Let's do projects.map, get the individual project here, and then return a button. Give this button a key of project ID, a variant of outline, and a class name, font normal, height auto, justify start, full width, text start, and padding of 4, and give it an as child prop then go ahead and add a link here with a dynamic href forward slash projects project dot ID. And then inside of here, create a div with a class name, flex item center and gap x of 4. Then add an image here with a source of logo SVG, alt of vibe, width of 32, height of 32 and the class name object contain.
Below the image add a new div with a class name flex and flex column. Inside of that div an h3 element with project.name inside and give the h3 element a class name of truncate and font medium and below it a paragraph using format distance to now which we imported from date fns, project updated at, add suffix true, and give the paragraph a class name text small and text muted foreground. And that is it. So in here now you can see all of your previous vibes. So you can go ahead and visit them.
And in here the source code is of course preserved. Great! So I believe that that marks the end of this chapter where the goal was to build a landing page and we added the templates, we added the project list, we added the ability to look at these older projects, and I think we did an amazing amazing job here. Obviously there are some things still missing like the navbar but we will do that later when we add authentication. So what I want to do in the next chapter is actually want to improve the theme of this project because my original theme in the demo was some kind of a yellowish color.
So I'm going to show you how I modify the theme to make it look like that. And I'm super interested in the result of this. I'm just going to wait. Hopefully, it will work. If not, it's just a lesson that these AI models are a bit undeterministic.
You can't really rely on them too much. But if you spend, you know, more than, I built this app in a span of a month, right? So I couldn't really spend too much time learning proper prompt engineering. But if you actually use this for your business, you are most certainly going to spend a lot of time on this, and you will learn prompt engineering, and you will learn how to improve the prompt, and how to fix these little mistakes. Because in comparison to what you've just built, an app failing is really not a big issue.
You can learn how to speed it up, you can use a new model, you can spend more credits, You can basically do a billion solutions, but the boilerplate is here and it's working. So for example, you can see that I've gotten an error for this file manager. You might not get an error. Again, it's a very simple fix. It forgot to add useClient to the top of the file.
We can see that in the file grid it was supposed to add useClient but it didn't, right? Or it should have added it to the page. So perhaps this can be a very, very easy fix, you know? You can maybe tell it inside of the prompt right here, you can somewhere add a rule that it must add useClient. How about this?
Let's add always add useClient to the top of page TSX. So because we are not expecting this to make any API calls, right? So then I can maybe remove this, and I can just extend it and any other relevant files which use browser APIs or React hooks. Use effect. Okay, I won't add too many tokens now.
But for example, you can do things like this. And I think that already, this should work much, much better. And I purposely want to retry it now just to see if that will fix. I'm trying to teach you that, you know, you didn't have to use this prompt. You can make your own prompt.
Like I built this prompt and I have no idea about prompt engineering. I just started very simple and then I extended and I extended and I extended. Right. So I just added this file safety rule to always add useClient at the top of page.tsx, simply because if it does that, it doesn't have to worry about adding it to the other places. So let's see if this will fix the problem, or maybe some new problem will arrive.
And finally, I managed to get it to work. So this was very funny. It actually failed again, right? It forgot to add useClient again. But look at this.
It added it, but it didn't add it at the top of the file. So you can see how funny these AI models are. Sometimes you will lose your mind trying to tell it to do something, right? So this is what I did. I modified this, always add useClient to the top, the first line of app page TSX.
So this way it understood me. And it did an interesting thing this time. You can see that it understood what I wanted now. And also, it decided to create a whole new separate file where it created everything. I'm not sure why it needed to do that.
But let's see what it created, because I think that this is very interesting, actually. Can I rename this? OK, I can't do that. Oh, I can. 1, 2, 3, save.
Oh, it works. I can rename. I can delete. Or can I? I can.
This is actually super impressive. Can I delete entire folders? Looks like something's wrong with the models. Keep in mind that sometimes the problems aren't in code, but the problems are in the iframe. Sometimes you might have to visit a live example.
Wow, this is actually a very, very nice example of a file manager. But yeah, you can see that I had to struggle a bit with this, right? I got a very good result in the end, but you know, the prompt can always be better. Again, I'm not a prompt engineer. I have no idea what I'm doing when it comes to prompt engineering.
So spend some time learning that and you will get even better results than what I am in this tutorial. But I still managed to get extremely impressive results. Great. So I believe that that marks the end of this chapter now. So 14, home page.
Let's go ahead and close everything here. And I will go and create a new branch 14 home page. Like so. I'm going to stage all of my changes and I will create a commit 14 home page. I will commit and I will publish my branch.
Perfect. Now let's go ahead and open a new pull request here and let's create a pull request And let's wait for the summary to arrive. And here we have the summary. We introduced a new home page layout with a visually enhanced background and responsive design. We added project creation form with template section, validation, and keyboard shortcut support.
Basically, a copy of our message form, right? We implemented project list view showing saved projects with quick navigation and relative timestamps. We provided a set of predefined project templates for faster project setup. We also fixed the fragment handling to prevent repeated state updates exactly. And we also updated the resizable handle styling for a smoother and more interactive user experience.
And it also detected our prompt change where we clarified the requirement for the use client directive in relevant files. Excellent! So in here as always we have a more in-depth walkthrough. In here we have a sequence diagram explaining exactly how all of those things happen. And in here we have some comments.
So it suggests adding some loading states here in the project list. We could very much do that. We could even leverage our prefetching and suspend. We'll see how we're going to handle that later. And in here it suggests also adding is dirty check.
I'm not sure if we need that. I think I completed the project without it so I think we don't need this. So I'm going to merge this pull request here and after I've done that I'm going to go back inside of the main branch and I'm going to click on synchronize changes and after that my graph here will update and it will show me that pull request 14 was just merged. Amazing, amazing job! I believe that marks the end of this chapter and see you in the next one.