diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a686e87 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Taiwo Yusuf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 0fc7196..a9b8255 100644 --- a/Makefile +++ b/Makefile @@ -33,3 +33,7 @@ image: ## Create Docker image .PHONY: docker-up docker-up: ## Start docker image docker run -p 3000:3000 kitty-facty + +.PHONY: install +install: ## Install dependencies + npm install diff --git a/README.md b/README.md index 55c13fe..e08440a 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,98 @@ -# Kitty Facty +# Kitty Facty - The Ultimate Kitty Fact Finder -Welcome to Kitty Facty, the web application that provides random cat facts to its users. This application is built using Next.js and the Cat Fact API. +![Logo](./public/kitty.png) + +Kitty Facty is a simple web application that allows users to browse cat facts. It is built using Next.js and TypeScript. It has the following features: + +- Pagination of cat facts. +- Sorting of cat facts by length, alphabetically or by length. +- Caching of cat facts to reduce the number of API calls. +- Error handling in case something goes wrong with the Cat Fact API. + +## Built With + +- [Typescript](https://www.typescriptlang.org/) +- [Next.js](https://nextjs.org/) + +## Demo + +A live demo of the application can be found [here](https://kitty-facty.vercel.app/). + +![Screenshot](./public/screenshot.png) + +## Quick Start 🚀 + +The quickest way to get started is to run the latest version of the application using [Docker](https://www.docker.com/). To do this, follow these steps: + +- Pull the latest image from Docker Hub: `docker pull teezzan/kitty-facty:latest` +- Start the Docker container: `docker run -p 3000:3000 teezzan/kitty-facty:latest` + +Viola! The application is now running on your local machine. You can now access it at `http://localhost:3000`. ## Getting Started -To get started with Kitty Facty, follow these steps: +This application can be deployed locally by the following ways. -- Clone this repository to your local machine. -- Install the dependencies by running npm install in the project root directory. -- Start the development server by running npm run dev. -- Open your browser and navigate to http://localhost:3000. +- Using [Docker](https://www.docker.com/) +- Using the local environment. -## Features +### Prerequisites -Kitty Facty provides the following features to its users: +To run the application on your local computer, you need to have the following installed: -- Pagination of cat facts. -- Sorting of cat facts by length or alphabetically. -- Caching of cat facts to reduce the number of API calls. -- Error handling in case something goes wrong with the Cat Fact API. +- [Node.js](https://nodejs.org/en/) +- [Make](https://www.gnu.org/software/make/) +- [Docker](https://www.docker.com/) (optional) + +While you don't need an `.env` file to run the application, it is the means by which you can chnage the default values for the application. A sample .env file named `.env.example` is provided. You can make a copy, rename it to `.env`, and update the values as needed. + +### Docker + +- Clone the repository and navigate to the project directory: + +```bash +git clone https://github.com/teezzan/kitty-facty.git +cd kitty-facty +``` + +- Build the Docker image: + +```bash +make build +``` + +- Run the Docker container: + +```bash +make docker-up +``` + +### Local Environment + +To deploy the application locally, follow these steps: + +- Clone the repository: `git clone https://github.com/teezzan/kitty-facty.git` +- Navigate to the project directory: `cd kitty-facty` +- Install dependencies: `make install` +- Start the local server: `make serve` + +## Makefile + +To view the available make commands, run make help. + +```bash +------------------------------------------------------------------------ +kitty-facty +------------------------------------------------------------------------ +serve Run locally +build Build application binaries +lint Run linters +test Run unit tests +image Create Docker image +docker-up Start docker image +install Install dependencies +help Show this help +``` ## Project Structure @@ -99,22 +173,10 @@ To run tests: make test ``` -## Docker - -To build and run the application using Docker: - -- Build the Docker image: - -```bash -make build -``` - -- Run the Docker container: - -```bash -make docker-up -``` - ## License This project is licensed under the MIT License. See the LICENSE file for more information. + +## Authors + +**[Taiwo Yusuf](https://github.com/teezzan/)** diff --git a/public/kitty.png b/public/kitty.png new file mode 100644 index 0000000..9a0a446 Binary files /dev/null and b/public/kitty.png differ diff --git a/public/screenshot.png b/public/screenshot.png new file mode 100644 index 0000000..e9507ee Binary files /dev/null and b/public/screenshot.png differ diff --git a/src/clients/catFactAPI.ts b/src/clients/catFactAPI.ts index a2c6572..448d9e3 100644 --- a/src/clients/catFactAPI.ts +++ b/src/clients/catFactAPI.ts @@ -10,6 +10,11 @@ import config from "@/config"; const baseURL = config.api.catFactBaseUrl; const cache = new NodeCache(); +/** + * Fetches cat facts from the Cat Fact API. + * @param params - The query parameters for the API request. + * @returns A Promise that resolves to a CatFactAPIResponse object. + */ export const getCatFactsFromAPI = async ( params: CatFactAPIArgs ): Promise => { @@ -33,6 +38,11 @@ export const getCatFactsFromAPI = async ( } }; +/** + * Transforms the response from the Cat Fact API to a CatFactData object. + * @param response - The AxiosResponse object returned by the API. + * @returns A CatFactData object. + */ export const transformCatFactAPIResponse = ( response: AxiosResponse ): CatFactData => { @@ -47,6 +57,11 @@ export const transformCatFactAPIResponse = ( }; }; +/** + * Fetches cat facts from the cache if they exist, otherwise fetches them from the API. + * @param params - The query parameters for the API request. + * @returns A Promise that resolves to a CatFactAPIResponse object. + */ export const getCatFactsFromCache = async ( params: CatFactAPIArgs ): Promise => { @@ -70,6 +85,11 @@ export const getCatFactsFromCache = async ( return response; }; +/** + * Fetches cat facts either from the cache or the Cat Fact API depending on the value of `config.api.useCache`. + * @param params - The query parameters for the API request. + * @returns A Promise that resolves to a CatFactAPIResponse object. + */ export const getCatFacts = async ( params: CatFactAPIArgs ): Promise => { diff --git a/src/config/index.ts b/src/config/index.ts index 3cfccf2..afcc9c0 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,21 +1,58 @@ +/** + * An object containing environment constants. + * @typedef {Object} EnvConfig + * @property {Object} api - An object containing API-related environment constants. + * @property {string} api.catFactBaseUrl - The base URL of the Cat Fact API. + * @property {number} api.defaultFactPerPage - The default number of facts to display per page. + * @property {number} api.defaultFactMaxLength - The default maximum length of facts to retrieve. + * @property {number} api.defaultFactPage - The default page number to retrieve. + * @property {boolean} api.useCache - Indicates whether to use cache for API requests. + */ + import { GetBool, GetInt, GetString } from "@/utils/env"; import { EnvConstants } from "./defaultValues"; -export default { +/** + * The environment configuration object. + * @type {EnvConfig} + */ +const config: EnvConfig = { api: { + /** + * The base URL of the Cat Fact API. + * @type {string} + */ catFactBaseUrl: GetString( "CAT_FACT_API_BASE_URL", EnvConstants.catFactAPIBaseUrl ), + /** + * The default number of facts to display per page. + * @type {number} + */ defaultFactPerPage: GetInt( "DEFAULT_FACTS_PER_PAGE", EnvConstants.defaultFactPerPage ), + /** + * The default maximum length of facts to retrieve. + * @type {number} + */ defaultFactMaxLength: GetInt( "DEFAULT_FACTS_MAX_LENGTH", EnvConstants.defaultFactMaxLength ), + /** + * The default page number to retrieve. + * @type {number} + */ defaultFactPage: GetInt("DEFAULT_FACTS_PAGE", EnvConstants.defaultFactPage), + /** + * Indicates whether to use cache for API requests. + * @type {boolean} + */ useCache: GetBool("USE_CACHE", EnvConstants.useCache), }, }; + +export default config; diff --git a/src/config/types.ts b/src/config/types.ts new file mode 100644 index 0000000..dc2925d --- /dev/null +++ b/src/config/types.ts @@ -0,0 +1,20 @@ +/** + * An object containing environment constants. + * @typedef {Object} EnvConfig + * @property {Object} api - An object containing API-related environment constants. + * @property {string} api.catFactBaseUrl - The base URL of the Cat Fact API. + * @property {number} api.defaultFactPerPage - The default number of facts to display per page. + * @property {number} api.defaultFactMaxLength - The default maximum length of facts to retrieve. + * @property {number} api.defaultFactPage - The default page number to retrieve. + * @property {boolean} api.useCache - Indicates whether to use cache for API requests. + */ + +type EnvConfig = { + api: { + catFactBaseUrl: string; + defaultFactPerPage: number; + defaultFactMaxLength: number; + defaultFactPage: number; + useCache: boolean; + }; +}; diff --git a/src/interfaces/api.ts b/src/interfaces/api.ts index 0e7001f..4daa00d 100644 --- a/src/interfaces/api.ts +++ b/src/interfaces/api.ts @@ -1,5 +1,7 @@ import { SortOrder } from "./constants"; - +/** + * Arguments required for getting cat facts from API + */ export type CatFactAPIArgs = { limit: number; page: number; @@ -8,16 +10,25 @@ export type CatFactAPIArgs = { sortByAlphabet: SortOrder | null; }; +/** + * Query strings allowed by Next.js API routes + */ export type NextAPIQueryStrings = Partial<{ [key: string]: string | string[]; }>; +/** + * A cat fact object returned by the API + */ export type Fact = { id?: Number; fact: String; length: Number; }; +/** + * Data object returned by the cat fact API + */ export type CatFactData = { currentPage: number; perPage: number; @@ -25,11 +36,17 @@ export type CatFactData = { facts: Fact[]; }; +/** + * Response object returned by the cat fact API + */ export type CatFactAPIResponse = { data: CatFactData; isError: boolean; }; +/** + * Object representing an error in the API response + */ export type APIError = { error: string; }; diff --git a/src/interfaces/constants.ts b/src/interfaces/constants.ts index 4c33225..0dc6d70 100644 --- a/src/interfaces/constants.ts +++ b/src/interfaces/constants.ts @@ -1,4 +1,11 @@ +/** + * An enum representing the sort order for sorting cat facts. + * @readonly + * @enum {string} + * @property {string} ASC - Sort cat facts in ascending order. + * @property {string} DESC - Sort cat facts in descending order. + */ export enum SortOrder { - ASC = "asc", - DESC = "desc", - } \ No newline at end of file + ASC = "asc", + DESC = "desc", +} diff --git a/src/pages/api/facts.ts b/src/pages/api/facts.ts index f00a7d4..e133c3f 100644 --- a/src/pages/api/facts.ts +++ b/src/pages/api/facts.ts @@ -9,6 +9,13 @@ import { } from "@/utils"; import type { NextApiRequest, NextApiResponse } from "next"; +/** + * Handles the HTTP GET request for the cat facts API endpoint. + * @async + * @param {NextApiRequest} req - The HTTP request object. + * @param {NextApiResponse} res - The HTTP response object. + * @returns {Promise} + */ export default async function handler( req: NextApiRequest, res: NextApiResponse diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 4de24e6..2dd90ab 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,4 +1,3 @@ -import Image from 'next/image' import { Inter } from 'next/font/google' import FactTable from '@/components/FactTable' @@ -7,6 +6,7 @@ const inter = Inter({ subsets: ['latin'] }) export default function Home() { return (
+

Kitty Fact Table

) diff --git a/src/utils/catFactUtils.ts b/src/utils/catFactUtils.ts index 8d56663..c1b1f0f 100644 --- a/src/utils/catFactUtils.ts +++ b/src/utils/catFactUtils.ts @@ -2,6 +2,11 @@ import config from "@/config"; import { NextAPIQueryStrings, CatFactAPIArgs, Fact } from "@/interfaces/api"; import { SortOrder } from "@/interfaces/constants"; +/** + * Parses a set of query strings and returns an object with the corresponding CatFactAPIArgs values. + * @param {NextAPIQueryStrings} query - The set of query strings to parse. + * @returns {CatFactAPIArgs} - An object with the corresponding CatFactAPIArgs values. + */ export const parseQueryStrings = ( query: NextAPIQueryStrings ): CatFactAPIArgs => { @@ -33,6 +38,13 @@ export const parseQueryStrings = ( }; }; +/** + * Adds an ID property to an array of Fact objects, based on the current page and limit. + * @param {Fact[]} facts - An array of Fact objects to add an ID property to. + * @param {number} currentPage - The current page being displayed. Defaults to 1. + * @param {number} limit - The number of facts to display per page. Defaults to the value in the config file. + * @returns {Fact[]} - An array of Fact objects with an ID property added to each object. + */ export const addIDToCatFacts = ( facts: Fact[], currentPage: number = 1, @@ -47,6 +59,12 @@ export const addIDToCatFacts = ( }); }; +/** + * Sorts an array of Fact objects alphabetically. + * @param {Fact[]} facts - An array of Fact objects to sort. + * @param {boolean} sortAsc - Determines if the sorting is ascending or descending. Defaults to true (ascending). + * @returns {Fact[]} - A sorted array of Fact objects. + */ export const sortFactsAlphabetically = ( facts: Fact[], sortAsc = true @@ -62,6 +80,12 @@ export const sortFactsAlphabetically = ( }); }; +/** + * Sorts an array of Fact objects by length. + * @param {Fact[]} facts - An array of Fact objects to sort. + * @param {boolean} sortAsc - Determines if the sorting is ascending or descending. Defaults to true (ascending). + * @returns {Fact[]} - A sorted array of Fact objects. + */ export const sortFactsByLength = (facts: Fact[], sortAsc = true): Fact[] => { return facts.sort((a, b) => { if (a.length > b.length) { @@ -74,6 +98,12 @@ export const sortFactsByLength = (facts: Fact[], sortAsc = true): Fact[] => { }); }; +/** + * Returns a positive number or a fallback value if the number is not positive. + * @param {number|undefined} num - The number to check if it's positive. + * @param {number} fallback - The fallback value to use if the number is not positive. + * @returns {number} - A positive number or the fallback value. + */ function getPositiveNumberOrDefault( num: number | undefined, fallback: number diff --git a/src/utils/env.ts b/src/utils/env.ts index 0f15c98..c7b4bc3 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -2,10 +2,18 @@ import dotenv from "dotenv"; dotenv.config(); -// Keys is a list of keys that were retrieved from environment. +/** + * The list of keys that were retrieved from the environment + * @type {string[]} + */ export const Keys: string[] = []; -// GetString retrieves an environment variable and parses it as string. +/** + * Retrieves an environment variable and parses it as string + * @param {string} key - the name of the environment variable to retrieve + * @param {string} fallback - the fallback value to use if the environment variable is not set or is invalid + * @returns {string} - the value of the environment variable or fallback + */ export function GetString(key: string, fallback: string): string { Keys.push(key); if (process.env[key]) { @@ -14,7 +22,12 @@ export function GetString(key: string, fallback: string): string { return fallback; } -// GetInt retrieves an environment variable and parses it as int. +/** + * Retrieves an environment variable and parses it as an integer + * @param {string} key - the name of the environment variable to retrieve + * @param {number} fallback - the fallback value to use if the environment variable is not set or is invalid + * @returns {number} - the value of the environment variable or fallback + */ export function GetInt(key: string, fallback: number): number { Keys.push(key); if (process.env[key]) { @@ -26,7 +39,12 @@ export function GetInt(key: string, fallback: number): number { return fallback; } -// GetBool retrieves an environment variable and parses it as boolean. +/** + * Retrieves an environment variable and parses it as a boolean + * @param {string} key - the name of the environment variable to retrieve + * @param {boolean} fallback - the fallback value to use if the environment variable is not set or is invalid + * @returns {boolean} - the value of the environment variable or fallback + */ export function GetBool(key: string, fallback: boolean): boolean { Keys.push(key); if (process.env[key]) {