Github Package Search App Using Next.js
Develop a simple search app where we get all the information required to choose the package in one shot.
Join the DZone community and get the full member experience.
Join For FreeEvery day as part of the development, we search for open source packages in Github using google. It will be time taking process as we need to search for the keywords, get into each Github repository to check the below items to choose the packages:
- Is it completely open source to change it or use for the commercial app?
- Whether it is not archived or is some active community working for it?
- Does it have a lot of issues?
- What are the languages used in this repository?
- How much is the size of the package, so it doesn't make my project bulkier?
- When did the recent update happen?
I want to develop a simple search app where we get all the information required to choose the package in one shot. It will also help any development team as a handy tool to choose the Github packages.
Github provides a search API to search the repository for the search term, and for each repo, get the details information like branches, tags, and contributors and show it as a single view. We choose Next.js so we can search in the backend and render only the contents on the client side. It provides a free deployment tool to deploy and is available publicly for use.
Head's Up of Next.js
Next.js is a server-side rendering of the react.js framework, which supports us in creating the single-page web application. It has everything required to develop production-ready web applications - production SSR Web UI applications: Hybrid static & server rendering, TypeScript support, Smart bundling, Route pre-fetching, and more. To know more about Next.js, please check their official site.
Project Setup
The pre-requisite is to have node.js 12.2.0 or later.
We can create the next app with TypeScript support by a simple command:
npx create-next-app@latest --ts
To get started in an existing project, create an empty tsconfig.json file in the root folder:
touch tsconfig.json
Project Start
Then, run next (normally npm run dev or yarn dev), and Next.js will guide you through the installation of the required packages to finish the setup:
npm run dev
Install Chakra UI
I used Chakra UI to get ready to use components quicker for building the UI. It provides Box and Stack layout components, which is easy to style your components through props.
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
Search App:
The motive is to provide search keywords and get the list of repositories available in Github with all the essential information like below:
Next.js app starts with index.tsx page. In that, I created the server-side props function, which will be responsible for the server call to fetch the data for the page before the page comes to the client. In this function, we have used Github search repositories API. Search text is given in the UI, and the response is extracted with the required information into the repoItem model.
export const getServerSideProps = async ({
query,
}: GetServerSidePropsContext<QueryParams>): Promise<GetStaticPropsResult<ContentPageProps>> => {
console.log("context.query :>> ", query);
if (!query || !query?.q) {
return { props: { repos: [], searchText: "", count: 0 } };
}
const searchText: string = `${query.q}`;
const url = `https://api.github.com/search/repositories?q=${query?.q}`;
const token: RequestInit = {
headers: [
["Authorization", `${process.env.token}`],
["Accept", "application/vnd.github.v3+json"]
],
method: "GET",
};
const res = await fetch(url, token);
const data = await res.json();
// console.log('data :>> ', data);
const repos: RepoItem[] = data.items.map((item: any) => {
const repo = {
id: item.id,
language: item.language,
name: item.name,
full_name: item.full_name,
description: item.description,
stars: item.stargazers_count,
watchers: item.watchers_count,
forks: item.forks,
homepage: item.homepage,
topics: item.topics,
tags: item.tags_url,
size:item.size,
issue: item.open_issues_count,
contributors: item.contributors_url,
archived: item.archived,
visibility: item.visibility,
updated_at: item.updated_at,
license: item.license,
owner: item.owner,
has_wiki: item.has_wiki
};
return repo;
});
const results = { repos: repos, searchText: searchText, count: data.total_count || 0 };
return { props: results };
};
Search Box is a form component where we allow the user to enter the search text, and on submitting the form, it hits the backend with the query string. The backend is the serversideprops which will, in turn, trigger the Github API to get the matched repositories for the search keyword(as explained above).
One thing to be noted is that I didn't use onchange as it will trigger on each keypress. Instead, I used the useRef, to get the input value when submitting the button.
Search Results will be displayed using the custom component - RepoContainer for each RepoItem data under the VStack layout component.
<VStack>
<Spacer />
{repos && repos.map((repoItem: RepoItem) => <RepoContainer key={repoItem.id} repo={repoItem} />)}
</VStack>
Each RepoItem consists of four stack components marked by comments in the below code snippet. First stack component consists of repository name, full name, and language details. In the second stack, meta details like the tags, branches, size of package, license, and owner are kept. The third stack, how the repository is stable and actively updated, and fourth stack shows the popularity of the repository community.
<Box
color={useColorModeValue("gray.700", "gray.200")}
w={"100%"}
borderRadius={10}
bg="#B1F1F3"
>
<Container as={Stack} maxW={"9xl"} py={10}>
<SimpleGrid templateColumns={{ sm: "1fr 1fr", md: "2fr 2fr 2fr 2fr" }} spacing={3}>
<Stack>{/* First stack component */}
<ListHeader>{repo?.name}</ListHeader>
<Text fontSize={"sm"}>
<NextLink href={`https://github.com/${repo?.full_name}`} passHref>
<Link color="blue">{repo?.full_name}</Link>
</NextLink>
</Text>
<Stack direction={"row"} mt={10}>
<Text fontSize={"sm"}>
Language:{" "}
<Badge colorScheme="red" variant="solid">
{repo.language}
</Badge>
</Text>
</Stack>
<Stack direction={"row"}>frameworks :</Stack>
</Stack>
<Stack align={"flex-start"} fontSize={"sm"}>{/* Second stack component */}
<Stack direction={"row"} align={"center"}>
{/* <Text>Releases </Text> */}
<GoTag /> <Tags url={repo.tags} />
<GoGitBranch /> <Branches repo={repoName} />
</Stack>
<Text>Size of the package : {Math.round((repo.size / 32768) * 100) / 100} MB </Text>
<Text>License : {repo.license?.name ? repo.license?.name:"None"} </Text>
<Text>Owner : <Avatar src={repo?.owner?.avatar_url} size='xs' /> {repo?.owner?.login}</Text>
</Stack>
<Stack align={"flex-start"} fontSize={"sm"}>{/* Third stack component */}
<Stack direction={"row"} align={"center"}>
<GoIssueOpened /> <Text>Issues :{repo.issue} |</Text>
<GoOrganization /> <Contributors url={repo.contributors} icon={<GoOrganization />} />
</Stack>
<Stack direction={"row"} align={"center"}>
<GoArchive />
<Text style={{ color: repo.archived ? 'red': 'black'}}> IsArchived: {repo.archived ? "true" : "false"} |</Text>
<GoEye />
<Text> Visibility: {repo.visibility}</Text>
</Stack>
<Stack direction={"row"} align={"center"}>
<GoWatch /> <Text>Last Updated : {updatedTime}</Text>
</Stack>
<Stack direction={"row"} align={"center"}>
<GoGitPullRequest /> <PR repo={repoName} />
</Stack>
</Stack>
<Stack align={"flex-start"}>{/* Fourth stack component */}
<Stack direction={"row"} align={"center"}>
<GoStar /> <Text>{repo.stars} |</Text> <GoRepoForked /> <Text>{repo.forks} |</Text>
<GoEye /> <Text>{repo.watchers} </Text>
</Stack>
<NextLink href={`${repo.homepage}`} passHref>
<Link color="blue">Homepage</Link>
</NextLink>
{/* <Text>Examples</Text> */}
{repo.has_wiki ? (
<NextLink href={`https://github.com/${repo?.full_name}/wiki`} passHref>
<Link color="blue">Wiki Link</Link>
</NextLink>
): ""}
</Stack>
</SimpleGrid>
<SimpleGrid>{/* Repo description */}
<Text>Description :{repo.description}</Text>
</SimpleGrid>
<SimpleGrid>{/* Repo Topics */}
<Stack direction={"row"} align={"center"}>
<Wrap>
{repo.topics &&
repo.topics.map((res: string) => (
<>
<WrapItem key={res}>
<Badge variant="solid" colorScheme="messenger" >
{res}
</Badge>
</WrapItem>
</>
))}
</Wrap>
</Stack>
</SimpleGrid>
</Container>
</Box>
At last, the description and Github topics are kept to get a glimpse of the project.
If you keenly note, there are components like Branches, and Prs, which react custom components. It will be rendered asynchronously once the repo container is loaded.
Let's take a closer look at the PR component; it returns a div tag with pr counts. PR counts are not directly available in the search repositories call. There is one more call to be made to get the pr counts, so we use the API routes. This API route, in turn, calls the Github API with the token and gets the count of pull requests. This count computation load has been delegated to the backend.
Here reponame is passed through props to the component, and the count state is resolved after the fetch API call.
interface propTypes {
repo: string;
}
function PR(props: propTypes) {
const { repo } = props;
const [count, setCount] = useState(0);
useEffect(() => {
if (repo) {
fetch(`/api/prcounts?repo=${repo}`).then((res) => res.json()).then(res => {
const prcount = res.data;
setCount(prcount);
});
} else {
setCount(0);
}
}, [repo])
return (
<div className='center'> Pull Request : {count}</div>
)
}
export default PR
In the pages/api/pr.ts file, we invoke the Github API call, get the pull requests, and find the count of pull requests. Then the pull request value is returned in JSON format like {data: prcount}to the frontend.
type ResponseData = {
data: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
const repoName: string = `${req.query.repo}`;
const url = new URL(`https://api.github.com/repos/${repoName}/pulls`);
const options = { 'headers' : {
'Authorization': process.env.token,
'Accept': "application/vnd.github.v3+json",
'User-Agent': 'Mozilla/5.0'
},
'method': 'GET',
};
const clientreq: ClientRequest = https.request(url, options, (apiresponse: IncomingMessage) => {
let respJson: string = "";
apiresponse.on('data', (chunk: string) => {
respJson += chunk;
});
apiresponse.on('end', () => {
if (apiresponse.statusCode === 200) {
let prResponses = JSON.parse(respJson);
const prCounts = prResponses.length;
res.status(200).json({'data': prCounts});
}
})
});
clientreq.on('error', (e: Error) => {
console.error('error message', e.message);
});
clientreq.end();
}
I made the same pattern for branches, and contributors, like a react component that invoked API routes to fetch the data from Github APIs.
Complete source code is available for reference.
Deployed for usage.
My plan is to expand this app and get more information like test build status, demo page, and remove fork duplicates in the search results. Eager to get feedback from you as well, like whether you need to see more information or some more options in terms of searching the package. Please drop in the below comments sections.
Opinions expressed by DZone contributors are their own.
Comments