
No Comments Yet
Be the first to share your thoughts and start the conversation.
Be the first to share your thoughts and start the conversation.
Complete source code available till this point of lesson is available at
By logging in, you'll unlock full access to this and other free tutorials on JSM Pro.
Why? Logging in lets us personalize your learning experience, track your progress, and keep you in the loop with new workshops, coding tips, and platform updates.
You'll also be the first to know about upcoming launches, events, and exclusive discounts.
No spam—just helpful content to level up your skills.
If that sounds fair, go ahead and log in to continue →
Enter your name and email to get instant access
In this lesson, we focus on enhancing a question card component in a React application, emphasizing the use of TypeScript for type safety and code organization. The discussion includes creating a reusable component for displaying questions, proper handling of props, and ensuring a responsive design that works well across various devices.
questionCard.tsx
to encapsulate the question display logic.00:00:02 Now that we've implemented the search and the filtering, we are ready to head over to our page and create a better looking question card.
00:00:12 Yep, this one that is just a simple H1 just doesn't cut it for me.
00:00:17 So let's actually head over to components.
00:00:20 Cards, and then create a new question card.tsx.
00:00:27 Run RAFCE.
00:00:29 Import it right here, where we're rendering the H1.
00:00:33 Let's call it a question card coming from components question card.
00:00:39 Of course, it'll have a key equal to question dot underscore ID, and then we'll pass over the full question information to each one of these question cards.
00:00:50 So let's head over into the question card and let's accept all of these props.
00:00:55 We're going to get them in form of a question, but we can further destructure those props so we can easily reach them.
00:01:02 We're going to get the underscore ID, the title, the tags, the author, the created at, upvotes, answers, and views.
00:01:13 And this will be of a type props.
00:01:17 So right here, we can define the interface of props.
00:01:22 and say that a question is of a type, and now what you could do is define all of these properties right here.
00:01:30 Like you could go very detailed with specifying all of these types right here.
00:01:34 But the problem is that you'll need to reuse this type or this interface quite often, because the question is the root data type in our application.
00:01:43 We see it in almost every single page.
00:01:46 So what would be a better way of storing that type or the interface so we can reuse it in other cases?
00:01:53 Well, for that, we can define a global TypeScript type.
00:01:57 So head over to File Explorer and create a new folder called Types.
00:02:03 Within types, create a new file called global.d, which stands for definition, .ts.
00:02:10 Here you can declare global types in TypeScript, which will help you keep your code organized and reusable.
00:02:17 We can share these types across whichever files you type right here, like interface, question, you'll be able to reuse across all the other files without
00:02:28 even needing to import them, making it easier to manage and reduce errors.
00:02:32 It'll significantly improve your TypeScript developer experience.
00:02:37 So each question has an underscore ID of a type string.
00:02:41 It has a title of a type string.
00:02:44 It doesn't have a description in our case, but it does have tags with the underscore ID and a name, which is an array of those tags.
00:02:51 And this is also a pretty cool thing.
00:02:53 If you're going to end up reusing some other subtype often, you can turn it into its own interface and then build those bigger interfaces like Lego building blocks.
00:03:04 Like let's say that each tag has an ID and a string.
00:03:08 You can just say right here, the tags in the question is just an array of different tag interfaces.
00:03:15 We can repeat the same thing with the author by declaring the interface user or no, it's going to be an author because they create each one of these questions.
00:03:24 So it's going to have an ID or underscore ID of a string.
00:03:29 It's going to have a name of a string and maybe even an image of a type string.
00:03:33 So within the question, we can say author.
00:03:37 is going to be author, created at is going to be of a type date, and then it's going to have uploads of a type number, answers of a type number,
00:03:48 and views of a type number as well.
00:03:50 Now, if we head over to the question card, we can define questions as simply an array of questions.
00:03:59 And if you control or command click in it, it's going to lead you straight to this interface.
00:04:04 But as you can see, if you hover over it, it's going to say question is not defined coming from ESLint.
00:04:10 This is an issue with ESLint when working on Tabscript projects.
00:04:15 And the actual recommended solution is to simply turn off ESLint for these types of issues.
00:04:21 So let's head over to our eslintrc.json, scroll down.
00:04:26 Let's go right here.
00:04:27 to overrides, set it as an array, specifically focus on files, everything.ts and everything.tsx and rules, no undef, so no undefined.
00:04:44 will be simply set to off.
00:04:47 Oh, and I noticed there's a little typo here.
00:04:49 This is supposed to say no undef.
00:04:53 And if you did this properly, head over back to the question card and check it out.
00:04:58 The error is gone.
00:05:00 And now TypeScript knows that this question right here is referring to the question interface, which is amazing because if you try to use something like
00:05:09 underscore ID, it'll actually know of which type it is.
00:05:12 But let's see why is it complaining about this.
00:05:15 It's saying title does not exist.
00:05:18 And that's it.
00:05:20 Yep.
00:05:20 It's saying title does not exist.
00:05:22 Let's see what did we use in the original page right here.
00:05:26 We said question title.
00:05:28 Okay.
00:05:29 Oh, they're saying question array.
00:05:31 Actually, this is not an array.
00:05:32 This is just one single question.
00:05:36 So if we do it like this, now it knows that this ID is specifically a string.
00:05:41 We can use it within our application and it knows that it is a string.
00:05:45 Great.
00:05:47 So let's create the UI of our question by wrapping everything in a div and giving it a class name of card-wrapper, rounded-10px to give it some smoother edges,
00:06:02 a padding of nine, on small devices, padding X of 11. Within this div, let's create another div that'll have a class name equal to flex,
00:06:14 flex-col-reverse, items-start, justify-between, a gap of 5, and on small devices, a flex of row.
00:06:26 Within it, we can have just an empty div which we'll use for positioning.
00:06:31 And then within that div, we can have a span which will render the created at property.
00:06:36 So when was this question created?
00:06:39 And created ad is actually a date.
00:06:42 So we need to either stringify it.
00:06:44 So dot to string.
00:06:48 So we can wrap it in a string constructor like this, or we need to have some other function, which will actually turn this into a human readable format.
00:06:58 So let's actually do the latter because this is definitely not looking good.
00:07:01 I'll head over to lib and then utils.
00:07:05 And then within here, we can create a new utility function called const get timestamp, and it's going to take in a date of a type date.
00:07:16 And we needed to return a timestamp of how long ago was this date.
00:07:20 And this is the perfect type of situations that I use chat GPT for.
00:07:24 So head over to chat GPT, give it this function constructor.
00:07:28 and say to create a function that takes in a date and returns a string specifying how long ago was it created, like five seconds ago or days or hours,
00:07:46 and simply run it.
00:07:49 It should give us a pretty simple function into a human readable time ago format.
00:07:53 I like how it called that.
00:07:54 Let's copy the code and let's paste it right here.
00:07:57 Okay, I think this is gonna look good.
00:08:01 And let's actually export it.
00:08:04 And now back in the question card, we can call, how did we call it?
00:08:09 Get timestamp, import it and pass the created ad into it.
00:08:14 And if you do that, it says just now for both of these, two seconds ago, three seconds ago.
00:08:20 Why is that?
00:08:21 Well, that is because in our question, we actually provided a date.now.
00:08:27 But if I use some random date, like let's do, there we go.
00:08:31 ChatGPD for some reason auto-filled this date for me.
00:08:35 You can see this was three years ago.
00:08:37 And then this one for some reason is still cached.
00:08:40 And then this one says now because we have a new date, but it's good to know that this get timestamp actually works.
00:08:48 Now let's actually render a question title.
00:08:51 That's going to be an H3.
00:08:54 and we can render just the title.
00:08:57 How to learn React, how to learn JavaScript.
00:09:01 Let's style it a bit by giving it a class name of, on small devices, h3-semi-bold, typically base-semi-bold, text-dark 200, light 900, line-clamp-1,
00:09:13 and flex-1.
00:09:21 There we go.
00:09:22 That's better.
00:09:22 Let's also wrap it in a link because once we click on this specific title, we want to navigate over to the question details page.
00:09:33 So let's give it a link with an href pointing to routes coming from constants routes dot question And we want to pass in the question ID,
00:09:45 not ask question.
00:09:47 It's going to be just question details.
00:09:50 So we need to get over to the routes and create a new one that's going to point to the question.
00:09:56 So that's going to be forward slash questions ID.
00:10:01 So forward slash question ID.
00:10:04 Let's also style the span by giving it a class name of subtle regular text dash dark 400 light 700. line-clamp-1, what this does is it allows it to take
00:10:21 maximum of one line of space, property flex, and on small devices hidden, because if we're looking at a larger screen, then we'll want to show just the
00:10:30 title and on the right side we'll show something else.
00:10:33 But on small devices, we can't show it.
00:10:35 This is looking good to me.
00:10:37 So now we can go below the link and below two more divs.
00:10:42 And we can create a div which will create some space for the next section that we're working on.
00:10:48 This one will be about tags.
00:10:52 So let's give it a class name of margin top of 3.5. flex, wfull, flex-wrap, and a gap of two in between the tags.
00:11:05 And here, we want to map over our tags and display them.
00:11:09 Thankfully, if you remember, we have already built our reusable tag card.
00:11:14 So, let's simply go inside and say tags.map, and for each specific tag, we can automatically return a tag card.
00:11:23 Coming from .slash tag card, To it, we can pass a couple of different things.
00:11:29 The first one being the key, since we're mapping over it, equal to tag.underscoreID.
00:11:34 And now that we have our global type for the tag, we can actually just use it here.
00:11:38 It'll automatically know exactly which properties each tag has.
00:11:43 We can also pass the underscore ID equal to tag.underscoreID.
00:11:48 And let's do a name equal to tag.name.
00:11:52 Finally, in this case, we want to use a compact version of the card so we can pass in compact.
00:11:58 And we can see that the tag card type requires questions, but actually it's going to be optional.
00:12:03 So let's set it as optional here as well.
00:12:05 For now, we don't have to worry about this compact thing not being used.
00:12:09 I'll explain what that is when we use the tag card in some other context.
00:12:13 As you know, we can completely change how a specific reusable component looks in fields based on its context and the props that we pass into it.
00:12:22 But as you can see, just by default, this works perfectly.
00:12:26 We have React and JavaScript with their corresponding logos, and then JS and JS at the same time at the bottom, because as you know,
00:12:34 these are just fake questions right now.
00:12:36 Now, what else does each one of our question cards need to have?
00:12:40 Some info about the author, when it was asked, and then info about the votes, answers and views.
00:12:47 So, let's add those next to make this card complete.
00:12:50 As you can see, the question card itself is being reused many times.
00:12:55 But even within the card, we have some reusable components, such as this one right here, where we reuse the tag multiple times.
00:13:03 In this case, we use our own improved version of the card with the icon.
00:13:07 But there is one more thing which we reuse a couple of times, and that is this thing right here, which we can call a metric,
00:13:14 which has an icon, and then a number of votes, answers, and views.
00:13:19 So, let's create a new component in the components folder, and we can call it metric.tsx.
00:13:27 Run RAFCE, and then we can import it right within the question card.
00:13:32 How is that going to look like?
00:13:33 Well, let's go below this div and create another div that'll have a class name equal to flex-between margin top of 6, w-full,
00:13:47 flex-wrap so we can show them even on the devices with a lower screen width and a gap of 3 in between the metrics.
00:13:56 Now right here we can create our first metric component by importing the metric and we can pass it a couple of props.
00:14:04 we can pass it an image URL, like which image do we want to show?
00:14:08 In this case, we'll be talking about the author of the post who created it.
00:14:12 So we can pass the author dot.
00:14:15 Let's see how we called it.
00:14:16 If I go to page home, we called it author, and it has an ID.
00:14:23 It also has a name.
00:14:24 Let's make sure that it has an image.
00:14:26 So right here, we can give it some kind of an image.
00:14:28 And since we don't yet have real images, I'll just go to Google and search for an avatar image.
00:14:34 And I will copy a link address.
00:14:37 Or no, we have to copy an image address for this to work.
00:14:40 And I'll paste it right here.
00:14:42 Now for the second question, we also need to add an image and I'll make it the same one.
00:14:48 So I'll just paste it here.
00:14:49 Great.
00:14:50 So we have added an image right now and going back to this metric, now we can say that the image URL is equal to author.image.
00:14:58 We can also pass an alt tag equal to author.name.
00:15:02 And we can pass a value equal to author.value.
00:15:07 And in this case, we're working with this one right here.
00:15:09 So we have to have an image, a name, and then we're going to have a title of this specific metric, which is going to say asked two minutes ago.
00:15:18 So we'll copy this part and provide it a title equal to a template string.
00:15:25 of asked two minutes ago, but instead of two minutes, I will actually call the get timestamp, which we created.
00:15:33 And I'll pass in the created at property that belongs to each one of these questions.
00:15:39 Next, we can also give it an href to where this will point to when we click it.
00:15:44 And that's going to be routes dot profile.
00:15:48 And it's going to point to author dot underscore ID.
00:15:52 We can give it some special text styles, which are going to be equal to body-medium and text-dark 400 underscore light 700. And I'll give it a special
00:16:05 isAuthor boolean variable because you can see that this isAuthor is quite different than these three right here.
00:16:11 These are going to be metrics as well.
00:16:13 just so we know how to properly show them.
00:16:15 Now, if you go back, you can see a singular metric here.
00:16:18 But now let's go below this metric and let's create another div.
00:16:23 The other three metrics will be contained within this div as they have to be closer together.
00:16:28 So let's give it a class name of flex, items-center, gap of three, onMaxSMDevices flex-wrap, and onMaxSMDevices justify-start.
00:16:42 Now within here, we can render a metric.
00:16:46 This one will show what?
00:16:48 Well, let's see.
00:16:49 It'll show votes.
00:16:50 So let's focus on the votes first by passing it an image URL equal to forward slash icons, forward slash like dot SVG.
00:17:00 We can pass it an alt tag equal to like.
00:17:03 We can pass it a value, which is going to be equal to the number of upvotes.
00:17:08 We can pass it a title, which is going to be empty space and then votes.
00:17:13 And finally, we can pass text styles, which are going to be equal to small dash medium, text dash dark 400 and light 800. And now I can duplicate this
00:17:25 one two more times.
00:17:27 The second one we'll talk about answers.
00:17:30 So we can give it an image URL of message.
00:17:35 The alt tag will be answers.
00:17:39 the value will be answers, the title will be answers, and the classes can remain the same.
00:17:47 And the third one is talking about views.
00:17:49 So right here, we can render an eye SVG, call it views, render views, and the title will be views.
00:18:00 If we save it and go back, You can see that now we have one metric on the left side and three metrics on the right side.
00:18:08 And all of them are exactly the same.
00:18:11 So now we have to do what reusable components do, accept all the props and then render them depending on the context.
00:18:19 So to do it, let's destructure all the props such as imgURL, the alt tag, the value, the title, the href, the text styles,
00:18:30 maybe even the image styles if we have them in the future.
00:18:34 And is author.
00:18:36 We can also define these as props.
00:18:40 So now we can define those props right here at the top by saying interface props.
00:18:45 And then here we can define all of them.
00:18:47 As you can see a perfect example of where Copilot does it completely for me.
00:18:53 Beautiful.
00:18:53 And now we can render a metric.
00:18:56 Now, some of these metrics should be clickable and some should not be.
00:19:00 So how do we know whether we wrap it in a link or whether we wrap it in a div?
00:19:05 Well, check this out.
00:19:07 Well, we'll define some always present content that each metric will have.
00:19:12 const metric content, which will be equal to, let's do just an empty react fragment for now.
00:19:19 And then, before we return, we'll check if an href is present, and if it is, we'll return a link containing that href, and then we can render the metric
00:19:32 content within it.
00:19:33 Else, if we don't have an href, we will render a div, and then we'll add the metric content within it.
00:19:40 And let's not forget to import link from next link.
00:19:43 I think we have to properly close it here, and then again, close it here.
00:19:47 So does this make sense?
00:19:49 Like we will always show some kind of a metric, but if we have href, we will wrap it in a link, else it'll simply be a div.
00:19:59 Now, how will that metric content look like?
00:20:02 Well, first we'll render an image.
00:20:05 And this image will have a source equal to IMG URL.
00:20:12 If you do this, it'll break because we haven't defined the image, so let's import it.
00:20:17 But once you import it, it'll also break because we have to give it a width and a height.
00:20:22 So let's give it a width of 16 and a height of 16. But even when you fix that, it'll also break because we haven't yet configured this static image we
00:20:32 use from Google under NextConfig.js.
00:20:36 So it says that we have to add the hostname of static.vecteasy.com.
00:20:41 So let's copy it, go to config, and here you'll have to add the following configuration.
00:20:48 Images, remote patterns, And then here you can define the protocol of HTTPS, a host name equal to whatever your error is saying.
00:21:00 For you, it might be different if you picked another avatar image.
00:21:04 For me, it is just like this and port can be empty.
00:21:08 If you define this and reload, you can see that now our logo shows.
00:21:13 Let's also give this image an alt tag equal to, well, it's just going to be the alt that we're passing in.
00:21:22 We can give it a class name equal to, it'll be dynamic of rounded-full, object-contain, and we'll render any image styles if we have passed it through props.
00:21:35 There we go.
00:21:35 That's a bit better.
00:21:37 Now below this image, I'll render a p tag with a class name equal to dynamic.
00:21:44 I'll first render the text styles, which we're passing, and then I'll give it flex items-center and the gap of one.
00:21:54 And within it, we want to render a value.
00:21:57 So this is going to be the number of views, likes, or whatever.
00:22:00 And we also showing the image right here.
00:22:04 Right below the value, we want to render a span and here we want to render a title.
00:22:11 There we go.
00:22:12 So you can see ask just now, votes, answers, and views.
00:22:16 And in case that this right here is an author, we want to hide it on smaller devices.
00:22:21 I'll explain soon why.
00:22:23 So let's give it a class name of, I'll make it a dynamic template string, small dash regular.
00:22:31 line dash clamp dash one.
00:22:34 So it fits into one line.
00:22:35 And then if is author, I'll give it a max on small devices hidden else nothing.
00:22:44 So now you can see that on small devices, we won't be able to see when this was created, but if we expand it, you can see that it was asked just now.
00:22:55 Why is it that way?
00:22:56 Because on smaller devices, a better place to put this timestamp is right here at the top, because we don't have enough space right here on the right side.
00:23:05 Why not?
00:23:06 Well, because we can further style this link and div by giving them a class name of flex-center gap of one.
00:23:18 If you do that, now we can notice that they take a bit more space, so we cannot really show when this was created.
00:23:24 That's great, it looks great on desktop devices, but we're missing who created this specific question.
00:23:32 So if we go back to where we're passing specific info to the metric, right here under value, you can see how we're passing author.value,
00:23:41 but this was supposed to be author.name.
00:23:44 So if we do it that way, you can see John Doe asked this question just now.
00:23:48 So it makes more sense to have it here on desktop devices, since we have a lot of space, but on mobile devices, since we don't have a lot of space,
00:23:56 it makes more sense to just say just now here or three years ago, And then we can show the avatar as well as other questions.
00:24:04 Of course, all of this will look even better once we actually have a real question.
00:24:08 Now, it looks like our metric is complaining a bit saying that the value is not assignable type number.
00:24:14 So we can fix this over in the metric by saying that value will actually either be a string or a number.
00:24:21 href will be optional and image styles will also be optional because we don't need to have them.
00:24:29 Great.
00:24:30 And with that, we're done with the metric, which means we're done with the entire card as well.
00:24:35 Oh, there's a slight issue here with the created ad does not exist on the type question.
00:24:40 So let's see whether that is truly the case.
00:24:42 Yep.
00:24:42 I am missing the created ad.
00:24:44 So we can add created ad, which is going to be of a type date.
00:24:49 Great.
00:24:49 So now we're using proper types in the question card using the global TypeScript declaration.
00:24:55 And what we can do to make it more type safe is right here where we're mapping over the tags.
00:25:00 Oh, we already did that.
00:25:01 We said that each tag is of a type tag.
00:25:05 So now it knows exactly which properties does it have.
00:25:08 Great.
00:25:09 So that's it.
00:25:09 You have just created a completely reusable question card with its own tags that you can click to and go to them, that you can click the author as well
00:25:18 to check out their profile.
00:25:20 And later on, you'll also be able to check out real votes, answers, and views.
00:25:25 Of course, fully mobile responsive as everything else.
00:25:28 And works great on light mode too.
00:25:31 So let's go ahead and merge that.
00:25:33 I'll say something like implement question card, commit and sync.
00:25:40 And that's it.
00:25:41 The UI of our homepage has been implemented.
00:25:44 Very soon, we'll be able to hook this up to a database and we'll be able to see the questions that people ask.
00:25:51 But to see the questions or to fetch them, we first need to be able to create them.
00:25:57 So implementing the Ask a Question page will be our next step.