If you’re interested in entrepreneurship and computer science, and you actively keep up with the latest news around those topics, you might already be familiar with Hackernews. For those who aren’t, Hackernews is a social news website run by the investment fund Y Combinator.
In my opinion, while the quality of posts that the site publishes is debatable, the UI seems quite outdated. Don’t get me wrong, it’s still a decent UI that is crazy fast, but it doesn’t seem polished enough to compete with websites in the year 2022.
Some argue that this is by design. The site is built on a tech stack as close to pure HTML, CSS, and JavaScript as possible to avoid the bundle size and other complexities of a UI framework.
But, with the advent of breakthrough technologies like Next.js, it’s possible to get closer to that level of performance despite using a UI framework. In this article, we’ll do just that by building a clone of the Hackernews client using Chakra UI and Next.js. Let’s get started!
Table of contents
- Tech stack
- Building the UX
- Chakra UI integration
- Home page
- List UI with dummy data
- API integration
- Server-side rendering
- Iterate over the list
- Parsing the domain
- Parsing the time
- Pagination
- Conclusion
Tech stack
Let’s take a closer look at our weapon of choice, the tech stack that we’ll use for this project.
UI framework: Next.js
As stated earlier, our UI framework of choice will be Next.js because we want to leverage server-side rendering, which Next.js supports out of the box. Apart from that, we’ll also indirectly benefit from other features like file-system based routing, code splitting, fast refresh, and more.
Along with Next.js, we’ll use Chakra UI for the component library. Chakra is an amazing UI library that provides modern-looking React components that you can customize without writing a single line of CSS. The library also features responsive design support out of the box.
Backend API
To query the latest items that we need to display in our app, we’ll make a call to the free Hacker News APIs. Basically, we need to call the following two APIs:
- The New and Top stories API retrieves 500 of the top item IDs in JSON format
- The get item by ID API gets all the details of an item when provided an item ID
To implement the backend, make a call to the first API and fetch the IDs of all 500 items. A call to https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty
returns the following code:
[ 30615959, 30634872, 30638542, 30638590, 30635426, 30637403, 30638830, 30632952, ... ]
We fix a page size of 20
items and determine which IDs fall under that page using the formula below:
pageStartIndex = Number(page)*Number(pagesize) pageEndIndex = (Number(page)+1)*Number(pagesize) - 1
Once we have the indices, we’ll trigger an API call to fetch the details of all 20 items within that range of indices in parallel. When you call for one item, the API URL https://hacker-news.firebaseio.com/v0/item/30615959.json?print=pretty
returns the following:
{ by: "rayrag", descendants: 50, id: 30615959, kids: [ 30637759, 30639031, 30637901, 30637711, ... ], score: 364, time: 1646841853, title: "Pockit: A tiny, powerful, modular computer
", type: "story", url: "https://www.youtube.com/watch?v=b3F9OtH2Xx4" }
All this will happen on the server-side due to the magic of Next.js. We’ll only get the necessary details to populate the 20 items on the UI. Once the user clicks on any of the list items, we’ll navigate to the URL of that item in a new tab in the browser.
Building the UX
Project setup
The frontend setup is as easy as creating a new Next.js repo, which we can do using the create-next-app
command. Navigate to a folder where you want to create the project and run the following command:
npx create-next-app hackernews
Next.js will take care of the rest. After the script has completed running, there will be a new folder created with the name hackernews
. Navigate into it and start the application to see the welcome screen:
cd hackernews yarn dev
The code above will bring up the familiar start page for Next.js projects:
Chakra UI integration
Now, let’s install Chakra UI in the same project using the command below:
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
Once Chakra UI is installed, we need to go to pages/_app.js
and wrap the root with ChakraProvider
so that it looks like the following code:
import { ChakraProvider } from "@chakra-ui/react"; import '../styles/globals.css'; function MyApp({ Component, pageProps }) { return ( <ChakraProvider> <Component {...pageProps} /> </ChakraProvider> ) } export default MyApp
Now, we’re all set to use Chakra UI in our project. You can follow along and build it yourself or refer to this GitHub repository.
Home page
Next, we’ll modify the pages/index.js
file. We’ll use Chakra UI components to build the site title and the main header along with the pagination menu. We’ll have just have two styles in our app. For one, a .container
will position our main site in the center, and a .main
style will hold our entire site UI.
Then, we create our header and title component as follows. The menu is hardcoded for now but will be changed later:
<Box className={styles.main} px={[4, 10]}> <Heading as='h1' size='4xl'> Hacker <span style=>news</span> </Heading> <Flex direction="row" justify='space-between' align='center' width="100%" mt="12"> <Heading as='h1' size='xl'> Top news </Heading> <Menu> <MenuButton as={Button} rightIcon={<ChevronDownIcon />}> Page </MenuButton> <MenuList> <MenuItem>1</MenuItem> <MenuItem>2</MenuItem> <MenuItem>3</MenuItem> <MenuItem>4</MenuItem> <MenuItem>5</MenuItem> </MenuList> </Menu> </Flex> </Box>
Here’s what the output looks like on the desktop browser:
And on a mobile browser:
List UI with dummy data
Next, we’ll create a component to display the Hackernews list items. We need to show the title, the upvotes, comments, and the user who posted the item. Before integrating the API, we’ll assume some dummy values for these and create the UI for the item.
Create a components folder and a file called ListItem.jsx
, which will hold the presentation code for the list items. To keep the list item responsive, we’ll use the Flex
component provided by Chakra UI to build several Flexbox rows and columns.
The component looks like the following code:
export default function ListItem({ item }) { return ( <Flex direction="row" align={"center"} mb={4}> <Flex style= justify="center" mt={-8}> <Tag size={"md"} key={"md"} borderRadius='full' variant='solid' colorScheme='teal' > <TagLabel>{item.index}</TagLabel> </Tag> </Flex> <div style=> <Flex direction={"column"}> <Heading as='h1' size='sm'> {item.heading} </Heading> <Flex direction={"row"} justify="space-between" mt="2" wrap={"wrap"}> <Text fontSize='sm' color="gray.500" >{item.site}</Text> <Text fontSize='sm'>{item.time} - by <span style=>{item.user}</span> </Text> </Flex> <Flex direction="row"> <Button leftIcon={<ArrowUpIcon />} colorScheme='blue' variant='ghost'> {item.likes} </Button> <Button leftIcon={<ChatIcon />} colorScheme='orange' variant='ghost'> {item.comments} </Button> </Flex> </Flex> </div> </Flex> ) }
Let’s hardcode a single JSON item for testing purposes. We’ll check how it gets displayed on the UI with the ListItem
component we developed just now:
const item = { heading: "Can't you just right click on this?", site: "lucasoftware.com", time: "10h", user: "bangonkeyboard", likes: 20, comments: 50, index: 1, }
The only functionality that this ListItem
code is missing is the redirection to a new tab when any of the items are clicked. We’ll add that later. Now, all we need to do is fetch the list of items from the backend and map over it to create the list items.
API integration
Next, we’ll add API integration. We’ll make a call to the Top Stories API that we discussed earlier, then fetch the details for the items based on the page that the user is on. The page number can be read from the query param in the URL.
Server-side rendering
To make all this happen on the server-side, we’ll use the getServerSideProps
method that Next.js provides. All the code that is written inside of that method is executed on the server-side, and the data returned from that method is supplied to the React component as props:
The code below goes inside the getServerSideProps
method and fetches the posts:
export async function getServerSideProps(context) { let pagesize = PAGE_SIZE; let { page=1 } = context.query; let posts = await fetchAllWithCache(API_URL); page = page == 0 ? 0 : page - 1; const slicedPosts = posts.slice(Number(page)*Number(pagesize), (Number(page)+1)*Number(pagesize)); const jsonArticles = slicedPosts.map(async function(post) { return await fetchAllWithCache(`https://hacker-news.firebaseio.com/v0/item/${post}.json?print=pretty`); }); const returnedData = await Promise.all(jsonArticles); return { props: { values: returnedData, totalPosts: posts.length } } }
Notice that the page number is being read on the server-side using the page query param that is made available by context param. Then, the results are sliced, and the details are fetched for the sliced post IDs.
We’ve also introduced a caching layer using memory-cache so that all these APIs are cached on our server for 60 minutes. The caching logic is as follows:
async function fetchWithCache(url) { const value = cacheData.get(url); if (value) { return value; } else { const minutesToCache = 60; const res = await fetch(url); const data = await res.json(); cacheData.put(url, data, minutesToCache * 1000 * 60); return data; } }
At the end of this method, we have the posts being passed to the React component as props.
Iterate over the list
Next, we iterate over the list that we get in props and call the ListItem
component:
<Flex direction="column" width="100%" mt="8"> {posts.map((post, i) => <ListItem item={post} key={post.id} index={(page - 1)*PAGE_SIZE+i+1} />)} </Flex>
Parsing the domain
There are two more things that we need to take care of. The Hacker News API does not return us the domain name separately, so we need to extract it out of the URL ourselves. A neat trick is to use the URL helper as follows:
const { hostname } = new URL(item.url || 'https://news.ycombinator.com');
The code above helps us extract the hostname
, or the domain name out of any URL. Otherwise, it will always be set to hackernews.com to prevent the app from crashing.
Parsing the time
Additionally, we need a utility to convert the timestamp that we get back into a human-readable time. For instance, the 1647177253
that we get as the value of time needs to be converted into 3 hours ago
. It sounds tricky but is actually quite straightforward.
The utility function below accomplishes just that. First, it calculates the number of seconds that have passed since that time stamp, then calculates the days, hours, minutes, and seconds that have passed sequentially and returns when a non-zero value is found:
export function getElapsedTime(date) { // get total seconds between the times var delta = Math.abs(new Date().getTime()/1000 - date); // calculate (and subtract) whole days var days = Math.floor(delta / 86400); if (days) return `${days} days ago`; delta -= days * 86400; // calculate (and subtract) whole hours var hours = Math.floor(delta / 3600) % 24; if (hours) return `${hours} hours ago`; delta -= hours * 3600; // calculate (and subtract) whole minutes var minutes = Math.floor(delta / 60) % 60; if (minutes) return `${minutes} minutes ago`; delta -= minutes * 60; // what's left is seconds var seconds = delta % 60; return `${seconds} seconds ago`; }
When we refresh the page, the code above generates a beautiful UI with 20 items populated. On our desktop, it looks like the following image:
And on mobile:
Pagination
Lastly, we need to support pagination, which we’ll do in two ways. For one, we’ll create a Load more button at the bottom of the page, which, when clicked, will load the next bunch of stories by redirecting to the next page number.
Secondly, we’ll have the page dropdown, which can be used to directly select the page we need to visit. All we need to do is load the correct route when a particular page is selected. The button looks something like this:
Below is the elegant code that loads more posts:
const onLoadMore = () => { router.push(`?page=${Number(page)+1}`) }
Now, we just create a menu with 25 numbers and call the same function with the page number when the menu item is clicked:
<MenuList> { Array.from(Array(25).keys()).map(item => <MenuItem key={item} onClick={() => onLoadMore(item+1)}>{item+1}</MenuItem>)} </MenuList>
And that takes care of the page navigation to the different pages.
Conclusion
With that in place, our app is complete! In this article, we’ve built a Hacker News client that is responsive for mobile view, is server-side rendered, and supports pagination.
Building this type of real-world application teaches us some important lessons and tricks, like the one we used to parse the URL, or the subroutine that we used for time conversion.
Give it a try and build your own version of a Hacker News client using the free APIs and the UI framework of your choice. I hope you enjoyed this article, happy coding!
The post Hackernews client with Chakra UI and Next.js appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/4pXF6t3
via Read more