Skip to content

A React + TypeScript app for browsing cat images and breeds using The Cat API. Features modals, infinite scroll, favourites, responsive layout, and Docker support.

Notifications You must be signed in to change notification settings

Alexandros00/catopia-thecatapi-react-ts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🚧 Work in progress – actively improving this React + TypeScript app built with The Cat API.

📦 This is a personal React + TypeScript project using The Cat API. It was originally based on a coding exercise and will be enhanced to demonstrate best practices in frontend development, including API integration, routing, lazy loading, Docker deployment, and testing.

CatLover App 🐱

Description

CatLover as a React Typescript frontend web application, for viewing cat images, see a list with all their breeds as well as various information about each, and also, save images as favourites.

Features

  • Browse cat images: View an endless list of cat images of various breeds and in every kind of photo you can imagine.
  • Load More functionality: The user is able to view a set of 10 images and then load more and more.
  • Click on an image and view its breed: An image can be selected by clicking on it and view the cat's breed*. *Cat breed may not be available on every image.
  • Save an image to your favourites: When clicking on an image, the user can select this image to be saved on favourites to view again later.
  • View all breeds alphabetically: A dedicated page to view all breeds and by clicking on any of them, details and information will appear, about this breed.
  • View your favourite images: By opening the Favourites, the user can view all saved favourites, and remove any of them.
  • View your favourite app in any device: CatLover page is supported in any device (mobile, tablet and desktop/laptop)

Screenshots

Mobile

Home Page / Cat Image list

Home Page / Cat Image list

Selected image

Selected image

Breeds list

Breeds list

Breed details

Breed details

Favourites

Favourites

ist of available pages

Modal with list of available pages

Tablet

Home Page / Cat Image list

Home Page / Cat Image list

Desktop/Laptop

Home Page / Cat Image list

Home Page / Cat Image list

How It Works

  1. Initial Data Fetch: On the first render, the app fetches 10 random images using The Cat API.
  2. Load More functionality: By scrolling to the end, a button can fetch another 10 images, this can be repeated as long as the api provides new images.
  3. Select image, add to Favourites: By clicking on an image, a modal opens providing the ability to add to favourites, or remove it, by communicating with The Cat API. Also, if the image data provide breed data, the same of the breed will appear as a link to breed details. Additionally, by copying the url when the modal is open it can be sent and opened in another browser/machine and the modal will open showing the selected image, while in the back, 10 images will be loaded.
  4. View breeds: When accessing the breed page, the aforementioned load more functionality provides access to 10 more breeds per click.
  5. Favourites page: As mentioned above all saved favourite images will appear here and the user is able to click on any of them and remove them.

Technology Stack

  • Frontend: React, TypeScript, SCSS
  • API: The Cat API
  • Testing: Vitest, Testing Library, Cypress
  • Build Tool: Vite
  • Containerization: Docker

2 Installation

To include the Favourites functionality an API key is needed from The Cat API*

*Without the API key, the app will load and perform well but without the ability to handle favourites! If you do not need this, proceed without an .env file.

  • A file named .env will be needed. This has to be placed in the root folder of the project and should contain the api-key that is used for all the API calls but is required for handling Favourites. This key should be placed in the form of VITE_CAT_API_KEY=XXXXX where XXXXX the key provided by https://thecatapi.com/#pricing

    Additionally, there has to be a user key, to connect you with the backend, so you will be able to retrieve your saved Favourites. this has to have the name: VITE_CAT_SUB_ID=YYYYYY where YYYYYY any string (eg: AS-23AS).

Prerequisites:
  • Git
  • Node.js version 20.10.0
  • npm or yarn (haven't tried it in yarn, but should work)

Run these commands to install and run the app locally (quick and easy):

# Clone the repository (this command needs you to have set up SSH keys in GitHub)
git clone git@github.com:Alexandros00/catopia-thecatapi-react-ts.git

# Move into the project's folder
cd catopia-thecatapi-react-ts

# Install dependencies
npm install

# Start the development server
npm run dev

Runs in http://localhost:3000

Set Up and Run Locally Using Docker

Prerequisites:
  • Git
  • Docker version 27.4.0

Running the following command, docker engine will:

  1. Build a Docker image containing the app.
  2. Create and run a container based on that image.
  3. Serve the app via an Nginx server. Caution: This command will create the docker image only the first time that will run! If the code changes, this command will use the image that already exist. In this case the image has to be deleted or re-created using different command.
docker compose up -d

Command to re-create the image (you can run this also at the first time)

docker compose up -d --build

Work with this app

Prerequisites:
  • Git
  • Node.js version 20.10.0
  • npm or yarn
  • Vite*
  • Docker version 27.4.0 (optional)

If you want to contribute to this app, develop more on this app, or customize it, you can fork this repository.

*Vite is a blazing fast frontend build tool powering the next generation of web applications. (Source: https://vite.dev, accessed on Mar, 2025)

Vite: • Reduces development server startup time compared to older tools like Webpack. • Optimized for modern JavaScript and frontend frameworks. • Simple configuration with support for plugins and customizations. • Supports VanillaJS, React, Vue, and more with support for TypeScript

Testing Strategy

The app should be tested using the following approaches:

  1. Unit Tests: Test individual functions and components for expected behavior.
  2. Integration Tests: Ensure components work together correctly, focusing on key user flows.
  3. End-to-End (E2E) Tests: Simulate real user interactions with the app using Cypress.

To run tests:

# Run unit and integration tests
npm run test

# Open Cypress UI for E2E tests
npm run cypress:open

# Run Cypress E2E tests in the terminal
npm run cypress:run



3 Implementation

Mobile-first approach. When the user visits the page will see a card list of cat images. Due to the mobile-first design, this list has to be vertical. I implemented it using a css grid for proper positioning of the cards and a fixed gap between them. The respective endpoint used to fetch the data. By scrolling down, when an image starts to enter the viewport an animated placeholder appears. When the card enters more, the image is loaded. The purpose of this is to enhance performance. Instead of loading all 10 images at once, even if they are not visible yet, they are loaded when enter the viewport. To achieve this, I used the Intersection Observer API(https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). This API enables the execution of a callback function when a specific element enters, exits, or changes its intersection with another element or the viewport, based on defined thresholds. I set an observer to observe each image and set it to run a callback function when the image enters the viewport. At first, the image has no value in src, so it does not get loaded. the source url is passed in the image's dataset.src = src. In the callback function the value from there is passed to src and the image is loaded. I implemented this (lazy-loading) for card images in order to:

  • Avoid downloading all images immediately when the page loads, something that will increase the page's load time significantly, since we have a lot of images.
  • The browser will render faster without delays for download more images than (currently) needed.
  • Less bandwidth usage, especially for a mobile-first design (costs and possibly slower network in some cases)
  • Additionally, for a mobile-first design, with lazy-loading less computational resources will be needed.
  • Better UX! The page loads faster and then images appear while scrolling, giving a smoother experience.
  • In general, better performance.

So, the 10 random cat images appear on the screen. They are random because The Cat API has a param called "order" with three possible values: "ASC,DESC,RANDOM with RANDOM as default, so I haven't included it on purpose. When clicking on an image, a modal opens with a smooth animation showing the image of the cat, and below, if available, the breed name. The name acts as a link to breed details. I thought that it would be good for this link to navigate to the Breeds page while opening a same modal as above that contains some breed info and a lint of images of cats of that breed. So, back to the first page with the modal displaying a cat's image and its breed. When clicking on the breed gets redirected to the Breeds page (/cat-breeds/:id?) and a modal opens showing information and images of that breed (we will come again on this later). Again, back on the first page with the modal showing a cat image. On the top right of this image there is an icon of a heart, empty, just the border. When the user clicks on this heart icon, the icon will be replaced with a spinner and a call to the backend will send the required data to save this image as favourite and after success the image of a filled heart will take the spinner's place, indicating that the image is saved in Favourites in the backend. When the modal opens, the id of the image is appended in the url like this http://localhost:3000/cat-images/nv so the user can either refresh the page or send it to his/her friends and the modal will open again with the same image and "behind it" there will be a list of 10 random images. There is also an X icon to close the modal or it can be closed by clicking on the background or by pressing ESC, the rest of tha page stays as is with the scroll position as it was. When clicking more, 10 more images are added to the first 10, then if click again, another 10. The images in the modal are not clickable, the only options the modal on this page open are to close it of course, and to add/remove to favourites.

Part II

As in the first view, I created a list of small cards (like buttons), each one containing a breed's name. there is also a button at the bottom to load more. The breeds come in order alphabetically.when clicking on one, a modal opens showing the name and the description of the breed as well as images of cats belonging to that breed and supports scrolling (in modal) in order to be able to view many images. THis modal has a url also, thus, the page can be refreshed and the modal will open again with the same breed's info. When clicking on an image in the modal it redirects to the modal of the first view.

Part III

In the third view, the favourite images are fetched and displayed. When the user clicks on an image, a modal opens, same as the one in the first view. Here, as in the first view, the user can remove an image from the favourites by clicking on the filled heart icon. In this case the image will be removed from the list in the background.

Testing

I installed and set up, vitest, testing-library and cypress in order to create unit tests for functions and for components as well as integration and e2e tests. Initial tests created for key components. More tests to be added as part of ongoing improvements. My strategy on this would be to:

  • Test every function as a unit and while doing it also apply error handlers for every possible case and refactor if and where is needed. With more time allocated, I plan to implement a test-driven development approach, so to implement the tests based on what the function needs to do and then implement the function to act as this.
  • Same for each component, to test that it renders and includes its basic parts eg:some text, image etc.
  • Then integration tests to verify that the components communicate as they should, preferably in pairs, two components per test.
  • Finally, e2e tests simulating user's interaction in the page

Of course in the above with mocking functions (eg. from external libraries) and API calls when needed.

General comments and information

  • The design is intentionally minimal to focus on core functionality first. Styling and UX enhancements are planned.

  • In each image card, before the image being displayed a custom loader appears with a smooth animation for better user experience.

  • In case an image can't be loaded, a placeholder appears by using onerror attribute of img element.

  • As mentioned above, there is configuration for running the app in docker container with an nginx as server for the app.

  • I set a Favourites context and a Favourites provider to keep favourites when changing views (this helps handle favourites in main page and of course to see if an image is already in favourites when clicking it and opens in the modal)

  • I set ErrorBoundary in main.tsx to handle the whole app and a Fallback component to show in case of error. Also, set an ErrorBoundary in image card and wrapped the card component, so if for example an image throw an error on render we will have this:

    Error handled in image card

    Error handled in image card

  • I handle routing with react-router v6 (haven't used v7 yet so i sticked with a familiar one on this). Inside Routes I use lazy loading of the components in Suspense with a custom Loader as fallback.

  • Implemented custom NotFound view that appears in case of wrong utl path.

  • Tried as much as possible (i hope i didn't forget any) to remove logic from React components and apply it in custom hooks.

  • Also, as shown in the screenshots above, added a Navbar on top with a menu on mobile and links on desktop to navigate between pages.

  • Decided to not add much main dependencies so I use, beside React, axios, react-router and react-error-boundary

  • Roboto fonts are installed locally and SCSS variables are used

  • For API calls via axios a class called Service is used with generics for reusability

  • Also, I set GitHub Actions and deployed the app on a remote server by pushing committed code on branch main.

Improvements

  • Testing of course as mentioned above
  • Implement error handling in more places as well as throw errors in more places (in order to catch their exceptions and provide user friendly messages)
  • Also, use a logging service to send all logs there
  • I would improve more the structure based on conceptually related elements to be grouped
  • To split code more based on Single Responsibility Principle
  • To improve styles in all screen sizes
  • Also, to configure pre-commit testing with husky library
  • Create JSDocs, at least for some parts that needed them
  • To use more accessibility enhancements
  • To improve SEO
  • Add debounce in buttons with 200-300ms delay to prevent for example multiple api calls (besides handling this in the service logic)
  • Add a react-toastify for better UX by informing the user on things like "Image has been saved to your Favourites" etc
  • Add a cache for example for Favourites, they can be saved in localStorage and reduce API calls
  • A search input could be added to search for breeds, this could be implemented with a debounce so it won't call the api on every key stroke but after some milliseconds from the last stroke.

Project structure

src/
├── App.tsx
├── api
├── assets
├── components
│   ├── BreedCard
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── BreedDetails
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── CatImageCard
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── CatImageDetails
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── CatImageList
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── CustomPlaceholder
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── ErrorFallback
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── FavouriteCard
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── LazyImage.tsx
│   ├── MyModal
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── Navbar
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── NavbarMenu
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── NotFound
│   │   ├── index.module.scss
│   │   └── index.tsx
│   └── PageLoader
│       ├── index.module.scss
│       └── index.tsx
├── constants
│   └── config.ts
├── contexts
│   ├── FavouritesProvider.tsx
│   └── favouritesContext.tsx
├── hooks
│   ├── useCheckEnvFile.ts
│   ├── useDeleteItem.ts
│   ├── useEscapeClose.ts
│   ├── useFavouritesContext.ts
│   ├── useGetById.ts
│   ├── useGetFavourites.ts
│   ├── useGetItems.ts
│   ├── useHandleAddFavorite.ts
│   ├── useHandleDeleteFavorite.ts
│   ├── useModal.ts
│   ├── usePostItem.ts
│   └── useScrollToTop.ts
├── main.tsx
├── models
│   ├── Breed.ts
│   ├── CatImage.ts
│   ├── Favourite.ts
│   └── FavouriteBodyForSaving.ts
├── pages
│   ├── cat-breeds
│   │   ├── index.module.scss
│   │   └── index.tsx
│   ├── favourites
│   │   ├── index.module.scss
│   │   └── index.tsx
│   └── landing-page
│       ├── index.module.scss
│       └── index.tsx
├── services
│   ├── Service.ts
│   ├── ServiceConfig.ts
│   ├── deleteItem.ts
│   ├── fetchItem.ts
│   ├── fetchItems.ts
│   └── postItem.ts
├── styles
│   ├── global.scss
│   └── variables.scss
├── utils
│   ├── envHelpers.ts
│   ├── helpers.ts
│   └── observer.ts
└── vite-env.d.ts

28 directories, 66 files

Coverage report from v8


 % Coverage report from v8
----------------------------------|---------|----------|---------|---------|-------------------
File                              | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------------------------|---------|----------|---------|---------|-------------------
All files                         |    6.17 |    46.15 |   45.09 |    6.17 |
 src                              |       0 |       50 |      50 |       0 |
  App.tsx                         |       0 |        0 |       0 |       0 | 1-51
  main.tsx                        |       0 |      100 |     100 |       0 | 2-26
 src/components                   |       0 |        0 |       0 |       0 |
  LazyImage.tsx                   |       0 |        0 |       0 |       0 | 1-60
 src/components/BreedCard         |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 3-19
 src/components/BreedDetails      |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 3-57
 src/components/CatImageCard      |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 2-40
 src/components/CatImageDetails   |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 3-89
 src/components/CatImageList      |       0 |        0 |       0 |       0 |
  index.tsx                       |       0 |        0 |       0 |       0 | 1-50
 src/components/CustomPlaceholder |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 2-6
 src/components/ErrorFallback     |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 2-20
 src/components/FavouriteCard     |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 2-27
 src/components/MyModal           |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 2-43
 src/components/Navbar            |   91.66 |      100 |      50 |   91.66 |
  index.tsx                       |   91.66 |      100 |      50 |   91.66 | 11-14
 src/components/NavbarMenu        |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 2-26
 src/components/NotFound          |       0 |        0 |       0 |       0 |
  index.tsx                       |       0 |        0 |       0 |       0 | 1-37
 src/components/PageLoader        |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 2-23
 src/constants                    |     100 |      100 |     100 |     100 |
  config.ts                       |     100 |      100 |     100 |     100 |
 src/contexts                     |       0 |        0 |       0 |       0 |
  FavouritesProvider.tsx          |       0 |        0 |       0 |       0 | 1-19
  favouritesContext.tsx           |       0 |        0 |       0 |       0 | 1-10
 src/hooks                        |       0 |        0 |       0 |       0 |
  useCheckEnvFile.ts              |       0 |        0 |       0 |       0 | 1-15
  useDeleteItem.ts                |       0 |        0 |       0 |       0 | 1-37
  useEscapeClose.ts               |       0 |        0 |       0 |       0 | 1-17
  useFavouritesContext.ts         |       0 |        0 |       0 |       0 | 1-13
  useGetById.ts                   |       0 |        0 |       0 |       0 | 1-54
  useGetFavourites.ts             |       0 |        0 |       0 |       0 | 1-39
  useGetItems.ts                  |       0 |        0 |       0 |       0 | 1-84
  useHandleAddFavorite.ts         |       0 |        0 |       0 |       0 | 1-65
  useHandleDeleteFavorite.ts      |       0 |        0 |       0 |       0 | 1-35
  useModal.ts                     |       0 |        0 |       0 |       0 | 1-43
  usePostItem.ts                  |       0 |        0 |       0 |       0 | 1-50
  useScrollToTop.ts               |       0 |        0 |       0 |       0 | 1-17
 src/models                       |       0 |        0 |       0 |       0 |
  Breed.ts                        |       0 |        0 |       0 |       0 |
  CatImage.ts                     |       0 |        0 |       0 |       0 |
  Favourite.ts                    |       0 |        0 |       0 |       0 |
  FavouriteBodyForSaving.ts       |       0 |        0 |       0 |       0 |
 src/pages/cat-breeds             |       0 |        0 |       0 |       0 |
  index.tsx                       |       0 |        0 |       0 |       0 | 1-79
 src/pages/favourites             |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 2-51
 src/pages/landing-page           |       0 |      100 |     100 |       0 |
  index.tsx                       |       0 |      100 |     100 |       0 | 2-54
 src/services                     |       0 |        0 |       0 |       0 |
  Service.ts                      |       0 |        0 |       0 |       0 | 1-117
  ServiceConfig.ts                |       0 |        0 |       0 |       0 | 1-15
  deleteItem.ts                   |       0 |        0 |       0 |       0 | 1-19
  fetchItem.ts                    |       0 |        0 |       0 |       0 | 1-21
  fetchItems.ts                   |       0 |        0 |       0 |       0 | 1-22
  postItem.ts                     |       0 |        0 |       0 |       0 | 1-23
 src/utils                        |   40.35 |    66.66 |   71.42 |   40.35 |
  envHelpers.ts                   |     100 |      100 |     100 |     100 |
  helpers.ts                      |   63.15 |       60 |      75 |   63.15 | 12-18
  observer.ts                     |       0 |        0 |       0 |       0 | 1-29
----------------------------------|---------|----------|---------|---------|-------------------

About

A React + TypeScript app for browsing cat images and breeds using The Cat API. Features modals, infinite scroll, favourites, responsive layout, and Docker support.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published